PennHan'blog


  • 首页

  • 归档

Android Activity之间的通信方式

发表于 2019-04-14

Activity之间通信方式

  • Intent
  • 借助类的静态变量
  • 借助全局变量/Application
  • 借助外部工具
    • 借助SharedPreference
    • 使用Android数据库SQLite
    • 赤裸裸的使用File
    • Android剪切板

Intent

借助四大组件(Activity、Service、BroadcastReceiver、ContentProvider),通过Intent传递数据,进行Activity间通信。

  1. Activity跳转传递Intent
  2. 广播传递Intent
  3. ContentProvider保存数据,B获取数据。
  4. Service

借助类的静态变量

B中定义静态变量,A中设置B中的静态变量,B中就能获取到A设置的静态变量,当然该静态变量可以设置在任意地方。

借助全局变量来实现/Application

一般可以把变量放在Applicaiton这个全局的变量中,通过获取Applicaiton来设置和获取数据变量。

也可以设置单例模式。道理同Applicaiton

借助外部工具

  1. sp存储
  2. 数据库存储
  3. 文件存储
  4. Android剪贴板等存储方式

Activity和Service间通信

  • startService
  • bindService
  • 回调接口
  • broadcast(广播)

startService

单向的,只能Activity向Service传递数据,该service不与Activity绑定。

bindService

双向传递数据,通过bindService向Service传递数据,通过ServiceConnection向Activity传递数据。

回调接口

1
2
3
public interface OnProgressListener {
void onProgress(int progress);
}

Service 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class MsgService extends Service {
/**
* 进度条的最大值
*/
public static final int MAX_PROGRESS = 100;
/**
* 进度条的进度值
*/
private int progress = 0;
/**
* 更新进度的回调接口
*/
private OnProgressListener onProgressListener;
/**
* 注册回调接口的方法,供外部调用
* @param onProgressListener
*/
public void setOnProgressListener(OnProgressListener onProgressListener) {
this.onProgressListener = onProgressListener;
}
/**
* 增加get()方法,供Activity调用
* @return 下载进度
*/
public int getProgress() {
return progress;
}
/**
* 模拟下载任务,每秒钟更新一次
*/
public void startDownLoad(){
new Thread(new Runnable() {
@Override
public void run() {
while(progress < MAX_PROGRESS){
progress += 5;
//进度发生变化通知调用方
if(onProgressListener != null){
onProgressListener.onProgress(progress);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
/**
* 返回一个Binder对象
*/
@Override
public IBinder onBind(Intent intent) {
return new MsgBinder();
}
public class MsgBinder extends Binder{
/**
* 获取当前Service的实例
* @return
*/
public MsgService getService(){
return MsgService.this;
}
}
}

Activity代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MainActivity extends Activity {
private MsgService msgService;
private ProgressBar mProgressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//绑定Service
Intent intent = new Intent("com.example.communication.MSG_ACTION");
bindService(intent, conn, Context.BIND_AUTO_CREATE);
mProgressBar = (ProgressBar) findViewById(R.id.progressBar1);
Button mButton = (Button) findViewById(R.id.button1);
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//开始下载
msgService.startDownLoad();
}
});
}
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//返回一个MsgService对象
msgService = ((MsgService.MsgBinder)service).getService();
//注册回调接口来接收下载进度的变化
msgService.setOnProgressListener(new OnProgressListener() {
@Override
public void onProgress(int progress) {
mProgressBar.setProgress(progress);
}
});
}
};
@Override
protected void onDestroy() {
unbindService(conn);
super.onDestroy();
}
}

通过broadcast(广播)的形式

Activity注册广播,Service发送广播给Activity接收。

Activity和Fragment间的通信

  • 回调接口
  • getActivity()
  • 广播
  • handler

Linux常用命令

发表于 2019-03-26

linux命令行中|为管道输出。

查找

一般查找
1
grep 关键字 文件名
压缩文件查找
1
gzip -dc 文件名|grep -a -E '关键字正则'|grep...
压缩文件输出
1
zcat 文件名

查看日志

实时输出日志
1
tail -200f 文件名
查询日志内容
1
2
grep 略
less 文件名 + /关键字

查看磁盘空间占用

1
df -h

20190325142259-image.png

查看当前文件夹下各文件空间占用
1
du --max-depth=1 -h

20190325142450-image.png

查看当前文件夹占用空间大小
1
du -sh

20190325144219-image.png

Android提升计划

发表于 2019-02-18

必备

  • 动画
  • 各种ServiceManager

非必备

  • 算法。各种排序算法,冒泡、快排等

2019计划

发表于 2019-02-13

2019年2月13日制定

职场计划

短期计划

一. 两个月之内跳槽(2019年4月15号)

  • 跳槽目标:小公司
  • 薪资:22k起

薪资情况和公司福利挂钩,包括公积金,按照实际情况再定

二. 芝士留学项目学生端(2019年3月15号)

  • 项目结束大体结束

短期计划时间安排

  • 每周加班4天,7:00-9:00
  • 周六周日 两天8小时

长期计划

2019年:

  1. 小公司作为跳板,在此基础上再找,争取19年工资到25k-30k
  2. 公司要求是一线大公司。(BATJ等等)

买房计划

现有100000

公积金:62000

19年

20190213172342-c1ac7e08acdc3c765245f5a1081e59d.png

累计所得

工资:

100000 + 16810 10 + 11800 2 - 住房:3000 12 + 花销:2000 12 = 231700

公积金:

62000 + 2160 10 + 1440 2 = 86480

20年

20190213172654-image.png

累计所得

工资:

231700 + 18855.60 12 - 住房:3000 12 + 花销:2000 * 12 = 400000

公积金:

