IDE采用Android Studio,Demo实现的功能为用户注册、登录和展示一个券列表,数据采用我们现有项目的测试数据,接口也是我们项目中的测试接口。
项目搭建
根据架构篇所讲的,将项目分为了四个层级:模型层、接口层、核心层、界面层。四个层级之间的关系如下图所示:
实现上,在Android Studio分为了相应的四个模块(Module):model、api、core、app。
model为模型层,api为接口层,core为核心层,app为界面层。
model、api、core这三个模块的类型为library,app模块的类型为application。
四个模块之间的依赖设置为:model没有任何依赖,接口层依赖了模型层,核心层依赖了模型层和接口层,界面层依赖了核心层和模型层。
项目搭建的步骤如下:
- 创建新项目,项目名称为KAndroid,包名为com.keegan.kandroid。默认已创建了app模块,查看下app模块下的build.gradle,会看到第一行为:
1apply plugin : 'com.android.application'
这行表明了app模块是application类型的。 - 分别新建模块model、api、core,Module Type都选为Android Library,在Add an activity to module页面选择Add No Activity,这三个模块做为库使用,并不需要界面。创建完之后,查看相应模块的build.gradle,会看到第一行为:
1apply plugin : 'com.android.library' - 建立模块之间的依赖关系。有两种方法可以设置:
第一种:通过右键模块,然后Open Module Settings,选择模块的Dependencies,点击左下方的加号,选择Module dependency,最后选择要依赖的模块,下图为api模块添加了model依赖;第二种:直接在模块的build.gradle设置。打开build.gradle,在最后的dependencies一项里面添加新的一行:compile project(‘:ModuleName’),比如app模块添加对model模块和core模块依赖之后的dependencies如下:
123456dependencies {compile fileTree ( dir : 'libs' , include : [ '*.jar' ] )compile 'com.android.support:appcompat-v7:22.0.0'compile project ( ':model' )compile project ( ':core' )}通过上面两种方式的任意一种,创建了模块之间的依赖关系之后,每个模块的build.gradle的dependencies项的结果将会如下:
model:1234dependencies {compile fileTree ( dir : 'libs' , include : [ '*.jar' ] )compile 'com.android.support:appcompat-v7:22.0.0'}api:
12345dependencies {compile fileTree ( dir : 'libs' , include : [ '*.jar' ] )compile 'com.android.support:appcompat-v7:22.0.0'compile project ( ':model' )}core:
123456dependencies {compile fileTree ( dir : 'libs' , include : [ '*.jar' ] )compile 'com.android.support:appcompat-v7:22.0.0'compile project ( ':model' )compile project ( ':api' )}app:
123456dependencies {compile fileTree ( dir : 'libs' , include : [ '*.jar' ] )compile 'com.android.support:appcompat-v7:22.0.0'compile project ( ':model' )compile project ( ':core' )}
创建业务对象模型
业务对象模型统一存放于model模块,是对业务数据的封装,大部分都是从接口传过来的对象,因此,其属性也与接口传回的对象属性相一致。在这个Demo里,只有一个业务对象模型,封装了券的基本信息,以下是该实体类的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/**
* 券的业务模型类,封装了券的基本信息。
* 券分为了三种类型:现金券、抵扣券、折扣券。
* 现金券是拥有固定面值的券,有固定的售价;
* 抵扣券是满足一定金额后可以抵扣的券,比如满100减10元;
* 折扣券是可以打折的券。
*
* @version 1.0 创建时间:15/6/21
*/
public
class
CouponBO
implements
Serializable
{
private
static
final
long
serialVersionUID
=
-
8022957276104379230L
;
private
int
id
;
// 券id
private
String
name
;
// 券名称
private
String
introduce
;
// 券简介
private
int
modelType
;
// 券类型,1为现金券,2为抵扣券,3为折扣券
private
double
faceValue
;
// 现金券的面值
private
double
estimateAmount
;
// 现金券的售价
private
double
debitAmount
;
// 抵扣券的抵扣金额
private
double
discount
;
// 折扣券的折扣率(0-100)
private
double
miniAmount
;
// 抵扣券和折扣券的最小使用金额
// TODO 所有属性的getter和setter
}
|
接口层的封装
在这个Demo里,提供了4个接口:一个发送验证码的接口、一个注册接口、一个登录接口、一个获取券列表的接口。这4个接口具体如下:
- 发送验证码接口
URL:http://uat.b.quancome.com/platform/api
参数:参数名 描述 类型 appKey ANDROID_KCOUPON String method service.sendSmsCode4Register String phoneNum 手机号码 String 输出样例:
1{ "event" : "0" , "msg" : "success" } - 注册接口
URL:http://uat.b.quancome.com/platform/api
参数:参数名 描述 类型 appKey ANDROID_KCOUPON String method customer.registerByPhone String phoneNum 手机号码 String code 验证码 String password MD5加密密码 String 输出样例:
1{ "event" : "0" , "msg" : "success" } - 登录接口
URL:http://uat.b.quancome.com/platform/api
其他参数:参数名 描述 类型 appKey ANDROID_KCOUPON String method customer.loginByApp String loginName 登录名(手机号) String password MD5加密密码 String imei 手机imei串号 String loginOS 系统,android为1 int 输出样例:
1{ "event" : "0" , "msg" : "success" } - 券列表
URL:http://uat.b.quancome.com/platform/api
其他参数:参数名 描述 类型 appKey ANDROID_KCOUPON String method issue.listNewCoupon String currentPage 当前页数 int pageSize 每页显示数量 int 输出样例:
12345{ "event" : "0" , "msg" : "success" , "maxCount" : 125 , "maxPage" : 7 , "currentPage" : 1 , "pageSize" : 20 , "objList" : [{ "id" : 1 , "name" : "测试现金券" , "modelType" : 1 , . . . } ,{ . . . } ,. . .] }
在架构篇已经讲过,接口返回的json数据有三种固定结构:
1
2
3
|
{
"event"
:
"0"
,
"msg"
:
"success"
}
{
"event"
:
"0"
,
"msg"
:
"success"
,
"obj"
:
{
.
.
.
}
}
{
"event"
:
"0"
,
"msg"
:
"success"
,
"objList"
:
[
{
.
.
.
}
,
{
.
.
.
}
]
,
"currentPage"
:
1
,
"pageSize"
:
20
,
"maxCount"
:
2
,
"maxPage"
:
1
}
|
因此可以封装成实体类,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
ApiResponseT
>
{
private
String
event
;
// 返回码,0为成功
private
String
msg
;
// 返回信息
private
T
obj
;
// 单个对象
private
T
objList
;
// 数组对象
private
int
currentPage
;
// 当前页数
private
int
pageSize
;
// 每页显示数量
private
int
maxCount
;
// 总条数
private
int
maxPage
;
// 总页数
// 构造函数,初始化code和msg
public
ApiResponse
(
String
event
,
String
msg
)
{
this
.
event
=
event
;
this
.
msg
=
msg
;
}
// 判断结果是否成功
public
boolean
isSuccess
(
)
{
return
event
.
equals
(
"0"
)
;
}
// TODO 所有属性的getter和setter
}
|
上面4个接口,URL和appKey都是一样的,用来区别不同接口的则是method字段,因此,URL和appKey可以统一定义,method则根据不同接口定义不同常量。而除去appKey和method,剩下的参数才是每个接口需要定义的参数。因此,对上面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
|
public
interface
Api
{
// 发送验证码
public
final
static
String
SEND_SMS_CODE
=
"service.sendSmsCode4Register"
;
// 注册
public
final
static
String
REGISTER
=
"customer.registerByPhone"
;
// 登录
public
final
static
String
LOGIN
=
"customer.loginByApp"
;
// 券列表
public
final
static
String
LIST_COUPON
=
"issue.listNewCoupon"
;
/**
* 发送验证码
*
* @param phoneNum 手机号码
* @return 成功时返回:{ "event": "0", "msg":"success" }
*/
public
ApiResponseVoid
>
sendSmsCode4Register
(
String
phoneNum
)
;
/**
* 注册
*
* @param phoneNum 手机号码
* @param code 验证码
* @param password MD5加密的密码
* @return 成功时返回:{ "event": "0", "msg":"success" }
*/
public
ApiResponseVoid
>
registerByPhone
(
String
phoneNum
,
String
code
,
String
password
)
;
/**
* 登录
*
* @param loginName 登录名(手机号)
* @param password MD5加密的密码
* @param imei 手机IMEI串号
* @param loginOS Android为1
* @return 成功时返回:{ "event": "0", "msg":"success" }
*/
public
ApiResponseVoid
>
loginByApp
(
String
loginName
,
String
password
,
String
imei
,
int
loginOS
)
;
/**
* 券列表
*
* @param currentPage 当前页数
* @param pageSize 每页显示数量
* @return 成功时返回:{ "event": "0", "msg":"success", "objList":[...] }
*/
public
ApiResponseListCouponBO
>>
listNewCoupon
(
int
currentPage
,
int
pageSize
)
;
}
|
Api的实现类则是ApiImpl了,实现类需要封装好请求数据并向服务器发起请求,并将响应结果的数据转为ApiResonse返回。而向服务器发送请求并将响应结果返回的处理则封装到http引擎类去处理。另外,这里引用了gson将json转为对象。ApiImpl的实现代码如下:
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
67
68
69
70
71
72
73
74
75
76
77
78
|
public
class
ApiImpl
implements
Api
{
private
final
static
String
APP_KEY
=
"ANDROID_KCOUPON"
;
private
final
static
String
TIME_OUT_EVENT
=
"CONNECT_TIME_OUT"
;
private
final
static
String
TIME_OUT_EVENT_MSG
=
"连接服务器失败"
;
// http引擎
private
HttpEngine
httpEngine
;
public
ApiImpl
(
)
{
httpEngine
=
HttpEngine
.
getInstance
(
)
;
}
@
Override
public
ApiResponseVoid
>
sendSmsCode4Register
(
String
phoneNum
)
{
MapString
,
String
>
paramMap
=
new
HashMapString
,
String
>
(
)
;
paramMap
.
put
(
"appKey"
,
APP_KEY
)
;
paramMap
.
put
(
"method"
,
SEND_SMS_CODE
)
;
paramMap
.
put
(
"phoneNum"
,
phoneNum
)
;
Type
type
=
new
TypeTokenApiResponseVoid
>>
(
)
{
}
.
getType
(
)
;
try
{
return
httpEngine
.
postHandle
(
paramMap
,
type
)
;
}
catch
(
IOException
e
)
{
return
new
ApiResponse
(
TIME_OUT_EVENT
,
TIME_OUT_EVENT_MSG
)
;
}
}
@
Override
public
ApiResponseVoid
>
registerByPhone
(
String
phoneNum
,
String
code
,
String
password
)
{
MapString
,
String
>
paramMap
=
new
HashMapString
,
String
>
(
)
;
paramMap
.
put
(
"appKey"
,
APP_KEY
)
;
paramMap
.
put
(
"method"
,
REGISTER
)
;
paramMap
.
put
(
"phoneNum"
,
phoneNum
)
;
paramMap
.
put
(
"code"
,
code
)
;
paramMap
.
put
(
"password"
,
EncryptUtil
.
makeMD5
(
password
)
)
;
Type
type
=
new
TypeTokenApiResponseListCouponBO
>>>
(
)
{
}
.
getType
(
)
;
try
{
return
httpEngine
.
postHandle
(
paramMap
,
type
)
;
}
catch
(
IOException
e
)
{
return
new
ApiResponse
(
TIME_OUT_EVENT
,
TIME_OUT_EVENT_MSG
)
;
}
}
@
Override
public
ApiResponseVoid
>
loginByApp
(
String
loginName
,
String
password
,
String
imei
,
int
loginOS
)
{
MapString
,
String
>
paramMap
=
new
HashMapString
,
String
>
(
)
;
paramMap
.
put
(
"appKey"
,
APP_KEY
)
;
paramMap
.
put
(
"method"
,
LOGIN
)
;
paramMap
.
put
(
"loginName"
,
loginName
)
;
paramMap
.
put
(
"password"
,
EncryptUtil
.
makeMD5
(
password
)
)
;
paramMap
.
put
(
"imei"
,
imei
)
;
paramMap
.
put
(
"loginOS"
,
String
.
valueOf
(
loginOS
)
)
;
Type
type
=
new
TypeTokenApiResponseListCouponBO
>>>
(
)
{
}
.
getType
(
)
;
try
{
return
httpEngine
.
postHandle
(
paramMap
,
type
)
;
}
catch
(
IOException
e
)
{
return
new
ApiResponse
(
TIME_OUT_EVENT
,
TIME_OUT_EVENT_MSG
)
;
}
}
@
Override
public
ApiResponseListCouponBO
>>
listNewCoupon
(
int
currentPage
,
int
pageSize
)
{
MapString
,
String
>
paramMap
=
new
HashMapString
,
String
>
(
)
;
paramMap
.
put
(
"appKey"
,
APP_KEY
)
;
paramMap
.
put
(
"method"
,
LIST_COUPON
)
;
paramMap
.
put
(
"currentPage"
,
String
.
valueOf
(
currentPage
)
)
;
paramMap
.
put
(
"pageSize"
,
String
.
valueOf
(
pageSize
)
)
;
Type
type
=
new
TypeTokenApiResponseListCouponBO
>>>
(
)
{
}
.
getType
(
)
;
try
{
return
httpEngine
.
postHandle
(
paramMap
,
type
)
;
}
catch
(
IOException
e
)
{
return
new
ApiResponse
(
TIME_OUT_EVENT
,
TIME_OUT_EVENT_MSG
)
;
}
}
}
|
而http引擎类的实现如下:
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
public
class
HttpEngine
{
private
final
static
String
SERVER_URL
=
"http://uat.b.quancome.com/platform/api"
;
private
final
static
String
REQUEST_MOTHOD
=
"POST"
;
private
final
static
String
ENCODE_TYPE
=
"UTF-8"
;
private
final
static
int
TIME_OUT
=
15000
;
private
static
HttpEngine
instance
=
null
;
private
HttpEngine
(
)
{
}
public
static
HttpEngine
getInstance
(
)
{
if
(
instance
==
null
)
{
instance
=
new
HttpEngine
(
)
;
}
return
instance
;
}
public
T
>
T
postHandle
(
MapString
,
String
>
paramsMap
,
Type
typeOfT
)
throws
IOException
{
String
data
=
joinParams
(
paramsMap
)
;
HttpUrlConnection
connection
=
getConnection
(
)
;
connection
.
setRequestProperty
(
"Content-Length"
,
String
.
valueOf
(
data
.
getBytes
(
)
.
length
)
)
;
connection
.
connect
(
)
;
OutputStream
os
=
connection
.
getOutputStream
(
)
;
os
.
write
(
data
.
getBytes
(
)
)
;
os
.
flush
(
)
;
if
(
connection
.
getResponseCode
(
)
==
200
)
{
// 获取响应的输入流对象
InputStream
is
=
connection
.
getInputStream
(
)
;
// 创建字节输出流对象
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
(
)
;
// 定义读取的长度
int
len
=
0
;
// 定义缓冲区
byte
buffer
[
]
=
new
byte
[
1024
]
;
// 按照缓冲区的大小,循环读取
while
(
(
len
=
is
.
read
(
buffer
)
)
!=
-
1
)
{
// 根据读取的长度写入到os对象中
baos
.
write
(
buffer
,
0
,
len
)
;
}
// 释放资源
is
.
close
(
)
;
baos
.
close
(
)
;
connection
.
disconnect
(
)
;
// 返回字符串
final
String
result
=
new
String
(
baos
.
toByteArray
(
)
)
;
Gson
gson
=
new
Gson
(
)
;
return
gson
.
fromJson
(
result
,
typeOfT
)
;
}
else
{
connection
.
disconnect
(
)
;
return
null
;
}
}
private
HttpURLConnection
getConnection
(
)
{
HttpURLConnection
connection
=
null
;
// 初始化connection
try
{
// 根据地址创建URL对象
URL
url
=
new
URL
(
SERVER_URL
)
;
// 根据URL对象打开链接
connection
=
(
HttpURLConnection
)
url
.
openConnection
(
)
;
// 设置请求的方式
connection
.
setRequestMethod
(
REQUEST_MOTHOD
)
;
// 发送POST请求必须设置允许输入,默认为true
connection
.
setDoInput
(
true
)
;
// 发送POST请求必须设置允许输出
connection
.
setDoOutput
(
true
)
;
// 设置不使用缓存
connection
.
setUseCaches
(
false
)
;
// 设置请求的超时时间
connection
.
setReadTimeout
(
TIME_OUT
)
;
connection
.
setConnectTimeout
(
TIME_OUT
)
;
connection
.
setRequestProperty
(
"Content-Type"
,
"application/x-www-form-urlencoded"
)
;
connection
.
setRequestProperty
(
"Connection"
,
"keep-alive"
)
;
connection
.
setRequestProperty
(
"Response-Type"
,
"json"
)
;
connection
.
setChunkedStreamingMode
(
0
)
;
}
catch
(
IOException
e
)
{
e
.
printStackTrace
(
)
;
}
return
connection
;
}
private
String
joinParams
(
MapString
,
String
>
paramsMap
)
{
StringBuilder
stringBuilder
=
new
StringBuilder
(
)
;
for
(
String
key
:
paramsMap
.
keySet
(
)
)
{
stringBuilder
.
append
(
key
)
;
stringBuilder
.
append
(
"="
)
;
try
{
stringBuilder
.
append
(
URLEncoder
.
encode
(
paramsMap
.
get
(
key
)
,
ENCODE_TYPE
)
)
;
}
catch
(
UnsupportedEncodingException
e
)
{
e
.
printStackTrace
(
)
;
}
stringBuilder
.
append
(
"&"
)
;
}
return
stringBuilder
.
substring
(
0
,
stringBuilder
.
length
(
)
-
1
)
;
}
}
|
至此,接口层的封装就完成了。接下来再往上看看核心层吧。
核心层的逻辑
核心层处于接口层和界面层之间,向下调用Api,向上提供Action,它的核心任务就是处理复杂的业务逻辑。先看看我对Action的定义:
1
2
3
4
5
6
7
8
9
10
|
public
interface
AppAction
{
// 发送手机验证码
public
void
sendSmsCode
(
String
phoneNum
,
ActionCallbackListenerVoid
>
listener
)
;
// 注册
public
void
register
(
String
phoneNum
,
String
code
,
String
password
,
ActionCallbackListenerVoid
>
listener
)
;
// 登录
public
void
login
(
String
loginName
,
String
password
,
ActionCallbackListenerVoid
>
listener
)
;
// 按分页获取券列表
public
void
listCoupon
(
int
currentPage
,
ActionCallbackListenerListCouponBO
>>
listener
)
;
}
|
首先,和Api接口对比就会发现,参数并不一致。登录并没有iemi和loginOS的参数,获取券列表的参数里也少了pageSize。这是因为,这几个参数,跟界面其实并没有直接关系。Action只要定义好跟界面相关的就可以了,其他需要的参数,在具体实现时再去获取。
另外,大部分action的处理都是异步的,因此,添加了回调监听器ActionCallbackListener,回调监听器的泛型则是返回的对象数据类型,例如获取券列表,返回的数据类型就是List,没有对象数据时则为Void。回调监听器只定义了成功和失败的方法,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
interface
ActionCallbackListenerT
>
{
/**
* 成功时调用
*
* @param data 返回的数据
*/
public
void
onSuccess
(
T
data
)
;
/**
* 失败时调用
*
* @param errorEvemt 错误码
* @param message 错误信息
*/
public
void
onFailure
(
String
errorEvent
,
String
message
)
;
}
|
接下来再看看Action的实现。首先,要获取imei,那就需要传入一个Context;另外,还需要loginOS和pageSize,这定义为常量就可以了;还有,要调用接口层,所以还需要Api实例。而接口的实现分为两步,第一步做参数检查,第二步用异步任务调用Api。具体实现如下:
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
public
class
AppActionImpl
implements
AppAction
{
private
final
static
int
LOGIN_OS
=
1
;
// 表示Android
private
final
static
int
PAGE_SIZE
=
20
;
// 默认每页20条
private
Context
context
;
private
Api
api
;
public
AppActionImpl
(
Context
context
)
{
this
.
context
=
context
;
this
.
api
=
new
ApiImpl
(
)
;
}
@
Override
public
void
sendSmsCode
(
final
String
phoneNum
,
final
ActionCallbackListenerVoid
>
listener
)
{
// 参数为空检查
if
(
TextUtils
.
isEmpty
(
phoneNum
)
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_NULL
,
"手机号为空"
)
;
}
return
;
}
// 参数合法性检查
Pattern
pattern
=
Pattern
.
compile
(
"1\d{10}"
)
;
Matcher
matcher
=
pattern
.
matcher
(
phoneNum
)
;
if
(
!
matcher
.
matches
(
)
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_ILLEGAL
,
"手机号不正确"
)
;
}
return
;
}
// 请求Api
new
AsyncTaskVoid
,
Void
,
ApiResponseVoid
>>
(
)
{
@
Override
protected
ApiResponseVoid
>
doInBackground
(
Void
.
.
.
voids
)
{
return
api
.
sendSmsCode4Register
(
phoneNum
)
;
}
@
Override
protected
void
onPostExecute
(
ApiResponseVoid
>
response
)
{
if
(
listener
!=
null
&
response
!=
null
)
{
if
(
response
.
isSuccess
(
)
)
{
listener
.
onSuccess
(
null
)
;
}
else
{
listener
.
onFailure
(
response
.
getEvent
(
)
,
response
.
getMsg
(
)
)
;
}
}
}
}
.
execute
(
)
;
}
@
Override
public
void
register
(
final
String
phoneNum
,
final
String
code
,
final
String
password
,
final
ActionCallbackListenerVoid
>
listener
)
{
// 参数为空检查
if
(
TextUtils
.
isEmpty
(
phoneNum
)
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_NULL
,
"手机号为空"
)
;
}
return
;
}
if
(
TextUtils
.
isEmpty
(
code
)
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_NULL
,
"验证码为空"
)
;
}
return
;
}
if
(
TextUtils
.
isEmpty
(
password
)
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_NULL
,
"密码为空"
)
;
}
return
;
}
// 参数合法性检查
Pattern
pattern
=
Pattern
.
compile
(
"1\d{10}"
)
;
Matcher
matcher
=
pattern
.
matcher
(
phoneNum
)
;
if
(
!
matcher
.
matches
(
)
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_ILLEGAL
,
"手机号不正确"
)
;
}
return
;
}
// TODO 长度检查,密码有效性检查等
// 请求Api
new
AsyncTaskVoid
,
Void
,
ApiResponseVoid
>>
(
)
{
@
Override
protected
ApiResponseVoid
>
doInBackground
(
Void
.
.
.
voids
)
{
return
api
.
registerByPhone
(
phoneNum
,
code
,
password
)
;
}
@
Override
protected
void
onPostExecute
(
ApiResponseVoid
>
response
)
{
if
(
listener
!=
null
&
response
!=
null
)
{
if
(
response
.
isSuccess
(
)
)
{
listener
.
onSuccess
(
null
)
;
}
else
{
listener
.
onFailure
(
response
.
getEvent
(
)
,
response
.
getMsg
(
)
)
;
}
}
}
}
.
execute
(
)
;
}
@
Override
public
void
login
(
final
String
loginName
,
final
String
password
,
final
ActionCallbackListenerVoid
>
listener
)
{
// 参数为空检查
if
(
TextUtils
.
isEmpty
(
loginName
)
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_NULL
,
"登录名为空"
)
;
}
return
;
}
if
(
TextUtils
.
isEmpty
(
password
)
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_NULL
,
"密码为空"
)
;
}
return
;
}
// TODO 长度检查,密码有效性检查等
// 请求Api
new
AsyncTaskVoid
,
Void
,
ApiResponseVoid
>>
(
)
{
@
Override
protected
ApiResponseVoid
>
doInBackground
(
Void
.
.
.
voids
)
{
TelephonyManager
telephonyManager
=
(
TelephonyManager
)
context
.
getSystemService
(
Context
.
TELEPHONY_SERVICE
)
;
String
imei
=
telephonyManager
.
getDeviceId
(
)
;
return
api
.
loginByApp
(
loginName
,
password
,
imei
,
LOGIN_OS
)
;
}
@
Override
protected
void
onPostExecute
(
ApiResponseVoid
>
response
)
{
if
(
listener
!=
null
&
response
!=
null
)
{
if
(
response
.
isSuccess
(
)
)
{
listener
.
onSuccess
(
null
)
;
}
else
{
listener
.
onFailure
(
response
.
getEvent
(
)
,
response
.
getMsg
(
)
)
;
}
}
}
}
.
execute
(
)
;
}
@
Override
public
void
listCoupon
(
final
int
currentPage
,
final
ActionCallbackListenerListCouponBO
>>
listener
)
{
// 参数检查
if
(
currentPage
0
)
{
if
(
listener
!=
null
)
{
listener
.
onFailure
(
ErrorEvent
.
PARAM_ILLEGAL
,
"当前页数小于零"
)
;
}
}
// TODO 添加缓存
// 请求Api
new
AsyncTaskVoid
,
Void
,
ApiResponseListCouponBO
>>>
(
)
{
@
Override
protected
ApiResponseListCouponBO
>>
doInBackground
(
Void
.
.
.
voids
)
{
return
api
.
listNewCoupon
(
currentPage
,
PAGE_SIZE
)
;
}
@
Override
protected
void
onPostExecute
(
ApiResponseListCouponBO
>>
response
)
{
if
(
listener
!=
null
&
response
!=
null
)
{
if
(
response
.
isSuccess
(
)
)
{
listener
.
onSuccess
(
response
.
getObjList
(
)
)
;
}
else
{
listener
.
onFailure
(
response
.
getEvent
(
)
,
response
.
getMsg
(
)
)
;
}
}
}
}
.
execute
(
)
;
}
}
|
简单的实现代码就是这样,其实,这还有很多地方可以优化,比如,将参数为空的检查、手机号有效性的检查、数字型范围的检查等等,都可以抽成独立的方法,从而减少重复代码的编写。异步任务里的代码也一样,都是可以通过重构优化的。另外,需要扩展时,比如添加缓存,那就在调用Api之前处理。
核心层的逻辑就是这样了。最后就到界面层了。
界面层
在这个Demo里,只有三个页面:登录页、注册页、券列表页。在这里,也会遵循界面篇提到的三个基本原则:规范性、单一性、简洁性。
首先,界面层需要调用核心层的Action,而这会在整个应用级别都用到,因此,Action的实例最好放在Application里。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
KApplication
extends
Application
{
private
AppAction
appAction
;
@
Override
public
void
onCreate
(
)
{
super
.
onCreate
(
)
;
appAction
=
new
AppActionImpl
(
this
)
;
}
public
AppAction
getAppAction
(
)
{
return
appAction
;
}
}
|
另外,一个Activity的基类也是很有必要的,可以减少很多重复的工作。基类的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
abstract
class
KBaseActivity
extends
FragmentActivity
{
// 上下文实例
public
Context
context
;
// 应用全局的实例
public
KApplication
application
;
// 核心层的Action实例
public
AppAction
appAction
;
@
Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
)
;
context
=
getApplicationContext
(
)
;
application
=
(
KApplication
)
this
.
getApplication
(
)
;
appAction
=
application
.
getAppAction
(
)
;
}
}
|
再看看登录的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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
public
class
LoginActivity
extends
KBaseActivity
{
private
EditText
phoneEdit
;
private
EditText
passwordEdit
;
private
Button
loginBtn
;
@
Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
)
;
setContentView
(
R
.
layout
.
activity_login
)
;
// 初始化View
initViews
(
)
;
}
@
Override
public
boolean
onCreateOptionsMenu
(
Menu
menu
)
{
getMenuInflater
(
)
.
inflate
(
R
.
menu
.
menu_login
,
menu
)
;
return
true
;
}
@
Override
public
boolean
onOptionsItemSelected
(
MenuItem
item
)
{
int
id
=
item
.
getItemId
(
)
;
// 如果是注册按钮
if
(
id
==
R
.
id
.
action_register
)
{
Intent
intent
=
new
Intent
(
this
,
RegisterActivity
.
class
)
;
startActivity
(
intent
)
;
return
true
;
}
return
super
.
onOptionsItemSelected
(
item
)
;
}
// 初始化View
private
void
initViews
(
)
{
phoneEdit
=
(
EditText
)
findViewById
(
R
.
id
.
edit_phone
)
;
passwordEdit
=
(
EditText
)
findViewById
(
R
.
id
.
edit_password
)
;
loginBtn
=
(
Button
)
findViewById
(
R
.
id
.
btn_login
)
;
}
// 准备登录
public
void
toLogin
(
View
view
)
{
String
loginName
=
phoneEdit
.
getText
(
)
.
toString
(
)
;
String
password
=
passwordEdit
.
getText
(
)
.
toString
(
)
;
loginBtn
.
setEnabled
(
false
)
;
this
.
appAction
.
login
(
loginName
,
password
,
new
ActionCallbackListenerVoid
>
(
)
{
@
Override
public
void
onSuccess
(
Void
data
)
{
Toast
.
makeText
(
context
,
R
.
string
.
toast_login_success
,
Toast
.
LENGTH_SHORT
)
.
show
(
)
;
Intent
intent
=
new
Intent
(
context
,
CouponListActivity
.
class
)
;
startActivity
(
intent
)
;
finish
(
)
;
}
@
Override
public
void
onFailure
(
String
errorEvent
,
String
message
)
{
Toast
.
makeText
(
context
,
message
,
Toast
.
LENGTH_SHORT
)
.
show
(
)
;
loginBtn
.
setEnabled
(
true
)
;
}
}
)
;
}
}
|
登录页的布局文件则如下:
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
|
LinearLayout
xmlns
:
android
=
"http://schemas.android.com/apk/res/android"
xmlns
:
tools
=
"http://schemas.android.com/tools"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"match_parent"
android
:
orientation
=
"vertical"
android
:
paddingBottom
=
"@dimen/activity_vertical_margin"
android
:
paddingLeft
=
"@dimen/activity_horizontal_margin"
android
:
paddingRight
=
"@dimen/activity_horizontal_margin"
android
:
paddingTop
=
"@dimen/activity_vertical_margin"
tools
:
context
=
"com.keegan.kandroid.activity.LoginActivity"
>
EditText
android
:
id
=
"@+id/edit_phone"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"wrap_content"
android
:
layout_marginTop
=
"@dimen/edit_vertical_margin"
android
:
layout_marginBottom
=
"@dimen/edit_vertical_margin"
android
:
hint
=
"@string/hint_phone"
android
:
inputType
=
"phone"
android
:
singleLine
=
"true"
/
>
EditText
android
:
id
=
"@+id/edit_password"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"wrap_content"
android
:
layout_marginTop
=
"@dimen/edit_vertical_margin"
android
:
layout_marginBottom
=
"@dimen/edit_vertical_margin"
android
:
hint
=
"@string/hint_password"
android
:
inputType
=
"textPassword"
android
:
singleLine
=
"true"
/
>
Button
android
:
id
=
"@+id/btn_login"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"wrap_content"
android
:
layout_marginTop
=
"@dimen/btn_vertical_margin"
android
:
layout_marginBottom
=
"@dimen/btn_vertical_margin"
android
:
onClick
=
"toLogin"
android
:
text
=
"@string/btn_login"
/
>
LinearLayout
>
|
可以看到,EditText的id命名统一以edit开头,而在Activity里的控件变量名则以Edit结尾。按钮的onClick也统一用toXXX的方式命名,明确表明这是一个将要做的动作。还有,string,dimen也都统一在相应的资源文件里按照相应的规范去定义。
注册页和登陆页差不多,这里就不展示代码了。主要再看看券列表页,因为用到了ListView,ListView需要添加适配器。实际上,适配器很多代码都是可以复用的,因此,我抽象了一个适配器的基类,代码如下:
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
67
|
public
abstract
class
KBaseAdapterT
>
extends
BaseAdapter
{
protected
Context
context
;
protected
LayoutInflater
inflater
;
protected
ListT
>
itemList
=
new
ArrayListT
>
(
)
;
public
KBaseAdapter
(
Context
context
)
{
this
.
context
=
context
;
inflater
=
LayoutInflater
.
from
(
context
)
;
}
/**
* 判断数据是否为空
*
* @return 为空返回true,不为空返回false
*/
public
boolean
isEmpty
(
)
{
return
itemList
.
isEmpty
(
)
;
}
/**
* 在原有的数据上添加新数据
*
* @param itemList
*/
public
void
addItems
(
ListT
>
itemList
)
{
this
.
itemList
.
addAll
(
itemList
)
;
notifyDataSetChanged
(
)
;
}
/**
* 设置为新的数据,旧数据会被清空
*
* @param itemList
*/
public
void
setItems
(
ListT
>
itemList
)
{
this
.
itemList
.
clear
(
)
;
this
.
itemList
=
itemList
;
notifyDataSetChanged
(
)
;
}
/**
* 清空数据
*/
public
void
clearItems
(
)
{
itemList
.
clear
(
)
;
notifyDataSetChanged
(
)
;
}
@
Override
public
int
getCount
(
)
{
return
itemList
.
size
(
)
;
}
@
Override
public
Object
getItem
(
int
i
)
{
return
itemList
.
get
(
i
)
;
}
@
Override
public
long
getItemId
(
int
i
)
{
return
i
;
}
@
Override
abstract
public
View
getView
(
int
i
,
View
view
,
ViewGroup
viewGroup
)
;
}
|
这个抽象基类集成了设置数据的方法,每个具体的适配器类只要再实现各自的getView方法就可以了。本Demo的券列表的适配器如下:
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
|
public
class
CouponListAdapter
extends
KBaseAdapterCouponBO
>
{
public
CouponListAdapter
(
Context
context
)
{
super
(
context
)
;
}
@
Override
public
View
getView
(
int
i
,
View
view
,
ViewGroup
viewGroup
)
{
ViewHolder
holder
;
if
(
view
==
null
)
{
view
=
inflater
.
inflate
(
R
.
layout
.
item_list_coupon
,
viewGroup
,
false
)
;
holder
=
new
ViewHolder
(
)
;
holder
.
titleText
=
(
TextView
)
view
.
findViewById
(
R
.
id
.
text_item_title
)
;
holder
.
infoText
=
(
TextView
)
view
.
findViewById
(
R
.
id
.
text_item_info
)
;
holder
.
priceText
=
(
TextView
)
view
.
findViewById
(
R
.
id
.
text_item_price
)
;
view
.
setTag
(
holder
)
;
}
else
{
holder
=
(
ViewHolder
)
view
.
getTag
(
)
;
}
CouponBO
coupon
=
itemList
.
get
(
i
)
;
holder
.
titleText
.
setText
(
coupon
.
getName
(
)
)
;
holder
.
infoText
.
setText
(
coupon
.
getIntroduce
(
)
)
;
SpannableString
priceString
;
// 根据不同的券类型展示不同的价格显示方式
switch
(
coupon
.
getModelType
(
)
)
{
default
:
case
CouponBO
.
TYPE_CASH
:
priceString
=
CouponPriceUtil
.
getCashPrice
(
context
,
coupon
.
getFaceValue
(
)
,
coupon
.
getEstimateAmount
(
)
)
;
break
;
case
CouponBO
.
TYPE_DEBIT
:
priceString
=
CouponPriceUtil
.
getVoucherPrice
(
context
,
coupon
.
getDebitAmount
(
)
,
coupon
.
getMiniAmount
(
)
)
;
break
;
case
CouponBO
.
TYPE_DISCOUNT
:
priceString
=
CouponPriceUtil
.
getDiscountPrice
(
context
,
coupon
.
getDiscount
(
)
,
coupon
.
getMiniAmount
(
)
)
;
break
;
}
holder
.
priceText
.
setText
(
priceString
)
;
return
view
;
}
static
class
ViewHolder
{
TextView
titleText
;
TextView
infoText
;
TextView
priceText
;
}
}
|
而券列表的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
44
45
46
47
48
49
50
51
52
53
54
55
|
public
class
CouponListActivity
extends
KBaseActivity
implements
SwipeRefreshLayout
.
OnRefreshListener
{
private
SwipeRefreshLayout
swipeRefreshLayout
;
private
ListView
listView
;
private
CouponListAdapter
listAdapter
;
private
int
currentPage
=
1
;
@
Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
)
;
setContentView
(
R
.
layout
.
activity_coupon_list
)
;
initViews
(
)
;
getData
(
)
;
// TODO 添加上拉加载更多的功能
}
private
void
initViews
(
)
{
swipeRefreshLayout
=
(
SwipeRefreshLayout
)
findViewById
(
R
.
id
.
swipe_refresh_layout
)
;
swipeRefreshLayout
.
setOnRefreshListener
(
this
)
;
listView
=
(
ListView
)
findViewById
(
R
.
id
.
list_view
)
;
listAdapter
=
new
CouponListAdapter
(
this
)
;
listView
.
setAdapter
(
listAdapter
)
;
}
private
void
getData
(
)
{
this
.
appAction
.
listCoupon
(
currentPage
,
new
ActionCallbackListenerListCouponBO
>>
(
)
{
@
Override
public
void
onSuccess
(
ListCouponBO
>
data
)
{
if
(
!
data
.
isEmpty
(
)
)
{
if
(
currentPage
==
1
)
{
// 第一页
listAdapter
.
setItems
(
data
)
;
}
else
{
// 分页数据
listAdapter
.
addItems
(
data
)
;
}
}
swipeRefreshLayout
.
setRefreshing
(
false
)
;
}
@
Override
public
void
onFailure
(
String
errorEvent
,
String
message
)
{
Toast
.
makeText
(
context
,
message
,
Toast
.
LENGTH_SHORT
)
.
show
(
)
;
swipeRefreshLayout
.
setRefreshing
(
false
)
;
}
}
)
;
}
@
Override
public
void
onRefresh
(
)
{
// 需要重置当前页为第一页,并且清掉数据
currentPage
=
1
;
listAdapter
.
clearItems
(
)
;
getData
(
)
;
}
}
|
完结
终于写完了,代码也终于放上了github,为了让人更容易理解,因此很多都比较简单,没有再进行扩展。
github地址:https://github.com/keeganlee/kandroid