作为一个自学Android开发3年的码农,笔者此前进行过很多项目的开发,想把标题叫做项目架构,好像不太符合当前的身份,要做架构还有很长的路要走。这篇文章就献给那些初出茅庐的Android开发者,或者那些有想法想要在Android的领地上大展身手的小团队吧。笔者曾经作为Android开发的负责人,带领小兵小将团队作战。劣质的代码规范,不仅影响团队的合作,还影响团队领导者的自信心,更重要的是还影响项目的后期维护和再次开发。今天就来捋一捋我摸索到的那些规范和技巧。

项目的基本规范

这里先讲一些笔者在项目开发准备过程中总结的一些比较有用的规范,没规矩不成方圆。

1. 使用markdown编写接口文档

markdown基本上是一个程序员必备的小技能,如果你还不会markdown,那得赶紧跟上时代的步伐了。这个工具可以让你爱上写作,它支持简单的文字排版,表格绘制,代码高亮显示,简单的流程图绘制,甚至你可以在github上使用hexo博客引擎轻松搭建起个人博客。个人建议使用markdown来编写接口文档:

头部可以写一些项目的说明,比如状态码说明(0,1,2…各状态码的含义),当前文档更新内容,或者是对项目的一些要求和总结。

然后就是正文的编写,各个接口分为请求方式,用途,参数说明,请求示例,返回结果说明等。

最后只需要在文档的开头打一个[TOC]标记,markdown解析器就可以生成相应的文档目录,方便查看。比起word写的文档更加方便修改和维护。

现在来说一下我使用的json返回格式规范吧。json返回内容主要分为4个部分:

  • code : 状态码,为了方便,一般都从0开始,0表示成功,其它的可以自己定义
  • msg : 返回的消息内容,主要是错误的一些提示信息
  • obj : 返回的对象的内容,JSONObject。
  • objLists : 返回的多个对象的集合,JSONArray。

这样刻意的规范对接口编写以及前端的解析都大有裨益,解析的技巧到Android开发部分再讲,比如获取书籍的简单信息的接口可以是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
{
"code":200,
"msg":"成功",
"objList":[
{
"cover":"http://xxx",
"author":"海明威",
"about_book":"作者莫言,作于2000年夏,杭州..."
},
{...},{...}
]
}

2. 使用git管理代码

相比markdown,git更是一个开发工程师的必备技能,它的使用极大的利于团队合作,避免了重复作业。还没学过的童鞋得抓紧时间上车了。。。笔者以前使用过bitbucket,允许5个免费的团队私有项目,但经常网页刷不粗来,所以就转用国内开发团队开发的coding,允许10个免费的私有项目。

3. 写一个正式的项目说明文档

项目的说明文档应该随着项目的开发定期编写和汇总,它可以说明一些用到的第三库的使用方法,或者顺便记录一下项目开发中遇到的坑,比如完成了一个功能点就需要把使用到的技术记录下来,方便其它成员查看,也方便以后自己DIY写个个人博客。说明文档可以随着项目一起同步到git仓库。同样,接口文档也可以同步到git仓库,还可以顺便使用git的diff工具看看哪些地方做了修改。

4. 定期会议

笔者曾经就比较讨厌开会,感觉开会是一件特别浪费时间的事情。然而,如果条件允许,是需要每天开一个晨会的,至少也需要一周开一个周会。开会干什么呢?每个小伙伴需要总结一下自己做的任务或者是学到的东西,然后进行评估,这样的会议有利于维持团队小伙伴工作的积极性,小伙伴们学到的东西可以及时的展现出来,同时也利于掌握项目的进度,进行新任务的制定。

Android开发部分

这个部分主要是分享一些笔者搭建多个Android项目过程中总结的一些技巧,想要搭建更高级的项目结构还应该从设计模式的方面来考虑。

1. 项目搭建