86480 + 2400 * 12 = 115280

买房

首付:30000 50m^2 0.3=450000

人生计划

20年底买房之后结婚

Android Studio常用插件汇总

发表于 2019-01-03

Android Studio插件汇总

Android ButterKnife Zelezny

20190301111617-4002920-931e11c6e8337e22.gif

侧边栏插件 CodeGlance

20190301111721-4002920-302348284aa5cd14.gif

码云扩展插件 Gitee

20190301111833-image.png

可视化数据库调试工具 stetho

20190301112405-image.png

参考文章:https://www.jianshu.com/p/03da9f91f41f

单例模式插件 singleton

20190301112513-image.png

Gson字符串转实体类 GsonFormat

20190306163217-687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f3136363836362d666639646333333661663732643764372e6769663f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970.gif

前端脱离后台的敏捷开发

发表于 2018-11-18

需求

开发一个项目时一般都会有前台、后台。前台负责展示数据、用户交互;后台负责数据的处理和存储。

前提

  • ✦开发阶段,前台和后台按照接口文档进行开发,这套接口规范相当于前台和后台之间交互的协议。
  • ✦一般来说前台和后台的开发进度都会有偏差。

问题

  1. ✦在开发阶段,前端的展示和交互需要依赖后台的数据;而后台的开发不需要前端配合,因为后台可以自己写请求做测试。
  2. ✦要提高效率,就需要在开发阶段结束,联调阶段开始之前,尽可能的提高代码完成度和代码准确性。

所以,就引出了以下需求:前端脱离后台的独立开发。

这里的独立开发不是单纯的脱离后台的开发,而是可以保证代码完成度和代码准确性前提下的,脱离后台的开发。

目的

前端独立开发所要达到的目的就是:

  • ✦ 在连调阶段之前,完成绝大部分的编码工作,工作量尽可能往前拉
  • ✦ 与后台平行工作,互不干扰,提高了工作效率。
  • ✦ 保证编码的准确性。

#最理想的情况是,在连调阶段连调一个接口,我不用修改一行代码,或者只需要切换一下环境就完成了一个接口的连调,包括接口周边逻辑的测试。#

解决方法

  • ✦ 前端使用写死的假数据
  • ✦ 使用挡板(打转工具)

使用挡板

  • ✦ 自己开发挡板
  • ✦ Mock.js
  • ✦ Moco
自己开发挡板
  1. 自己写一套服务,部署在服务器上。
  2. 通过Web服务录入数据到mongoDB。
  3. 提供给前端的接口,并且接口数据直接从mongoDB读取返回。

优点:自己开发的挡板比较放心?
缺点:开发挡板需要花时间。

Mock.js

实例教程
生成随机数据,拦截Ajax请求,使用如下:

1
2
3
4
5
6
7
8
9
10
11
// 使用 Mock
var Mock = require('mockjs')
var data = Mock.mock({
// 属性 list 的值是一个数组,其中含有 1 到 10 个元素
'list|1-10': [{
// 属性 id 是一个自增数,起始值为 1,每次增 1
'id|+1': 1
}]
})
// 输出结果
console.log(JSON.stringify(data, null, 4))
  1. 配置模拟数据:
1
2
3
4
5
Mock.mock('http://g.cn', {
'name' : '[@name](/user/name)()',
'age|1-100': 100,
'color' : '[@color](/user/color)'
});
  1. 发送ajax请求(jquery版)
1
2
3
4
5
6
7
$.ajax({
url: 'http://g.cn',
}).done(function(data, status, xhr){
console.log(
JSON.stringify(data, null, 4)
)
})
  1. 查看响应的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
// 结果1
{
"name": "Elizabeth Hall",
"age": 91,
"color": "#0e64ea"
}
// 结果2
{
"name": "Michael Taylor",
"age": 61,
"color": "#081086"
}
// 结果N ..

优点:随机数据
缺点:有学习成本,前端。

Moco

写一个接口的配置文件:

1
2
3
4
5
6
7
8
[
{
"response" :
{
"text" : "Hello, Moco"
}
}
]

将配置文件和jar包放到服务器上,执行以下命令:

1
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json

优点:简单易用,工作量极小,适用范围广。
缺点:不能随机数据。

辅助工具

postman–接口测试工具

参照通过postman提升接口测试效率

Transmit–FTP工具

官网传送
破解版传送

Android获取设备唯一标识

发表于 2018-01-10

Android获取设备唯一标识

需求

对于移动端产品来说,能准确的识别、定位、跟踪一台设备是一件很重要的事,基于设备唯一性可以做的事有很多,比如设备锁、防作弊、安全防护等。但是在android和ios两大阵营上,都没有能标识设备唯一性的ID供开发者采集,所以就有了本篇文章,让我们来一起分析下android端可以用来标识唯一设备的属性、优缺点及解决方案。

备选设备ID

IMEI

国际移动设备识别码(International Mobile Equipment Identity,IMEI),即通常所说的手机序列号、手机“串号”,用于在移动电话网络中识别每一部独立的手机等行动通讯装置,相当于移动电话的身份证。序列号共有15位数字,前6位(TAC)是型号核准号码,代表手机类型。接着2位(FAC)是最后装配号,代表产地。后6位(SNR)是串号,代表生产顺序号。最后1位(SP)一般为0,是检验码,备用。

获取方式
1
2
TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String imei = manager.getDeviceId();
问题
  • 可以被作弊软件修改。
  • 厂商定制系统中的Bug:少数手机设备上,由于该实现有漏洞,会返回垃圾。如000000000000000、11111111111111111.
  • 手机管家,360手机助手,和一些厂商ROM自带的权限管家,有阻止应用采集手机IMEI的功能
  • 山寨手机厂商为了节省成本,批量复制重复IMEI串烧进手机的EEPROM里面。

