
前言
在本次项目重构之前,我的项目采用的是什么架构呢?
额,没有架构...或者说,不那么标准的MVC,一个页面就是一个Activity或者Fragment,各种数据,网络请求,响应都写在Activity或者Fragment--这两个不怎么标准的Controller中,代码混乱,而且长长长长长长,写代码的时候一时爽,到了维护阶段,那酸爽,不敢相信。采用新的架构迫在眉睫。
事实上,已经有很多的App项目采用了MVP或者MVVM等架构。由于并没有那么权威的实现,很多开发者也陷入了选择困难症,在各种架构直接摇摆不定,找不到适合自己项目架构。而Google也适时的推出了一系列官方示例用于参考。
Google示例项目
示例项目以一个TODO APP为例,目前仍然在进行中。本次项目重构采用的是基础版 todo-mvp
示例项目的代码组织方式和我之前以adapter, activity, fragment等组织方式不同,采用的是按照功能划分,一种功能就是一个包,包内文件以xxxactivity, xxxfragment, xxxcontract, xxxpresenter命名,xxx代表着所要实现的功能。本次项目重构两种方式均有采用。
重构
首先是仿照TODO MVP,建立两个Base接口,BaseView和BasePresenter,这两个基本接口是所有的View和Presenter的基类。
public interface BasePresenter {
void start();
}
BasePresenter中有方法start(),作用是Presenter开始获取数据并改变界面显示,调用时机为Fragment的onResume()方法中。
public interface BaseView {
void setPresenter(T presenter);
void initViews(View view);
}
BaseView中有方法setPresenter(),将Presenter示例传入view,调用时机为Presenter实现类的构造方法中,initViews()传入View实例,用于初始化界面元素,调用时机为Fragment的onCreate()方法中。
接着创建契约类,用于统一管理View和Presenter的所有接口。这里以知乎日报的部分为例。
public interface ZhihuDailyContract {
interface View extends BaseView {
void showError();
void showLoading();
void stopLoading();
void showResults(ArrayList list);
void showNetworkError();
}
interface Presenter extends BasePresenter {
void loadPosts(long date, boolean clearing);
void refresh();
void loadMore(long date);
void startReading(int position);
void goToSettings();
}
}
然后创建相应的Activity,在官方的示例项目中,Activity是作为View和Presenter的桥梁使用,用于创建View和Presenter实例。本项目由于涉及了TabLayout和ViewPager的使用,所以创建View和Presenter的部分我放到了ViewPager的Adapter中实现。
public class MainPagerAdapter extends FragmentPagerAdapter {
private String[] titles;
private final Context context;
public MainPagerAdapter(FragmentManager fm, Context context) {
super(fm);
this.context = context;
titles = context.getResources().getStringArray(R.array.page_titles);
}
@Override
public Fragment getItem(int position) {
if (position == 1){
GuokrFragment fragment = GuokrFragment.newInstance();
new GuokrPresenter(context, fragment);
return fragment;
} else if (position == 2){
DoubanMomentFragment fragment = DoubanMomentFragment.newInstance();
new DoubanMomentPresenter(context, fragment);
return fragment;
}
ZhihuDailyFragment fragment = ZhihuDailyFragment.newInstance();
new ZhihuDailyPresenter(context, fragment);
return fragment;
}
@Override
public int getCount() {
return titles.length;
}
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
}
Fragment在示例项目中的角色为View的具体实现类。
public class ZhihuDailyFragment extends Fragment
implements ZhihuDailyContract.View{
private RecyclerView recyclerView;
private SwipeRefreshLayout refresh;
private FloatingActionButton fab;
private ZhihuDailyNewsAdapter adapter;
private ZhihuDailyContract.Presenter presenter;
public ZhihuDailyFragment() {
}
public static ZhihuDailyFragment newInstance() {
return new ZhihuDailyFragment();
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_douban_zhihu_daily,container,false);
initViews(view);
presenter.loadPosts(Calendar.getInstance().getTimeInMillis(), false);
refresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
presenter.refresh();
}
});
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
...
}
});
return view;
}
@Override
public void setPresenter(ZhihuDailyContract.Presenter presenter) {
if (presenter != null) {
this.presenter = presenter;
}
}
@Override
public void initViews(View view) {
recyclerView = (RecyclerView) view.findViewById(R.id.rv_main);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(),LinearLayoutManager.VERTICAL));
refresh = (SwipeRefreshLayout) view.findViewById(R.id.refresh);
fab = (FloatingActionButton) view.findViewById(R.id.fab);
fab.setRippleColor(getResources().getColor(R.color.colorPrimaryDark));
}
@Override
public void showError() {
Snackbar.make(fab, R.string.loaded_failed,Snackbar.LENGTH_SHORT).show();
}
@Override
public void showLoading() {
refresh.post(new Runnable() {
@Override
public void run() {
refresh.setRefreshing(true);
}
});
}
@Override
public void stopLoading() {
refresh.post(new Runnable() {
@Override
public void run() {
refresh.setRefreshing(false);
}
});
}
@Override
public void showResults(ArrayList list) {
if (adapter == null) {
adapter = new ZhihuDailyNewsAdapter(getContext(), list);
adapter.setItemClickListener(new OnRecyclerViewOnClickListener() {
@Override
public void OnItemClick(View v, int position) {
presenter.startReading(position);
}
});
recyclerView.setAdapter(adapter);
} else {
adapter.notifyDataSetChanged();
}
}
@Override
public void showNetworkError() {
Snackbar.make(fab,R.string.no_network_connected,Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.go_to_set, new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.goToSettings();
}
}).show();
}
}
创建Presenter。
public class ZhihuDailyPresenter implements ZhihuDailyContract.Presenter, OnStringListener {
private ZhihuDailyContract.View view;
private Context context;
private StringModelImpl model;
private ArrayList list = new ArrayList();
public ZhihuDailyPresenter(Context context, ZhihuDailyContract.View view) {
this.context = context;
this.view = view;
this.view.setPresenter(this);
model = new StringModelImpl(context);
}
@Override
public void loadPosts(long date, boolean clearing) {
view.showLoading();
if (clearing) {
list.clear();
}
model.load(Api.ZHIHU_HISTORY + formatter.ZhihuDailyDateFormat(date), this);
}
@Override
public void refresh() {
list.clear();
loadPosts(Calendar.getInstance().getTimeInMillis(), true);
}
@Override
public void loadMore(long date) {
if (NetworkState.networkConnected(context)) {
model.load(Api.ZHIHU_HISTORY + formatter.ZhihuDailyDateFormat(date), this);
} else {
view.showNetworkError();
}
}
@Override
public void startReading(int position) {
context.startActivity(new Intent(context, ZhihuDetailActivity.class)
.putExtra("id",list.get(position).getId())
);
}
@Override
public void goToSettings() {
context.startActivity(new Intent(Settings.ACTION_SETTINGS));
}
@Override
public void start() {
}
@Override
public void onSuccess(String result) {
Gson gson = new Gson();
ZhihuDailyNews post = gson.fromJson(result, ZhihuDailyNews.class);
for (ZhihuDailyNews.Question item : post.getStories()) {
list.add(item);
}
view.showResults(list);
view.stopLoading();
}
@Override
public void onError(VolleyError error) {
view.stopLoading();
view.showError();
}
}
Presenter获取到了View,并通过调用setPresenter()方法将自身传入,如果需要对改变界面显示,直接调用View层的方法即可。这样Presenter就于View层实现了分离。
最后是Model层的实现。由于使用了Gson,数据的返回类型只需要为String类型即可。
public interface OnStringListener {
/**
* 请求成功时回调
* @param result
*/
void onSuccess(String result);
/**
* 请求失败时回调
* @param error
*/
void onError(VolleyError error);
}
定义了两个方法,分别为请求成功时和请求失败时的回调。
然后需要一个StringModel的实现类--StringModelImpl。
public class StringModelImpl {
private Context context;
public StringModelImpl(Context context) {
this.context = context;
}
public void load(String url, final OnStringListener listener) {
StringRequest request = new StringRequest(url, new Response.Listener() {
@Override
public void onResponse(String s) {
listener.onSuccess(s);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError volleyError) {
listener.onError(volleyError);
}
});
VolleySingleton.getVolleySingleton(context).addToRequestQueue(request);
}
}
这样,Model, View, Presenter均已实现,实现了各个层次的分离。
采用MVP架构进行重构,代码量上相对于原项目时有所增加的,但这种数量的增加相对于MVP架构带来的好处是显而易见的。当然这是对于代码量比较大的项目而言,平时用于练手的小项目就没有必要强项目所难,勉强的实现MVP了,这样只会增加代码量而已。