笔者曾经也纠结于使用一个什么样的结构来搭建项目,MVC、MVP还是MVVM,但是个人觉得MVP这样的结构虽然设计的挺美好的,但是无端新添加了那么多类,各个接口调来调去的,看的眼花缭乱,不见得它可以增强项目的可维护性,另外对团队的其它成员也有了更高的要求,旧的项目想迁移到这个模式也是个挺费劲的事情。所以还是在MVC的基础上来做一些规范吧。对于项目搭建,也有一个非常火的开源项目来专门讲述了这件事情,详见Android-CleanArchitecture。我自己采用了一个更简单易懂一些的方式。

如下图所示,项目主体使用模块化的思想,各模块尽量在AS中新建作为一个module,这样各模块可以单独来维护。对于主体模块,将项目分成了界面层(对应默认的app目录),核心层,接口层,模型层,为减少耦合,界面层依赖于核心层,核心层依赖于接口层,界面层、核心层、模型层都依赖于模型层。其中接口层主要是处理一些网络相关的操作,核心层可以封装一些数据库操作、文件读写等实用的方法。笔者则将核心层和接口层合为了一层。

2. 接口层的JSON处理

前面已经设计了json格式的固定结构,如:

1
2
3
{"code": 0, "msg": "success"}
{"code": 0, "msg": "success", "obj":{...}}
{"code": 0, "msg": "success", "objList":[{...}, {...}], "currentPage": 1, "pageSize": 20, "maxCount": 2, "maxPage": 1}

a. 现在来封装一种用来解析的实体,用上Java的六脉神剑之一——泛型,注意变量名需要和json的键名保持一致以方便解析。

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
public class ApiResponse<T> {
//TODO 等待确定接口返回样式
private String code; //返回的状态码
private String msg; //返回的信息,比如成功,验证码已发送等
private T obj; // 单个对象
private T objLists; // 数组对象
// 判断网络请求是否成功
public boolean isSuccess() {
return code.equals("0") || code.equals("200");
}
//判断返回的是Bean还是List<Bean>
public boolean isListOfT() { //当objList不为null时为真
return objLists != null;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
public T getObj() {
return obj;
}
public T getObjList() {
return objLists;
}
}

T对应的是最终需要解析出来使用的实体类。

b. 然后是定义对应的请求接口:

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
public interface AccountAction {
/**
* 用手机号注册
*
* @param username 用户名
* @param authcode 验证码
* @param passwd MD5加密的密码
* @param phoneNum 手机号
*/
public void register(String username, String passwd, String phoneNum, String authcode, ActionCallback<Void> callback);
/**
* 登录
*
* @param loginName 登录名(手机号)
* @param password MD5加密的密码
* imei 手机IMEI串号
*/
public void login(String loginName, String password, String installation_id, ActionCallback<LocalBean> callback);
/**
* 退出登录
*/
public void logout(ActionCallback<Void> listener);
public void changeUserInfo(String username, String avatar, String signature, String school, String city, int gender, ActionCallback<Void> callback);
/**
* 获取用户的简单信息
*
* @param uid 想要查询的用户的id
*/
public void getSimpleUser(String uid, ActionCallback<User> callback);
}

c. 定义接口的具体实现

1
2
//传入参数进行网络请求相关的内容
public class AccountImpl implements AccountAction { ... }

原来的核心层处于接口层和界面层之间,向下调用Api,向上提供Action,它的核心任务就是处理复杂的业务逻辑;现在对核心层和接口层进行了合并,接口实现类需要做接口参数合法性判断,进行网络请求,解析数据,并最终返回数据对象。合并前的项目结构可以详细查看这篇博客,Android项目重构之路:实现篇

这里我使用了强大的okhttp来做网络请求并使用了gson做数据解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AccountImpl implements AccountAction {
private Context mContext;
public AccountImpl(Context context) {
this.mContext = context;
}
@Override
public void register(String username, String passwd, String phoneNum, String authcode, ActionCallback<Void> callback) {
new OkHttpRequest.Builder()
.url(Urls.REGISTER_URL)
.addParams("username", username)
.addParams("passwd", passwd)
.addParams("phoneNum", phoneNum)
.addParams("authCode", authcode)
.post(new ResponseCallback<Void>(callback));
//ResponseCallback里面需要传参ActionCallback,解析json并对结果进行适当的处理。
}
}

d. 定义请求回调接口

1
2
3
4
5
6
7
8
public interface ActionCallback<T> {
public void onSuccess(T data);
public void onFailure(String errorCode, String messge);
public void onBefore();
public void onAfter();
public void inProgress(float progress);
public void onError();
}

e. App层调用接口后,最终结果会通过该回调接口返回。最终使用某一个接口的画风会是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BookApplication extends Application{
private AccountAction api;
private static BookApplication instance;
@Override
public void onCreate() {
super.onCreate();
if(instance == null) instance = this;
//最好在Application的onCreate()方法中定义,不要多次创建实例
accountAction = new AccountImpl(this);
}
public AccountAction getApi(){
return accountAction;
}
}
//在其它地方使用接口
api.login(new ActionCallback<Void>(){
//处理结果
});

3. 定义Activity和Fragment的基类

a. BaseActivity:

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 BaseActivity extends AppCompatActivity {
public Context mContext;
public BookApplication application;
public AccountAction accountAction;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivitysManager.getInstanse().add(this);
mContext = this;
application = (BookApplication) getApplication();
accountAction = application.getAccountAction();
setContentView(setInflateId());
ButterKnife.bind(this);
init();
initAcition();
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivitysManager.getInstanse().killActivity(this);
}
//设置布局id
public abstract int setInflateId();
//视图,组件,数据的初始化
public abstract void init();
//事件监听
public void initAcition(){}
//Activity设置带返回按钮的Toolbar
public void setupBackToolbar(String title){
setupBackToolbar(R.id.toolbar,title);
}
//id不同的需要自行传入
public void setupBackToolbar(int toolbarId,String title){
Toolbar mToolbar = (Toolbar) findViewById(toolbarId);
setSupportActionBar(mToolbar);
getSupportActionBar().setHomeButtonEnabled(true); //显示小箭头
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationIcon(R.drawable.btn_back2);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
setTitle(title);
}
//以下为常用方法
public void showToast(String msg){
if(TextUtils.isEmpty(msg)) return;
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
}
protected void startActivity(Class<?> cls) {
Intent intent = new Intent(this, cls);
startActivity(intent);
}
public void startActivity(Class<?> cls, String... objs) {
Intent intent = new Intent(this, cls);
for (int i = 0; i < objs.length; i++) {
intent.putExtra(objs[i], objs[++i]);
}
startActivity(intent);
}
}