Mac Address

wifi无线网卡的MAC地址,与终端硬件关联,可用作设备的唯一标识。

获取方式
1
2
3
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = wifi.getConnectionInfo();
String mac = info.getMacAddress();
问题

可以被作弊软件修改。
Android6.0及以上的系统上,通过上面的系统接口采集到的MAC地址返回的是一个固定串:02:00:00:00:00:00

IMSI

国际移动用户识别码(IMSI:International Mobile Subscriber Identification Number)是区别移动用户的标志,储存在SIM卡中,可用于区别移动用户的有效信息。其总长度不超过15位,同样使用0~9的数字。其中MCC是移动用户所属国家代号,占3位数字,中国的MCC规定为460;MNC是移动网号码,由两位或者三位数字组成,中国移动的移动网络编码(MNC)为00;用于识别移动用户所归属的移动通信网;MSIN是移动用户识别码,用以识别某一移动通信网中的移动用户。此ID是跟SIM卡绑定的,更换SIM卡就会发生变化,同样在仅支持wifi的pad设备上是没有的。

获取方式
1
2
TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String imsi = manager.getSubscriberId();

#####问题
存在无SIM卡的终端设备获取不到的问题
最大的问题是会发生变化,与终端设备非强绑定。

Android_ID

Android_ID是设备首次启动时,系统生成的一个唯一串号,长16字节(例:70560687d711af97),由com.android.providers.settings 这个系统程序所管理,Android6.0 以下储存在/data/data/com.android.providers.settings/databases/settings.db中的secure 表。

获取方式
1
Settings.Secure.getString(context.getContentResolver(), "android_id")
问题

此ID与硬件无关,与Android系统有关,所以当系统还原出厂设置,刷机时这个ID就会重新生成了。
由于国内的手机基本都是厂商定制的ROM,所以此ID的控制权均由厂商ROM层来控制,难免会发生一些bug,导致部分机型上的ID都相同。
另外在OTA ROM升级的时候,有些厂商会保留原有db信息不变,有些厂商会清除重新生成。
与系统强依赖,稳定性不足。

CID

Card Identification Register(CID),顾名思义,做为存储卡片的唯一识别信息,规范上要求每张卡片的ID都唯一。CID 寄存器有 16 字节长,如下表 所示,它包含了本卡的特别识别码(ID 号)。 这些信息是在卡的生产期间被编程(烧录),主控制器不 能修改它们的内容。 注意:SD卡的 CID 寄存器和 MMC 卡的 CID 寄存器在记录结构上是不同的。一般来讲MMC0 保存的是内置存储卡的信息,MMC1 保存的是扩展存储卡也就是SD卡的信息。因为SD卡跟终端设备又不是强关联的,会因为更换升级发生变化,那么我们主要看手机内置存储卡的CID。

获取方式
1
adb shell cat /sys/class/mmc_host/mmc*/mmc*:*/cid

#####问题
在部分山寨低端机器上,会出现采集不到和采集的是SD卡的CID。

方案

通过以上对可能作为设备唯一ID的属性分析后可以看到,没有一个属性可以作为标识设备的唯一ID,我们可能抱着90%多的有效率,选择某一个属性作为设备的唯一标识,又不可避免android端复杂的系统、定制厂商、root等环境。所以这里给出的方案又不能称之为方案,只是尽可能的标识唯一设备。所以方案可能是这样的:

按照以上分析的稳定性和可靠性分下先后顺序:

##CID > IMEI > MAC > ANDROID_ID##
生成deviceId规则为:
按照以上先后顺序依次获取,判断取得的值的有效性,有效的场合返回,无效的场合获取下一个值;直到取到有效值,都无效的场合使用UUID随机生成。
在尽可能获得设备唯一ID的基础上,将获得的ID保存在本地,下次直接从内存或外部存储中读取。这样尽量保证获取的设备id的唯一性。

Tip:保存到SD卡的设备ID可以保存为”.xxxx.txt”的形式,隐藏文件也能尽可能不会被用户误删。

所以,所谓的方案只是提供了一种简单的思路,不是什么完美的解决方案。

兼容性问题

大家都知道在Android6.0系统上,新增了不少特性,与终端ID采集密切相关的就是权限控制这个特性,结论如下:

IMEI,IMSI需用户手动授权方可采集。
6.0里面READ_PHONE_STATE权限被归属成敏感权限,在运行时,如果应用程序获取TelephonyManager实例时,系统就会弹出是否允许应用拨打电话和管理通话的选择对话框,用户拒绝之后,就采集不到。
针对6.0及以上的系统,ID的采集需要进行兼容,使用 checkPermission 接口从内存中直接获取权限数据,判断用户是否已经授权。

MAC地址通过非系统接口可采集(6.0以下通过系统接口采集),采集场景仅限于处于wifi连接状态。
非系统接口,其实指的就是直接读取文件的方式,如下:

1
2
cat /sys/class/net/wlan0/address 
cat /sys/devices/virtual/net/wlan0/address

AndroidID采集不受影响,但此ID不与硬件绑定,刷机,系统还原,升级等场景会发生变化,不稳定。

说明:应用编译时选择的targetSdkVersion>=23,在6.0及以上的系统运行才会触发权限提示框,<23打出的apk包不受此影响。

备注

以上大部分内容引自腾讯质量开放平台,在此感谢。

Kotlin初探

发表于 2017-06-21

