官方项目地址
https://github.com/googlesamples/android-architecture
项目结构图
Project视图下
- src/main-项目路径
- src/androidTest-UI层测试
- src/androidTestMock-UI层测试mock数据支持
- src/test-业务层单元测试
- src/mock-业务层单元测试mock数据支持
项目包是按功能来划分的,每个包内又有XActivity、XContract、XFragment、XPresenter。其中XContract有View实体和Presenter实体所用到的接口;XFragment是View的实体;XPresenter是Presenter的实体;XActivity做些关联及其他基础操作。
app的build.gradle
因为当前没有对测试做深入探究,所以测试相关,要等后面作深入研究后再做解析
dependencies {
// App's dependencies, including test
compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
compile "com.android.support:cardview-v7:$rootProject.supportLibraryVersion"
compile "com.android.support:design:$rootProject.supportLibraryVersion"
compile "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion"
compile "com.android.support:support-v4:$rootProject.supportLibraryVersion"
compile "com.android.support.test.espresso:espresso-idling-resource:$rootProject.espressoVersion"
compile "com.google.guava:guava:$rootProject.guavaVersion"
// Dependencies for local unit tests
testCompile "junit:junit:$rootProject.ext.junitVersion"
testCompile "org.mockito:mockito-all:$rootProject.ext.mockitoVersion"
testCompile "org.hamcrest:hamcrest-all:$rootProject.ext.hamcrestVersion"
// Android Testing Support Library's runner and rules
androidTestCompile "com.android.support.test:runner:$rootProject.ext.runnerVersion"
androidTestCompile "com.android.support.test:rules:$rootProject.ext.runnerVersion"
// Dependencies for Android unit tests
androidTestCompile "junit:junit:$rootProject.ext.junitVersion"
androidTestCompile "org.mockito:mockito-core:$rootProject.ext.mockitoVersion"
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
// Espresso UI Testing
androidTestCompile "com.android.support.test.espresso:espresso-core:$rootProject.espressoVersion"
androidTestCompile ("com.android.support.test.espresso:espresso-contrib:$rootProject.espressoVersion")
androidTestCompile "com.android.support.test.espresso:espresso-intents:$rootProject.espressoVersion"
// Resolve conflicts between main and test APK:
androidTestCompile "com.android.support:support-annotations:$rootProject.supportLibraryVersion"
androidTestCompile "com.android.support:support-v4:$rootProject.supportLibraryVersion"
androidTestCompile "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion"
}
工程文件
BaseView.java
public interface BaseView<T> {
// view关联presenter
void setPresenter(T presenter);
}
BasePresenter.java
public interface BasePresenter {
//第一次加载数据
void start();
}
TasksActivity.java
启动的第一个activity,其实跟MVP相关的就下面代码,其他都是配置菜单栏之类的。当前项目的实现都是,activity中使用FrameLayout控件来放一个fragment作为展示的界面;activity创建presenter并将其与fragment关联。
TasksFragment tasksFragment =(TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (tasksFragment == null) {
// 实例化tasksFragment
tasksFragment = TasksFragment.newInstance();
// 将tasksFragment添加到FrameLayout
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
}
// 实例化presenter
mTasksPresenter = new TasksPresenter(Injection.provideTasksRepository(getApplicationContext()), tasksFragment);
我们看到先new的TasksView然后是new TasksPresenter,会不会出现TasksView执行onResume时,TasksPresenter还没有赋值呢?做了个测试,验证了下是赋过值的。相关方法执行顺序
TasksContract.java(View层与Presenter层接口)
当前功能的view层和presenter层的接口,TasksFragment.java和TasksPresenter.java都要实现相关的方法。罗列出部分
public interface TasksContract {
interface View extends BaseView<Presenter> {
//setPresenter()在BaseView中
//当前界面显示tasks
void showTasks(List<Task> tasks);
//跳转到添加task页
void showAddTask();
}
interface Presenter extends BasePresenter {
//start()在BasePresenter中
//Fragment的onActivityResult调用
void result(int requestCode, int resultCode);
//加载tasks
void loadTasks(boolean forceUpdate);
//item的点击会调用用的三个方法
void openTaskDetails(@NonNull Task requestedTask);
void completeTask(@NonNull Task completedTask);
void activateTask(@NonNull Task activeTask);
}
}
FakeTasksRemoteDataSource.java(model层)
TasksActivity中
//TasksPresenter第一个参数,就是model层的实例
mTasksPresenter = new TasksPresenter(Injection.provideTasksRepository(getApplicationContext()), tasksFragment);
TasksRepository单例方式实例化对象
public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,TasksDataSource tasksLocalDataSource) {
if (INSTANCE == null) {
INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
}
return INSTANCE;
}
接口对象中,会用到的几个方法
public interface TasksDataSource {
interface LoadTasksCallback {
void onTasksLoaded(List<Task> tasks);
void onDataNotAvailable();
}
void getTasks(@NonNull LoadTasksCallback callback);
}
流程
应用启动后,TaskActivity初始化做的事情
环节有所简化(显示/隐藏加载进度框;Model层的强制刷新等),不然流程图占屏幕太大了
如何抽取接口方法?
Model层
与数据库、网络请求有关的操作,如getTasks
void getTasks(@NonNull LoadTasksCallback callback);
Presenter层
界面的初始化界面数据、响应View层的事件
//onActivityResult的事件调用
void result(int requestCode, int resultCode);
//初始化
void loadTasks(boolean forceUpdate);
//点击添加新task
void addNewTask();
//点击task查看详情
void openTaskDetails(@NonNull Task requestedTask);
View层
当前项目中,几个比较典型的
这层抽取的细节比较多,所以有些难取舍。
1. 让Presenter能够关联到View,也是基类中的方法
public void setPresenter(@NonNull TasksContract.Presenter presenter);
2. 初始化数据成功后,刷新显示界面;初始化数据成功后,但是tasks长度为0则showNoTasks
public void showTasks(List<Task> tasks);
public void showNoTasks();
3. 显示/隐藏正在加载对话框
public void setLoadingIndicator(boolean active);
4. 点击跳转到其他界面,在View调用提供的方法
mNoTaskAddView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showAddTask();
}
});
@Override
public void showAddTask() {
Intent intent = new Intent(getContext(), AddEditTaskActivity.class);
startActivityForResult(intent, AddEditTaskActivity.REQUEST_ADD_TASK);
}
点击跳转到其他界面,在Presenter调用View提供的方法
//TasksPresenter代码
@Override
public void openTaskDetails(@NonNull Task requestedTask) {
checkNotNull(requestedTask, "requestedTask cannot be null!");
mTasksView.showTaskDetailsUi(requestedTask.getId());
}
//TasksFragment代码
TaskItemListener mItemListener = new TaskItemListener() {
@Override
public void onTaskClick(Task clickedTask) {
mPresenter.openTaskDetails(clickedTask);
}
};
@Override
public void showTaskDetailsUi(String taskId) {
// in it's own Activity, since it makes more sense that way and it gives us the flexibility
// to show some Intent stubbing.
Intent intent = new Intent(getContext(), TaskDetailActivity.class);
intent.putExtra(TaskDetailActivity.EXTRA_TASK_ID, taskId);
startActivity(intent);
}
两种方式均可,但个人更偏向于第一种。点击事件当前View处理即可
5. onActivityResult方法
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
mPresenter.result(requestCode, resultCode);
}
6. Toast提示信息,也要抽取成方法。方便View界面调用
private void showMessage(String message);
7. ListView或Adapter中如果需要使用Presenter层实例方法时,以接口回调形式实现
/**
* Listener for clicks on tasks in the ListView.
*/
TaskItemListener mItemListener = new TaskItemListener() {
@Override
public void onTaskClick(Task clickedTask) {
mPresenter.openTaskDetails(clickedTask);
}
@Override
public void onCompleteTaskClick(Task completedTask) {
mPresenter.completeTask(completedTask);
}
@Override
public void onActivateTaskClick(Task activatedTask) {
mPresenter.activateTask(activatedTask);
}
};
private static class TasksAdapter extends BaseAdapter {
private List<Task> mTasks;
private TaskItemListener mItemListener;
public TasksAdapter(List<Task> tasks, TaskItemListener itemListener) {
setList(tasks);
mItemListener = itemListener;
}
}
其他的都类比着来就可以了。
其他项目中我抽取的
1. 设置title栏的值;设置title栏按钮的文字
@Override
public void setUiTitle(int titleId){
mTvTitle.setText(titleId);
}
@Override
public void setRightBtText(int rightBtTextId){
mTvRight.setText(rightBtTextId);
}
2. 添加点击事件当Presenter实例化的时候调用;处理点击事件
//添加点击事件的接口
public void initOnClickListener();
//View中处理点击事件
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_right://点击添加分类
mGoodsKindPresenter.addKind();
break;
}
}
3. 添加文本监听,当Presenter实例化的时候调用initTextChangedListener,添加文本监听事件
/**
* 添加文本改变的监听
*/
@Override
public void initTextChangedListener();
4. 获取activity的context参数
public void Context getContext();
5. 界面弹出框及内容的点击事件监听
/**
* 加载类别弹出框ui
* @param parentList
* @param childrenList
*/
void showDialogSelectedKinds(List<KindDomain> parentList, List<KindDomain> childrenList);
/**
* 选取父分类
* @param domain
*/
void dialogOnItemClick(KindDomain domain);
我就罗列这么多吧,其他的自己琢磨吧!
View和Model之间的耦合度降低,使其更关注自身业务逻辑,结构清晰,维护方便。这样也便于单元测试,代码框架更适用于快速迭代开发。但也有缺点就是类很多