这里的技巧就是:

  1. 使用自定一个Activity栈管理器(ActivitysManager),在onCreate的时候将activity加入到栈中,在onDestroy时将activity移除栈,把这些操作写在基类中可以使子类进栈出栈自动进行,无需冗余代码。
  2. 把BaseActivity作为抽象类,提取出抽象的setInflateId()和init()方法(当然使用接口类也可以)
  3. 使用ButterKnife绑定View

b. BaseFragment:

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
public abstract class BaseFragment extends Fragment implements View.OnTouchListener{
private View rootView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if(null == rootView){
rootView = inflater.inflate(setInflateId(), container, false);
rootView.setOnTouchListener(this);
}
return rootView;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ButterKnife.bind(this,rootView);
init();
}
@Override
public void onDestroy() {
super.onDestroy();
BookApplication.getRefWatcher(getActivity()).watch(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
public abstract int setInflateId();
public abstract void init();
}

BaseFragment和BaseActivity代码类似,为了防止Fragment出现点击穿透的现象,需要实现OnTouchListener。

c. 带有刷新、加载更多、缺省显示的专用于列表展示的Fragment:

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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
public abstract class BaseRecyclerFragment<Bean> extends BaseFragment implements ActionCallback<List<Bean>> {
@Bind(R.id.recyclerView)
public HaoRecyclerView recyclerView;
@Bind(R.id.swiperefresh)
public SwipeRefreshLayout swipeRefresh;
@Bind(R.id.empty_view)
View emptyView;
@Bind(R.id.iv_default)
ImageView iv_empty;
@Bind(R.id.tv_default)
TextView tv_empty;
@OnClick(R.id.empty_view)
void emptyClick() {
toRefresh();
}
protected static final int STATE_REFRESH = 0;
protected static final int STATE_LOADMORE = 1;
protected int state = STATE_REFRESH; //默认刷新模式
protected boolean showFailureToast = true;
protected SwipeRefreshLayout.OnRefreshListener listener;
protected BaseRecyclerAdapter<Bean> adapter;
@Override
public int setInflateId() {
return R.layout.fragment_recycler;
}
@Override
public void init() {
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
adapter = setAdapter();
recyclerView.setAdapter(adapter);
recyclerView.setCanloadMore(false);//默认不能加载更多,item数量达到pageItemCount时再允许
if (canLoadmore()) {
pageItemCount = 15; //默认加载15页
} else {
pageItemCount = 2000;
}
initAction();
initRefreshLayout();
}
private boolean enableResumeRefresh = false;
public void enableResumeRefresh(boolean enableResumeRefresh) {
this.enableResumeRefresh = enableResumeRefresh;
}
int resumeCount = 0;
@Override
public void onResume() {
super.onResume();
if (resumeCount == 0) {
//自动刷新
swipeRefresh.post(new Runnable() {
@Override
public void run() {
if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
}
});
listener.onRefresh();
}
if (resumeCount < 2) resumeCount++;
if (enableResumeRefresh) {
if (resumeCount > 1 && currentPage == 1) {
//resume执行多次了,且当前在显示第一页则刷新
swipeRefresh.post(new Runnable() {
@Override
public void run() {
if (swipeRefresh != null) swipeRefresh.setRefreshing(true);
}
});
listener.onRefresh();
}
}
}
public abstract BaseRecyclerAdapter<Bean> setAdapter();
public abstract boolean canLoadmore();
//ItemAnimator,加载数据等操作,事件处理等
public abstract void initAction();
//下拉刷新
public void toRefresh() {
state = STATE_REFRESH;
hideDefaultView();
}
//加载更多
public void toLoadMore() {
state = STATE_LOADMORE;
}
public void setEmptyView(int drawableId, String text) {
setEmptyImg(drawableId);
setEmptyText(text);
}
public void setEmptyText(String text) {
tv_empty.setText(text);
}
public void setEmptyImg(int drawableId) {
iv_empty.setImageResource(drawableId);
}
public void hideDefaultView() {
emptyView.setVisibility(View.GONE);
}
private void initRefreshLayout() {
//设置自定义加载中和到底了效果
swipeRefresh.setColorSchemeResources(R.color.colorPrimary);
swipeRefresh.setProgressViewOffset(true, -20, 100);
ProgressView progressView = new ProgressView(getContext());
progressView.setIndicatorId(ProgressView.BallPulse);
progressView.setIndicatorColor(getResources().getColor(R.color.titleColorPrimary));//0xff69b3e0
recyclerView.setFootLoadingView(progressView);
setSwipeRefreshListener();
swipeRefresh.setOnRefreshListener(listener);
recyclerView.setLoadMoreListener(new LoadMoreListener() {
@Override
public void onLoadMore() {
toLoadMore();
new Handler().postDelayed(new Runnable() {
public void run() {
if (recyclerView != null) recyclerView.loadMoreComplete();
}
}, 1000);
}
});
}
public void setSwipeRefreshListener() {
this.listener = new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
toRefresh();
recyclerView.loadMoreComplete();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (swipeRefresh != null) swipeRefresh.setRefreshing(false);
if (recyclerView != null) recyclerView.refreshComplete();
}
}, 1000);
}
};
}
protected int pageItemCount = 2000; //单页显示数量 数量大表示是单页显示模式
public void setPageItemCount(int count) {//可不必调用
pageItemCount = count; //单页显示的Fragment不用调用此方法
}
protected boolean enableEmptyView = true;
public void showDefaultView() {
if (enableEmptyView)
emptyView.setVisibility(View.VISIBLE);
}
protected int currentPage = 1; //标识当前加载的分页
@Override
public void onSuccess(List<Bean> datas) {
if (datas!=null){
Log.d("TAG", "onSuccess: "+datas.size());
}
if (datas != null && datas.size() >= pageItemCount) {
//获取到的数量足够才允许加载更多
recyclerView.setCanloadMore(true);
} else {
recyclerView.setCanloadMore(false);
}
if (state == STATE_REFRESH) {//刷新模式currentPage置1
currentPage = 1;
if (datas == null || datas.size() == 0) {
showDefaultView();
} else {
hideDefaultView();
}
adapter.setDataList(datas);
} else {//加载更多模式currentPage自增
currentPage++;
adapter.addItems(datas);
}
}
@Override
public void onFailure(String errorCode, String message) {
if (!TextUtils.isEmpty(message)) {
if (showFailureToast) showToast(message);
}
if (adapter.getItemCount() <= 0) {
showDefaultView();
}
}
@Override
public void onBefore() {
}
@Override
public void onAfter() {
}
@Override
public void inProgress(float progress) {
}
@Override
public void onError() {
if (adapter.getItemCount() <= 0) {
showDefaultView();
}
}
public void setShowFailureToast(boolean isShowFailureToast) {
this.showFailureToast = isShowFailureToast;
}
}