Kotlin语言介绍

  • 来源:Kotlin是JetBrains在2010年推出的基于JVM的新编程语言
  • 用途:服务器端程序、Android 应用程序或者在浏览器中运行的前端程序
  • 优势:
    1. 简洁、易表现: 你可以编写少得多的代码,并且代码的可读性可能更好.
    2. 安全: 在编译初期就处理了各种null的情况,避免了执行时异常。
    3. 互操作性:你可以继续使用所有的你用Java写的代码和库,因为两个语言之间的互操作性是完美的。
    4. 甚至可以在一个项目中使用Kotlin和Java两种语言混合编程
    5. 函数式支持(Lambdas)
    6. 代理
    7. …

Kotlin概览(vs Java)

— | — | —
定义 | Java | Kotlin
变量|String string = “Hello”;|var string: String = “Hello”
final变量 |final String string = “Hello”;|const val string: String = “Hello”
函数 |public boolean testString(String name){…} |open fun testString(val name:String): boolean {…}
数组|String[] names = new String[]{“Kyo”, “Ryu”, “Iory”}; String[] emptyStrings = new String[10];
|val ints = intArrayOf(1, 3, 5)
变长参数 |void hello(String… names){…}|fun hello(vararg names: String){
}
三元运算符 |int code = isSuccessfully? 200: 400;|int code = if(isSuccessfully) 200 else 400
main 函数 |class Main{
public static void main(String… args){
…
}
} |fun main(args: Array){
…
}
实例化类 |Date date = new Date(); | val date = Date()
延迟初始化成员变量 |public class Hello{
private final String name = “Peter”;
}|class Hello{
private val name by lazy{
NameProvider.getName()
}
}
获得 class 的实例 |public class Hello{
…
}
…
Class<?> clazz = Hello.class;
Hello hello = new Hello();
Class<?> clazz2 = hello.getClass();
|class Hello
val clazz = Hello::class.java
val hello = Hello()
val clazz2 = hello.javaClass

Kotlin特性概览

简洁,使用更少的代码做更多的事

这是我认为Kotlin最大的优点。更像一种描述性编程语言。

例子一:数据类

数据类大量重复的getter和setter相信会是很多人在开发过程中吐槽的一个点。举一个很经典的例子,我们需要一个Person的数据类。

在Java中,需要这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Person {
private String name;
private int age;
private int sex;
private float height;
public Person(String name, int age, int sex, float height) {
this.name = name;
this.sex = sex;
this.age = age;
this.height = height;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", sex=" + (sex == 0 ? "男" : "女") +
", height=" + height +
'}';
}

在Kotlin里,我们只需要一行代码就能完成以上的功能:

1
2
3
4
data class Person(var name: String,
var age: Int,
var sex: Int,
var height: Float)

例子二:区间表达式

在Java中我们经常要写这样的代码:

1
2
3
for(int i = 0; i <= 10; i++){
System.out.println(i)
}

但是在Kotlin中,支持 .. 操作符形式的区间表达式,我们转换成Kotlin就变成了这样:

1
2
3
for(i in 0..10){
println(i)
}

不仅如此,还有更多相关的功能。

1
2
3
4
5
6
7
8
9
10
11
12
//倒序迭代
for(i in 10 downTo 0){
...
}
//步长为2的迭代
for(i in 0..10 setp 2){
...
}
//i在[0,10)区间,排除了10
for(i in 0 until 10){
...
}

例子三:Lamda表达式

Java中:

1
2
3
4
5
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//dosomething
}});

Kotlin中:

1
2
3
btn.setOnClickListener { 
//dosomething
}

例子四:类扩展

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' 对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
}
//使用
val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // 'swap()' 内部的 'this' 得到 'l' 的值
fun Fragment.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(getActivity(), message, duration).show()
}
//使用
fragment.toast("Hello world!")

安全

例子一:空安全

一直以来,NullPointException空指针异常在开发中是最低级也最致命的问题。我们往往需要进行各种null的判断以试图去避免NPE的发生。Kotlin基于这个问题,提出了一个空安全的概念,即每个属性默认不可为null。

如果要允许为空,我们需要手动声明一个变量为可空字符串类型,写为String?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var a: String = "abcd"
a = null //编译错误
var a: String? = "abcd"
a = null //编译成功
var person: Person? = null
person.name = "shinelw" //编译失败
person?.name = "shinelw" //编译成功
// 这里不能通过编译. Artist 不能是null
var notNullArtist: Artist = null
// Artist 可以是 null
var artist: Artist? = null
// 无法编译, artist可能是null,我们需要进行处理
artist.print()
// 只要在artist != null时才会打印
artist?.print()
// 智能转换. 如果我们在之前进行了空检查,则不需要使用安全调用操作符调用
if (artist != null) {
artist.print()
}
// 只有在确保artist不是null的情况下才能这么调用,否则它会抛出异常
artist!!.print()
// 使用Elvis操作符来给定一个在是null的情况下的替代值
val name = artist?.name ?: "empty"

与Java互操作(混编)

这是Kotlin与Scala相比,优势突出的一点。我们可以在Kotlin中调用现存的Java代码,并且也能在Java代码中顺利的调用Kotlin代码。这意味着我们可以马上在现有的Java项目中使用上Kotlin,同时所有之前旧的Java也一样有效。

Kotlin 调用 Java

大多数Java代码,可以直接调用

1
2
3
4
5
import java.util.*
fun demo() {
val list = ArrayList<string>()
list.add("hello world")
}

Java 调用 Kotlin

Kotlin 属性会编译成以下 Java 元素:

  • 一个 getter 方法,名称通过加前缀 get 算出;
  • 一个 setter 方法,名称通过加前缀 set 算出(只适用于 var 属性);
  • 一个私有字段,与属性名称相同(仅适用于具有幕后字段的属性)。

例如,var firstName: String 编译成以下 Java 声明:

1
2
3
4
5
6
7
private String firstName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}

