作为一个自学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> { private String code; private String msg; private T obj; private T objLists; public boolean isSuccess() { return code.equals("0") || code.equals("200"); } public boolean isListOfT() { 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)); } }
|
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; 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); } public abstract int setInflateId(); public abstract void init(); public void initAcition(){} public void setupBackToolbar(String title){ setupBackToolbar(R.id.toolbar,title); } 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); } }
|
这里的技巧就是:
- 使用自定一个Activity栈管理器(ActivitysManager),在onCreate的时候将activity加入到栈中,在onDestroy时将activity移除栈,把这些操作写在基类中可以使子类进栈出栈自动进行,无需冗余代码。
- 把BaseActivity作为抽象类,提取出抽象的setInflateId()和init()方法(当然使用接口类也可以)
- 使用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); if (canLoadmore()) { pageItemCount = 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) { 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(); 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)); 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; } 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; if (datas == null || datas.size() == 0) { showDefaultView(); } else { hideDefaultView(); } adapter.setDataList(datas); } else { 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一起使用,主要的用处有:
- setPageItemCount() 设置单页显示数量
- setEmptyView() 定义缺省显示图标
- enableResumeRefresh() 是否允许在onResume()中刷新页面
- showDefaultView() 是否允许没有数据的时候显示缺省图标
- 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); } @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; 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) { } 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)); 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() { 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 { 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"> </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来处理这样的事件。