这个类需要配合layout一起使用,主要的用处有:

  1. setPageItemCount() 设置单页显示数量
  2. setEmptyView() 定义缺省显示图标
  3. enableResumeRefresh() 是否允许在onResume()中刷新页面
  4. showDefaultView() 是否允许没有数据的时候显示缺省图标
  5. canLoadmore() 是否可以加载更多,默认是单页加载模式,即不分页,要想分页只需调用setPageItemCount()方法设置单页显示数量即可

然后一个子类的画风会是这样的:

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
public class BookFinishedFragment extends BaseRecyclerFragment<BorrowBook> {
@Override
public BaseRecyclerAdapter setAdapter() {
return new BookHistoryAdapter();
}
@Override
public boolean canLoadmore() {
return true;
}
@Override
public void initAction() {
BookApplication.getInstance().getAccountAction().getHistoryList(2,15,1,this);
setShowFailureToast(false); //禁止出现返回失败的toast
}
@Override
public void toRefresh() {
super.toRefresh();
BookApplication.getInstance().getAccountAction().getHistoryList(2,15,1,this);
}
@Override
public void toLoadMore() {
super.toLoadMore();
BookApplication.getInstance().getAccountAction().getHistoryList(2,15, currentPage + 1, BookFinishedFragment.this);
}
}