在 org.foo.bar 包内的 example.kt 文件中声明的所有的函数和属性,包括扩展函数, 都编译成一个名为 org.foo.bar.ExampleKt 的 Java 类的静态方法。

1
2
3
4
5
6
7
8
// example.kt
package demo
class Foo
fun bar() {
}
// Java
new demo.Foo();
demo.ExampleKt.bar();

函数

在Kotlin中,函数的声明使用fun关键字:

1
2
fun double(x: Int): Int {
}

使用:

1
val result = double(2)

使用.操作符调用成员函数:

1
Simple().foo() //创建累的实例并调用foo()方法

中缀符号

当满足下列条件的时候,函数还可以使用中缀符号调用:

  • 当函数是成员函数或者扩展函数的时候
  • 当他们仅有一个参数的时候
  • 当他们通过infix关键字标记的时候

默认参数

1
2
3
fun read(b:Array<Byte>,off:Int = 0,len:Int = b.size()){
...
}

返回Unit类型的函数

相当于java的void类型:

1
2
3
4
5
6
7
fun printHello(name: String?): Unit {
if (name != null)
println("Hello ${name}")
else
println("Hi there!")
// `return Unit` or `return` is optional
}

单表达式函数

当一个函数仅返回一个表达式,则可以省略花括号,函数体可以直接跟在=号后面:

1
fun double(x: Int): Int = x * 2

高阶函数和lambda表达式

高阶函数就是可以接受函数作为参数并返回一个函数的函数:

1
2
3
4
5
6
7
8
9
fun <T> lock(lock: Lock, body: () -> T ) : T {
lock.lock()
try {
return body()
}
finally {
lock.unlock()
}
}

一般是传递一个字面函数(通常是 lambda 表达式):

1
2
val result = lock(lock, {
sharedResource.operation() })

字面函数和函数表达式

字面函数或函数表达式就是一个 “匿名函数”,也就是没有声明的函数,但立即作为表达式传递下去:

1
max(strings, {a, b -> a.length < b.length })

代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
**
* 被代理接口
*/
interface Base {
fun display()
}
/**
* 被代理类
*/
open class BaseImpl : Base {
override fun display() = print("just display me.")
}
/**
* 代理类,使用:以及关键词by进行声明
* 许代理属性和其他继承属性共用
* class Drived(base: Base) : Base by base,BaseImpl()
*/
class Drived(base: Base) : Base by base
//使用
fun show() {
var b = BaseImpl()
var drived = Drived(b)
drived.display()
}
**
* 代理类型:
* 懒加载:Lazy
* 观察者:Delegates.observable()
* 非空属性:Delegates.notNull<>()
*/
class Water {
public var weight:Int by Delegates.notNull()
/**
* 代理属性
*/
public val name: String by lazy {
println("Lazy.......")
"Code4Android"
}
public var value: String by Delegates.observable("init value") {
d, old, new ->
println("$d-->$old->$new")
}
}
fun main(arg: Array<String>) {
show()
var water = Water()
println(water.name)
println(water.name)
water.value = "11111"
water.value = "22222"
water.value = "33333"
println(water.value)
println(water.value)
//必须先赋值否则IllegalStateException: Property weight should be initialized before get.
water.weight=2
print(water.weight)
}

Use Android APIs with Kotlin

声明Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//in Kotlin
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity)
}
}
//in Java
public class MyActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
}
}

On-click listener

1
2
3
4
5
6
7
8
9
10
11
12
13
//in Kotlin
val fab = findViewById(R.id.fab) as FloatingActionButton
fab.setOnClickListener {
...
}
//in Java
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
...
}
});

Item click listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//in Kotlin
private val mOnNavigationItemSelectedListener
= BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
mTextMessage.setText(R.string.title_home)
return@OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
mTextMessage.setText(R.string.title_dashboard)
return@OnNavigationItemSelectedListener true
}
}
false
}
//in Java
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_home:
mTextMessage.setText(R.string.title_home);
return true;
case R.id.navigation_dashboard:
mTextMessage.setText(R.string.title_dashboard);
return true;
}
return false;
}
};

学习资料

  • 官方文档http://kotlinlang.org/docs/reference/
  • 中文文档http://www.kotlincn.net/docs/reference/
  • Kotlin语言中文教程-GitBookhttps://huanglizhuo.gitbooks.io/kotlin-in-chinese/content/
  • Kotlin for android中文教程https://wangjiegulu.gitbooks.io/kotlin-for-android-developers-zh/zhe_ben_shu_shi_he_ni_ma_ff1f.html
  • java与Kotlin混编http://kotlinlang.org/docs/reference/java-interop.html

微信小程序项目开发总结

发表于 2017-06-16

一、基础知识

小程序相关的项目构建,语法,设计等全部内容都在微信小程序的官方文档有详细的描述,所以基础知识很容易掌握,这里给出开发过程中需要一直翻看的官方文档地址;小程序的页面也有自己的一套设计规范,如果对产品没有特殊的要求,可以按照微信官方的设计规范进行UI设计,或则对产品的设计起到一个参考的作用。有必要一直翻看的还有WeUI:

  • 代码地址:GitHub
  • 设计规范:GitHub

因为小程序本身是微信的一部分,所以设计和使用上多少要和微信保持同步。有了这三个文档,基本就可以将小程序从无到有的搭建起来了,也得益于微信给力的官方文档,很细致。

二、项目框架选型

微信web开发者工具

使用微信自己的开发者工具,开发、调试、测试,提交都可以做,但是编码体验很不好,一般的做法是使用webstorm编码,微信web开发者工具只起到编译看效果,调试的作用。能大大提高编码效率。
webstorm引入微信小程序API live template :

  1. File -> import settings引入wecharCode.jar
  2. 重启后,在编码时使用wx.就会有代码提示

使用gulp进行编译构建

由于微信小程序本身对工程化几乎没有任何的支持,所以动手搭建一份:wxapp-redux-starter。
使用gulp进行编译构建,主要功能包括:

  • ✦ 集成了Redux,数据管理更方便
  • ✦ 开发过程中,使用xml取代wxml,对开发工具更友好
  • ✦ 开发过程中,使用less取代wxss,功能更强大
  • ✦ 引入es-promise,以便可以创建并使用Promise
  • ✦ 添加promisify工具函数,可以便捷的将官方Api转换成Pormise模式
  • ✦ 引入normalizr,可以将数据扁平化,更方便进行数据管理
  • ✦ 引入babel自动进行ES2015特性转换
  • ✦ 对wxml/wxss/js/img压缩,相对开发者工具提供的压缩,会减小一丢丢体积。

具体参考这篇文章

三、Tips

  • Webstorm 静态检查为警告的部分,要尽量修正,规避掉一些不必要的bug。
  • 规范小程序的目录结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    +—-project
    +——–common(公共代码)
    +——–http(网络请求)
    +——–icons(存放小图标)
    +——–model(模块工具)
    +——–pages(页面)
    +——–templates(模版)
    +——–utils(工具)
    +——–work(文档等)
    +——–app.js、app.json、app.wxss
  • javascript语言虽然语句末尾可以不用’;’,但是为了规范都要加上’;’.

  • 根据小程序设计规范将各部分的标准样式整理到app.wxss里全应用统一使用。具体参照app.wxss
  • js文件的编码顺序建议如下:
    1. data: {}
    2. 生命周期方法
    3. 页面点击响应方法
    4. 私有方法(前面加下划线)
  • 网络返回的数据建议写法如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    //建议写法,因为数据结构可以一眼看出来
    let data = response["data"];
    let username = data["username"];

    //不建议写法,不容易看出数据的结构,不容易找错
    let data = response.data;
    let username = data.username;
    使用ES6特性–箭头表达式


    // ES5
    var net = require('../public/net');
    Page({
    data: {
    list: []
    },
    onShow: function () {
    var self = this;//ES5写法,要在这赋值
    net.get('/Index/getList', function (res) {
    res = res || {};
    var status = res.status,
    data = res.data,
    list = data.list;
    if(status == 2) {
    self.setData({list: list});//在这用,很容易忘了
    }
    });
    }
    });
    // ES6
    import * as net from '../public/net';
    Page({
    data: {
    list: []
    },
    onShow: function () {
    net.get('/Index/getList', (res = {}) => {
    let {status, data: {list}} = res;
    if (status == 2) {
    // 此处 this 的指向与 onShow 内一致,无需增加 self 变量
    this.setData({list});
    }
    });
    }
    });
    使用ES6特性–数组筛选

    // js
    var helpers = {
    // 判断是否有时间参数
    hasTime: (i) => {
    return !isNaN(parseInt(i.stamp));
    },
    // 时间转换处理
    parseTime: (i) => {
    i.time = new Date(i.stamp + '000');
    return i;
    }
    };
    net.get('/Index/getList', (res = {}) => {
    let {status, data: {list}} = res;
    this.set({
    list: list.filter(helpers.hasTime) // 筛选掉没有时间戳字段的数据
    .map(helpers.parseTime) // 将时间戳字段转化为 JS 的 Date 对象
    });
    });

四、其他

使用开发工具的正确姿势

微信提供的开发工具的编辑功能实在是太难用,用它写代码简直就是在浪费生命.它的正确用法##作为运行和调试工具.##

那么适合作为编辑工具的是: ##webStorm##.

基于IntelJ内核,开启Dracula主题,跟Android studio的使用习惯非常接近,so cool!各种方法提示,自动保存,快速查找…应有尽有.闭源的微信开发工具就不要用来写代码了,珍惜生命.
webStorm要识别wxml和wxss,还需要配置一下文件类型:

Preferences -> File Types -> HTML : 添加.wxml
Preferences -> File Types -> CasCading Style Sheet : 添加
.wxss

开发时,使用webstorm写代码,web开发者工具调试、运行,效率会高很多。

网络请求的封装

微信提供了网络请求的API,但是真实项目中使用时,还是需要封装一下的。

1
2
3
4
5
6
7
8
9
10
11
12
13
wx.request({
url: 'test.php', //仅为示例,并非真实的接口地址
data: {
x: '' ,
y: ''
},
header: {
'content-type': 'application/json'
},
success: function(res) {
console.log(res.data)
}
})

封装微信网络框架

  1. 定义好携带构建请求的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    function requestConfig() {
    //url
    this.url = '';
    //请求头
    this.header = {
    'Content-Type': 'application/json',
    };
    //请求参数
    this.params = {};
    //是否显示等待对话框
    this.loading = true;
    this.netMethod = 'POST';
    //通信加解密数据
    this.safeSdk = {};
    this.callback = {
    httpCallPreFilter: () => {
    //网络请求前处理
    },
    httpCallBackPreFilter: (response) => {
    //网络请求返回前处理
    },
    doHttpSucess: (response) => {
    //网络请求成功--公共处理
    },
    doHttpFailure: (response) => {
    //网络请求失败--公共处理
    },
    doComplete: () => {
    //网络请求结束--公共处理
    }
    };
    this.setMethodGet = function () {
    this.netMethod = 'GET';
    return this;
    };
    this.send = function () {
    _request(this);
    };
    this.upload = function (filePath) {
    _upload(this, filePath);
    }
    }