有木有,几十行代码搞定了。当然adapter的实现就另当别论了(同样有干货技巧)。

另外对于在RecyclerView上实现刷新和加载更多的实现方法,目前有两种方式,一种是直接修改RecyclerView,一种是增加一个父级View,比如SwipeRefreshLayout。笔者比较推荐第二种方式,或者用一个子类继承RecyclerView,RecyclerView由官方继续维护和更新。

4. 自定义强大的Adapter

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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
public abstract class BaseRecyclerAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements CommonHolder.OnNotifyChangeListener {
private List<T> dataList = new ArrayList<>();
private boolean enableHead = false;
CommonHolder headHolder;
ViewGroup rootView;
public final static int TYPE_HEAD = 0;
public static final int TYPE_CONTENT = 1;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
rootView = parent;
//设置ViewHolder
int type = getItemViewType(position);
if (type == TYPE_HEAD) {
return headHolder;
} else {
return setViewHolder(parent);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
runEnterAnimation(holder.itemView, position);
//数据绑定
if (enableHead) {
if (position == 0) {
//((CommonHolder) holder).bindData(null);//头部不填充数据
} else {
((CommonHolder) holder).bindData(dataList.get(position - 1));
}
} else {
((CommonHolder) holder).bindData(dataList.get(position));
}
((CommonHolder) holder).setOnNotifyChangeListener(this);
}
public ViewGroup getRootView() {
return rootView;
}
@Override
public int getItemCount() {
if (enableHead) {
return dataList.size() + 1;
}
return dataList.size();
}
@Override
public int getItemViewType(int position) {
if (enableHead) {
if (position == 0) {
return TYPE_HEAD;
} else {
return TYPE_CONTENT;
}
} else {
return TYPE_CONTENT;
}
}
private int lastAnimatedPosition = -1;
protected boolean animationsLocked = false;
private boolean delayEnterAnimation = true;
private void runEnterAnimation(View view, int position) {
if (animationsLocked) return;
if (position > lastAnimatedPosition) {
lastAnimatedPosition = position;
view.setTranslationY(DensityUtil.dip2px(view.getContext(), 100));//(position+1)*50f
view.setAlpha(0.f);
view.animate()
.translationY(0).alpha(1.f)
.setStartDelay(delayEnterAnimation ? 20 * (position) : 0)
.setInterpolator(new DecelerateInterpolator(2.f))
.setDuration(500)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animationsLocked = true;
}
}).start();
}
}
@Override
public void onNotify() {
//提供给CommonHolder方便刷新视图
notifyDataSetChanged();
}
public void setDataList(List<T> datas) {
dataList.clear();
if (null != datas) {
dataList.addAll(datas);
}
notifyDataSetChanged();
}
public void clearDatas() {
dataList.clear();
notifyDataSetChanged();
}
/**
* 添加数据到前面
*/
public void addItemsAtFront(List<T> datas) {
if (null == datas) return;
dataList.addAll(0, datas);
notifyDataSetChanged();
}
/**
* 添加数据到尾部
*/
public void addItems(List<T> datas) {
if (null == datas) return;
dataList.addAll(datas);
notifyDataSetChanged();
}
/**
* 添加单条数据
*/
public void addItem(T data) {
if (null == data) return;
dataList.add(data);
notifyDataSetChanged();
}
/**
* 删除单条数据
*/
public void deletItem(T data) {
dataList.remove(data);
Log.d("deletItem: ", dataList.remove(data) + "");
notifyDataSetChanged();
}
/**
* 设置是否显示head
*
* @param ifEnable 是否显示头部
*/
public void setEnableHead(boolean ifEnable) {
enableHead = ifEnable;
}
public void setHeadHolder(CommonHolder headHolder1) {
enableHead = true;
headHolder = headHolder1;
}
public void setHeadHolder(View itemView) {
enableHead = true;
headHolder = new HeadHolder(itemView);
notifyItemInserted(0);
}
public CommonHolder getHeadHolder() {
return headHolder;
}
/**
* 子类重写实现自定义ViewHolder
*/
public abstract CommonHolder<T> setViewHolder(ViewGroup parent);
static class HeadHolder extends CommonHolder<Void> {
public HeadHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
@Override
public void bindData(Void aVoid) {//不用实现
}
}
}