构建requestConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 注意,此方法调用后还要调用.send()才是发送出去.
* @param Interfaces
* @param protocol
* @param params
* @param callback 拷贝上方注释区的代码使用
* @param loading
* @returns {requestConfig}
*/
function buildRequest(url, params, callback, loading) {
let config = new requestConfig();
config.loading = typeof loading === 'undefined' ? true : loading;
let baseUrl = "";
baseUrl = constants.url;
config.url = baseUrl + url;
config.params = params;
if (_isFunction(callback.httpCallPreFilter)) {
let pubHttpCallPreFilter = config.callback.httpCallPreFilter;
config.callback.httpCallPreFilter = () => {
if (!callback.httpCallPreFilter()) {
pubHttpCallPreFilter();
}
}
}
if (_isFunction(callback.httpCallBackPreFilter)) {
let pubHttpCallBackPreFilter = config.callback.httpCallBackPreFilter;
config.callback.httpCallBackPreFilter = (response) => {
if (!callback.httpCallBackPreFilter(response)) {
pubHttpCallBackPreFilter(response);
}
}
}
if (_isFunction(callback.doHttpSucess)) {
let pubDoHttpSucess = config.callback.doHttpSucess;
config.callback.doHttpSucess = (response) => {
if (!callback.doHttpSucess(response)) {
pubDoHttpSucess(response);
}
}
}
if (_isFunction(callback.doHttpFailure)) {
let pubDoHttpFailure = config.callback.doHttpFailure;
config.callback.doHttpFailure = (response) => {
if (!callback.doHttpFailure(response)) {
pubDoHttpFailure(response);
}
}
}
if (_isFunction(callback.doComplete)) {
let pubDoComplete = config.callback.doComplete;
config.callback.doComplete = () => {
if (!callback.doComplete()) {
pubDoComplete();
}
}
}
return config;
}

发送请求和上传文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 请求数据
* @param requestConfig
* @private
*/
function _request(requestConfig) {
wx.request({
url: requestConfig['url'],
method: requestConfig['netMethod'],
data: requestConfig['params'],
header: requestConfig['header'],
success: (res) => {
_requestSuccess(res, requestConfig);
},
fail: (res) => {
_requestFailed(res, requestConfig)
},
complete: (res) => {
requestConfig.callback.doComplete();
}
})
}
/**
* 上传文件
* @param requestConfig 请求配置
* @param filePath 本地文件路径
* @private
*/
function _upload(requestConfig, filePath) {
wx.uploadFile({
header: headers,
method: requestConfig['netMethod'],
url: requestConfig['url'],
filePath: filePath,
name: 'file',
success: (res) => {
_requestSuccess(res, requestConfig);
},
fail: (res) => {
_requestFailed(res, requestConfig);
},
complete: () => {
requestConfig.callback.doComplete();
}
})
}

方法暴露并在需要时引用

1
2
3
4
module.exports = {
buildRequest:buildRequest
}
var netUtil=require("../../utils/netUtil.js");
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_requestLogin: function (code) {
let params = {
code: code,
userId: this.data.userId,
password: this.data.password
};
network_utils.buildRequest(url, params, {
doHttpSucess: (response) => {
if (constants.RetCodes.SUCCESS === response.retCode) {
console.log('[login] success.');
this._onLoginSuccess(response);
return true;
}
return false;
},
doComplete: () => {
this.setData({
loading: false
});
}
}, false).send();
},

flex布局

以下两篇阮一峰老师的文章:

Flex 布局教程:语法篇

Flex 布局教程:实例篇

登录态验证流程

微信官方给出了标准的登录流程,参照登录态维护

通过postman提升接口测试效率

发表于 2017-05-10

介绍

Postman是google开发的一款功能强大的网页调试与发送网页HTTP请求,并能运行测试用例的的Chrome插件。其主要功能包括:
模拟各种HTTP requests
从常用的 GET、POST 到 RESTful 的 PUT 、 DELETE …等等。 甚至还可以发送文件、送出额外的 header。

Collection 功能(测试集合)

Collection 是 requests的集合,在做完一個测试的時候, 你可以把這次的 request 存到特定的 Collection 里面,如此一來,下次要做同样的测试时,就不需要重新输入。而且一个collection可以包含多条request,如果我们把一个request当成一个test case,那collection就可以看成是一个test suite。通过collection的归类,我们可以良好的分类测试软件所提供的API.而且 Collection 还可以 Import 或是 Share 出來,让团队里面的所有人共享你建立起來的 Collection。

人性化的Response整理

一般在用其他工具來测试的時候,response的内容通常都是纯文字的 raw, 但如果是 JSON ,就是塞成一整行的 JSON。这会造成阅读的障碍 ,而 Postman 可以针对response内容的格式自动美化。 JSON、 XML 或是 HTML 都會整理成我们可以阅读的格式
内置测试脚本语言
Postman支持编写测试脚本,可以快速的检查request的结果,并返回测试结果

设定变量与环境

Postman 可以自由 设定变量与Environment,一般我们在编辑request,校验response的时候,总会需要重复输入某些字符,比如url,postman允许我们设定变量来保存这些值。并且把变量保存在不同的环境中。比如,我們可能会有多种环境, development 、 staging 或 local, 而这几种环境中的 request URL 也各不相同,但我们可以在不同的环境中设定同样的变量,只是变量的值不一样,这样我们就不用修改我们的测试脚本,而测试不同的环境。

安装

从chrome的扩展程序中安装

入门

打开postman之后如下图所示,左边是项目具体的接口,右上上面是请求链接,请求体,右边下面是具体的响应体

URL

要组装一条Request, URL永远是你首先要填的内容,在Postman里面你曾输入过的URL是可以通过下拉自动补全的哦。如果你点击Params按钮,Postman会弹出一个键值编辑器,你可以在哪里输入URL的Parameter,Postman会帮你自动加入到URL当中,反之,如果你的URL当中已经有了参数,那Postman会在你打开键值编辑器的时候把参数自动载入
Headers
点击’Headers’按钮,Postman同样会弹出一个键值编辑器。在这里,你可以随意添加你想要的Header attribute,同样Postman为我们通过了很贴心的auto-complete功能,敲入一个字母,你可以从下拉菜单里选择你想要的标准atrribute
Method
要选择Request的Method是很简单的,Postman支持所有的Method,而一旦你选择了Method,Postman的request body编辑器会根据的你选择,自动的发生改变
Request Body
如果我们要创建的request是类似于POST,那我们就需要编辑Request Body,Postman根据body type的不同,提供了4中编辑方式:

进阶

cookies
分开打包的应用程序运行在沙箱浏览器,它不能访问cookie设置浏览器内。这种限制也可以使用拦截器扩展。

身份验证Authentication
postman有一个helpers可以帮助我们简化一些重复和复杂的任务。当前的一套helpers可以帮助你解决一些authentication protocols的问题。

Basic Auth
填写用户名和密码,点击Refresh headers

Digest Auth
要比Basic Auth复杂的多。使用当前填写的值生成authorization header。所以在生成header之前要确保设置的正确性。如果当前的header已经存在,postman会移除之前的header。

OAuth 1.0a
postman的OAuth helper让你签署支持OAuth 1.0基于身份验证的请求。OAuth不用获取access token,你需要去API提供者获取的。OAuth 1.0可以在header或者查询参数中设置value。

OAuth 2.0
postman支持获得OAuth 2.0 token并添加到requests中。

Writting Test
Postman的Tests标签可以用来写测试:

本质上是javascript code,可以为tests object设置values。这里使用描述性文字作为key,检验body中的各种情况,当然你可以创建任意多的key,这取决于你需要测试多少点。 tests也会随着request保存到collection中。api测试保证前端后台都能正常的于api协作工作,而不用在出错时猜测是哪里的问题。 需要在request的test中创建了test后,再进行request,test的结果在body的test中查看。 注意: 1.这里的key描述必须是唯一的,否则相同描述只会执行第一个。 2.这里的key可以使用中文。 例子: tests[“Body contains user_id”] = responseBody.has(“user_id”) 这里描述性的key为:Body contains user_id。检测点为:responseBody.has(“user_id”),意思是检测返回的body中是否包含”user_id”这个字段。
查看responses中的Tests结果:记过显示每个key,也就是我们测试点的具体结果,是否通过。

Testing Sandbox
postman的测试是运行在沙箱环境,是与app独立的。查看什么在沙箱中是可用的,参见Sandbox documentation.

Snippets
用于快速添加常用的测试代码。可以自定义snippets。

Viewing results
postman每次执行request的时候,会执行tests。测试结果会在tests的tab上面显示一个通过的数量。

Testing Sandbox
Testing examples
测试代码会在发送request并且接收到responses后执行。

1.设置环境变量 postman.setEnvironmentVariable(“key”, “value”);
2.设置全局变量 postman.setGlobalVariable(“key”, “value”);
3.检查response body中是否包含某个string tests[“Body matches string”] = responseBody.has(“string_you_want_to_search”);
4.检测JSON中的某个值是否等于预期的值
var data = JSON.parse(responseBody);
tests[“Your test name”] = data.value === 100;
JSON.parse()方法,把json字符串转化为对象。parse()会进行json格式的检查是一个安全的函数。 如:检查json中某个数组元素的个数(这里检测programs的长度)
var data = JSON.parse(responseBody);
tests[“program’s lenght”] = data.programs.length === 5;
5.转换XML body为JSON对象 var jsonObject = xml2Json(responseBody);
6.检查response body是否与某个string相等 tests[“Body is correct”] = responseBody === “response_body_string”;
7.测试response Headers中的某个元素是否存在(如:Content-Type)
tests[“Content-Type is present”] = postman.getResponseHeader(“Content-Type”);
//getResponseHeader()方法会返回header的值,如果该值存在
或者:
tests[“Content-Type is present”] = responseHeaders.hasOwnProperty(“Content-Type”);
上面的方法,不区分大小写。下面的方法,要区分大小写。

8.验证Status code的值 tests[“Status code is 200”] = responseCode.code === 200;

9.验证Response time是否小于某个值 tests[“Response time is less than 200ms”] = responseTime < 200;
10.name是否包含某个值 tests[“Status code name has string”] = responseCode.name.has(“Created”);
11.POST 请求的状态响应码是否是某个值 tests[“Successful POST request”] = responseCode.code === 201 || responseCode.code === 202;
12.很小的JSON数据验证器
var schema = {
“items”: {
“type”: “boolean”
}
};
var data1 = [true, false];
var data2 = [true, 123];
console.log(tv4.error);
tests[“Valid Data1”] = tv4.validate(data1, schema);
tests[“Valid Data2”] = tv4.validate(data2, schema);
结果:
运行Collections
postman允许你运行collection,你可以运行任意的次数。 最后会给出一个整体运行的结果。会保存每一次运行的结果,提供给你比较每一次运行解雇的不同。
选择collection,选择环境。点击运行按钮。

在需要csv和json文件的地方记得添加。
运行collection测试会在另一个窗口运行。如果需要在main窗口修改东西,在新窗口能正常读取。

参考文档:

https://juejin.im/entry/57597a62a341310061337885

http://www.jianshu.com/p/9d36e77fab9e

12

PennHan

在Android的道路上摸爬滚打,望有光明向我招手。

12 日志
4 标签
RSS
© 2019 PennHan
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4