实际上RecyclerView本身的Adapter就挺强大的,这里进行了进一步的封装。首先是提取出来了一个CommonHolder,BaseRecyclerAdapter的子类只需要实现setViewHolder()方法即可;另外该Adapter还支持添加一个Header;支持在CommonHolder中调用notifyChange()方法来通知Adapter某一个Item发生了改变。

你可能觉得这样的改动意义不大,但是当遇到这样的场景。比如你在做一个聊天页面,聊天消息的格式有纯文本,有图片,有语音或者是其它的格式,并且对一种类型的消息也有发送者和接受者两种形式,如果是在一个Adapter中编写,肯定代码一不小心就是几千行,而且各种id,各种事件,代码的可读性简直惨不忍睹。如果采用我的这种方式呢,它就会是另外一种画风。比如你可以定义一个TextHolder,ImageHolder,VoiceHolder,每个类几百行代码,可读性提升的不是一点点。如下所示:

聊天页面适配器:

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
public class ImMultiItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final int ITEM_LEFT = 0;
private final int ITEM_RIGHT = 1;
RightHolder.MsgErrorResendAction errorResendAction;
// 时间间隔最小为十分钟
private final long TIME_INTERVAL = 10 * 60 * 1000;
private List<AVIMMessage> messageList = new ArrayList<AVIMMessage>();
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ITEM_LEFT) {
return new LeftHolder(parent.getContext(), parent);
} else if (viewType == ITEM_RIGHT) {
return new RightHolder(parent.getContext(), parent);
} else {
//TODO
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((CommonHolder)holder).bindData(messageList.get(position));
if (holder instanceof LeftHolder) {
((LeftHolder)holder).showTimeView(shouldShowTime(position));
} else if (holder instanceof RightHolder) {
((RightHolder)holder).showTimeView(shouldShowTime(position));
if(errorResendAction != null){
((RightHolder)holder).setMsgErrorResendAction(errorResendAction);
}
}
}
@Override
public int getItemViewType(int position) {
AVIMMessage message = messageList.get(position);
if (message.getFrom().equals(ChatManager.getInstance().getSelfId())) {
return ITEM_RIGHT;
} else {
return ITEM_LEFT;
}
}
//...
}

当然我这里偷工减料写成了LeftHolder跟RightHolder两种Holder:

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 class LeftHolder extends CommonHolder {
@Bind(R.id.tv_time)
protected TextView timeView;
@Bind(R.id.tv_content)
protected TextView contentView;
@Bind(R.id.iv_photo)
protected ImageView iv_photo;
@Bind(R.id.avatar)
CircleImageView avatar;
public LeftHolder(Context context, ViewGroup root) {
super(context, root, R.layout.item_chat_left);
}
@OnClick(R.id.tv_content)
public void onContentClick() {
}
@Override
public void bindData(Object o) {
final AVIMMessage message = (AVIMMessage) o;
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm");
String time = dateFormat.format(message.getTimestamp());
String content = getContext().getString(R.string.im_unknown);
if (message instanceof AVIMTextMessage) {
content = ((AVIMTextMessage) message).getText();
contentView.setText(content);
contentView.setVisibility(View.VISIBLE);
iv_photo.setVisibility(View.GONE);
} else if (message instanceof AVIMImageMessage) {
//...
iv_photo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ArrayList<String> pics = new ArrayList<String>();
pics.add(toLoadPicPath);
int[] location = new int[2];
iv_photo.getLocationOnScreen(location);
int width = iv_photo.getWidth();
int height = iv_photo.getHeight();
PhotoViewActivity.showWithParams(getContext(), pics, 0, width, height, location[0], location[1]);
}
});
}
timeView.setText(time);
UserHelper.getInstance().loadAvatar(avatar, UserHelper.getInstance().getAvatar(message.getFrom()));
avatar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
UserDetailActivity.launch((Activity) getContext(), message.getFrom());
}
});
}
public void showTimeView(boolean isShow) {
timeView.setVisibility(isShow ? View.VISIBLE : View.GONE);
}
}

同样也是几十行代码搞定了,而且代码的编写也很简单,在构造函数中绑定View的id,在bindData()方法中绑定数据或者处理业务逻辑。

其它规范和技巧

1. layout文件命名规范

随着项目的膨胀,layout文件必然很多,没有个规范是不行的。比如可以采用这样的方式:

  • Activity的layout文件以activity_为前缀,甚至是activity_share_,share为Activity所属的业务范围,比如这里的share表示分享相关的Activity
  • dialog相关layout使用dialog_为前缀
  • 需要包含在其它layout中的公用布局文件使用inc_为前缀
  • 友盟分享layoout使用umeng_为前缀
  • 自定义View类使用view_为前缀
  • 适配器相关的布局文件使用item_为前缀

2. 项目结构

此处以我做的一个项目的结构来做说明。先看app目录,im、wxapi、zxing都是几个小模块,views中是一些自定义view

再看ui目录,笔者把app界面的入口(SplashActivity、LoginActivity、MainActivity)放这了,再也不用漫天找入口了。然后account、book、share这些都是按照业务分的目录。account表示账户相关的操作,神马修改密码、查看用户资料全在这了;book表示书籍相关类,比如书籍列表、书籍详情;share表示分享相关的业务,比如分享圈、评论、点赞逻辑都在这了!看着就一目了然有木有!!!

3. Activity间跳转

笔者在项目中规定,有需要通过intent传参跳转Activity的,必须在要跳转到的Activity里面定义一个static方法,把参数名什么的放里面。
比如笔者需要从一个书籍列表展示页面跳转到详情页面,可以在BookDetailActivity这么搞:

1
2
3
4
5
public static void launch(Activity activity, String lid) {
Intent intent = new Intent(activity, BookDetailActivity.class);
intent.putExtra("lid", lid);
activity.startActivity(intent);
}

启动BookDetailActivity则必须调用这条方法。这样做一是为了防止非法的打开一个需要传参的Activity,二是跳转的参数名直接写在了要跳转到的Activity,防止传参错误,同时也更容易去检查这种错误。

4. AndroidManifest.xml文件规范

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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huiman.bookfarm">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<application
android:name=".BookApplication"
android:allowBackup="true"
android:icon="@mipmap/logo"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<!-- ***************** meta-data 配置 ******************************************** -->
<!-- ********************* Activity配置****************************************** -->
<!-- ***************** Service、BroadcastReceiver配置 ******************************* -->
<!-- ***************** 使用到的第三方sdk组件配置 ******************************* -->
</application>
</manifest>

文件大致分为以上四个部分,各个组件上面需要有功能说明。

5. 从一个主题的角度来看待页面的文字和配色属性

设想现在有了新的需求,原来的单主题的UI界面需要多添加几个主题,比如需要增加夜间模式,如果没有相应的规范来约束,改起来必定是相当头疼。不如从创建一个项目开始就把它设想成是一个多主题的应用,这样你所有的界面的字体大小,背景颜色等都会属于这一套主题,这回无形中规范你的value中属性的命名。

给Activity切换主题需要使用setTheme(R.style.ThemeDay)这一条方法,你定义的style则是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<style name="ThemeDay">
<item name="main_bg">@color/bg_white</item>
<item name="content_bg">@color/content_light</item>
<item name="text_color1">@color/text_dark1</item>
<item name="text_color2">@color/text_dark2</item>
<item name="text_color3">@color/text_dark3</item>
<item name="text_color4">@color/text_dark3</item>
<item name="bg2">@color/bg_light2</item>
<item name="bg_toolbar">@color/bg_white</item>
</style>
<style name="ThemeNight">
<item name="main_bg">@color/bg_dark_content</item>
<item name="content_bg">@color/bg_dark</item>
<item name="text_color1">@color/text_dark4</item>
<item name="text_color2">@color/text_dark3</item>
<item name="text_color3">@color/text_dark2</item>
<item name="text_color4">@color/text_dark1</item>
<item name="bg2">@color/bg_dark</item>
<item name="bg_toolbar">@color/bg_dark_content</item>
</style>

当然字体颜色最好写颜色名,可以自定一些文字属性,比如TextPrimaryColor、TextSecondColor、TextThirdColor,TextSizeBig、TextSizeMedium等等。

6. 其它需要改进的地方

项目中笔者为了实现方便,在Holder中需要使用到Context的地方使用了强转的方式,比如需要点击一个Item跳转到另一个界面,笔者一般是这样做的:

1
2
3
4
5
6
itemView.setOnClickListener(new OnClickListener{
@Override
public void onClick(View v){
((Activity)context).startActivity(context,DetailActivity.class);
}
});

这里使用了暴力强转的方式,因为一个View所属的context必然是一个Activity(不可能是Application、Service,Dialog实际上也是Activity),这样的做法虽然也可用,但还是有一些隐患的,有可能会导致Crash。所以笔者建议可以试试使用EventBus来处理这样的事件。