概述
引言
最近有打算写一些独立的App的打算,所以对现在的Android架构与技术的选择进行了重新的思考,同时总结了对三个典型架构深入的理解及它们的应用方法。
MVC
概念
MVC是Model(模型)-View(视图)-Controller(控制器)的缩写,它用一种将业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到Model层组件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。下图说明了各层之间的关系:
- View层:
负责界面的展示。
在原生的Android项目中,layout.xml里面的xml文件就对应于MVC的View层,里面都是一些view的布局代码。 - Controller层:
负责获取和处理用户的输入;
负责将用户输入传给负责业务逻辑层去做数据上的操作(如增删改查);
负责获取Model层对于数据操作的结果,并将其传给View层去做展示;
在原生的Android项目中,对应的就是各种activity组件。 - Model层:
负责业务逻辑处理,比如数据库存取操作,网络操作,复杂的算法,耗时的任务。
在原生的Android项目中,各种java bean,还有一些类似repository类就对应于model层。
将系统进行MVC分层的核心思路就是分离组件,降低组件耦合性,组件独立演化。
应用范例
比如,你的界面有一个按钮,按下这个按钮去网络上下载一个文件。这个按钮是view层的,是使用xml来写的,而那些和网络连接相关的代码写在其他类里,比如你可以写一个专门的networkFile类,这个就是model层,那怎么连接这两层呢?是通过button.setOnClickListener()这个函数,这个函数就写在了activity中,对应于controller层。
缺陷
在原生的Android项目中,xml作为view层,仅仅是静态的布局界面,你想去动态的改变一个页面的背景,或者动态的隐藏/显示一个按钮,或者显示提示下载进度使用的view组件progressDialog,这些都没办法在xml中做,只能把代码写在activity中,造成了activity既是controller层,又承担了view层的部分功能,两层之间产生了耦合。这样是非常不利于进行单元测试的。
MVP
概念
MVP是Model–View–Presenter的缩写,MVP与MVC的主要区别是在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中通过Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。模型与视图完全分离,我们可以修改视图而不影响模型,可以更高效地使用模型,因为所有的交互在Presenter里面,同时方便对各层进行单元测试。
在Android开发实践中,如何应用MVP呢?MVC中,Activity耦合了controller层和view层的问题如何在MVP中解决呢?
首先,我们知道面向接口的编程可以很好的解决各组件模块之间的耦合问题。对于view层和presenter层的通信,我们是可以通过接口实现的,具体的意思就是说我们的activity,fragment可以去实现实现定义好的接口,而在对应的presenter中通过接口调用方法。因此,有两种解决思路:
- 直接将Activity看作View,让它只承担View的责任。
- 将Activity看作一个MVP三者以外的一个controller,用于创建、连接View和Presenter。
在Google推出的官方MVP实例里,使用的就是第2种思路,它让每一个Activity都拥有一个Fragment来作为View,然后每个Activity也对应一个Presenter,在Activity里只处理与生命周期有关的内容,并跳出MVP之外,负责实例化Model,View,Presenter,并负责将三者合理的建立联系,承担的就是一个上帝视角。下面摘录了Google官方demo的关键类及接口,可以对照上面的解说理解其含义。
应用范例
1.创建接口BaseView,里面声明通用的接口方法:
package com.example.android.architecture.blueprints.todoapp;
public interface BaseView<T> {
void setPresenter(T presenter);
}
2.接口BasePresenter,里面声明通用的接口方法:
package com.example.android.architecture.blueprints.todoapp;
public interface BasePresenter {
void start();
}
3.创建复合接口StatisticsContract,方便根据需求扩展新的接口方法:
package com.example.android.architecture.blueprints.todoapp.statistics;
import com.example.android.architecture.blueprints.todoapp.BasePresenter;
import com.example.android.architecture.blueprints.todoapp.BaseView;
/**
* This specifies the contract between the view and the presenter.
*/
public interface StatisticsContract {
interface View extends BaseView<Presenter> {
void setProgressIndicator(boolean active);
void showStatistics(int numberOfIncompleteTasks, int numberOfCompletedTasks);
void showLoadingStatisticsError();
boolean isActive();
}
interface Presenter extends BasePresenter {
}
}
4.在Presenter层中,创建 StatisticsPresenter类同时必须实现接口StatisticsContract.Presenter:
import android.support.annotation.NonNull;
import com.example.android.architecture.blueprints.todoapp.data.Task;
import com.example.android.architecture.blueprints.todoapp.data.source.TasksDataSource;
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository;
import com.example.android.architecture.blueprints.todoapp.util.EspressoIdlingResource;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Listens to user actions from the UI ({@link StatisticsFragment}), retrieves the data and updates
* the UI as required.
*/
public class StatisticsPresenter implements StatisticsContract.Presenter {
private final TasksRepository mTasksRepository;
private final StatisticsContract.View mStatisticsView;
public StatisticsPresenter(@NonNull TasksRepository tasksRepository,
@NonNull StatisticsContract.View statisticsView) {
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null");
mStatisticsView = checkNotNull(statisticsView, "StatisticsView cannot be null!");
mStatisticsView.setPresenter(this);
}
@Override
public void start() {
loadStatistics();
}
private void loadStatistics() {
mStatisticsView.setProgressIndicator(true);
// The network request might be handled in a different thread so make sure Espresso knows
// that the app is busy until the response is handled.
EspressoIdlingResource.increment(); // App is busy until further notice
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
int activeTasks = 0;
int completedTasks = 0;
// This callback may be called twice, once for the cache and once for loading
// the data from the server API, so we check before decrementing, otherwise
// it throws "Counter has been corrupted!" exception.
if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
EspressoIdlingResource.decrement(); // Set app as idle.
}
// We calculate number of active and completed tasks
for (Task task : tasks) {
if (task.isCompleted()) {
completedTasks += 1;
} else {
activeTasks += 1;
}
}
// The view may not be able to handle UI updates anymore
if (!mStatisticsView.isActive()) {
return;
}
mStatisticsView.setProgressIndicator(false);
mStatisticsView.showStatistics(activeTasks, completedTasks);
}
@Override
public void onDataNotAvailable() {
// The view may not be able to handle UI updates anymore
if (!mStatisticsView.isActive()) {
return;
}
mStatisticsView.showLoadingStatisticsError();
}
});
}
}
5.在View层中,编写 类StatisticsFragment同时必须实现接口StatisticsContract.View:
/**
* Main UI for the statistics screen.
*/
public class StatisticsFragment extends Fragment implements StatisticsContract.View {
private TextView mStatisticsTV;
private StatisticsContract.Presenter mPresenter;
public static StatisticsFragment newInstance() {
return new StatisticsFragment();
}
@Override
public void setPresenter(@NonNull StatisticsContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.statistics_frag, container, false);
mStatisticsTV = (TextView) root.findViewById(R.id.statistics);
return root;
}
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}
@Override
public void setProgressIndicator(boolean active) {
if (active) {
mStatisticsTV.setText(getString(R.string.loading));
} else {
mStatisticsTV.setText("");
}
}
@Override
public void showStatistics(int numberOfIncompleteTasks, int numberOfCompletedTasks) {
if (numberOfCompletedTasks == 0 && numberOfIncompleteTasks == 0) {
mStatisticsTV.setText(getResources().getString(R.string.statistics_no_tasks));
} else {
String displayString = getResources().getString(R.string.statistics_active_tasks) + " "
+ numberOfIncompleteTasks + "n" + getResources().getString(
R.string.statistics_completed_tasks) + " " + numberOfCompletedTasks;
mStatisticsTV.setText(displayString);
}
}
@Override
public void showLoadingStatisticsError() {
mStatisticsTV.setText(getResources().getString(R.string.statistics_error));
}
@Override
public boolean isActive() {
return isAdded();
}
}
6.在Model层中,创建接口TasksDataSource:
package com.example.android.architecture.blueprints.todoapp.data.source;
import android.support.annotation.NonNull;
import com.example.android.architecture.blueprints.todoapp.data.Task;
import java.util.List;
public interface TasksDataSource {
interface LoadTasksCallback {
void onTasksLoaded(List<Task> tasks);
void onDataNotAvailable();
}
interface GetTaskCallback {
void onTaskLoaded(Task task);
void onDataNotAvailable();
}
void getTasks(@NonNull LoadTasksCallback callback);
void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
void saveTask(@NonNull Task task);
void completeTask(@NonNull Task task);
void completeTask(@NonNull String taskId);
void activateTask(@NonNull Task task);
void activateTask(@NonNull String taskId);
void clearCompletedTasks();
void refreshTasks();
void deleteAllTasks();
void deleteTask(@NonNull String taskId);
}
7.在Model层中,创建类TasksRepository实现接口TasksDataSource:
package com.example.android.architecture.blueprints.todoapp.data.source;
import static com.google.common.base.Preconditions.checkNotNull;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.example.android.architecture.blueprints.todoapp.data.Task;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Concrete implementation to load tasks from the data sources into a cache.
* <p>
* For simplicity, this implements a dumb synchronisation between locally persisted data and data
* obtained from the server, by using the remote data source only if the local database doesn't
* exist or is empty.
*/
public class TasksRepository implements TasksDataSource {
private static TasksRepository INSTANCE = null;
private final TasksDataSource mTasksRemoteDataSource;
private final TasksDataSource mTasksLocalDataSource;
/**
* This variable has package local visibility so it can be accessed from tests.
*/
Map<String, Task> mCachedTasks;
/**
* Marks the cache as invalid, to force an update the next time data is requested. This variable
* has package local visibility so it can be accessed from tests.
*/
boolean mCacheIsDirty = false;
// Prevent direct instantiation.
private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
@NonNull TasksDataSource tasksLocalDataSource) {
mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
}
/**
* Returns the single instance of this class, creating it if necessary.
*
* @param tasksRemoteDataSource the backend data source
* @param tasksLocalDataSource
the device storage data source
* @return the {@link TasksRepository} instance
*/
public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
TasksDataSource tasksLocalDataSource) {
if (INSTANCE == null) {
INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
}
return INSTANCE;
}
/**
* Used to force {@link #getInstance(TasksDataSource, TasksDataSource)} to create a new instance
* next time it's called.
*/
public static void destroyInstance() {
INSTANCE = null;
}
@Override
public void getTasks(@NonNull final LoadTasksCallback callback) {
checkNotNull(callback);
// Respond immediately with cache if available and not dirty
if (mCachedTasks != null && !mCacheIsDirty) {
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
return;
}
if (mCacheIsDirty) {
// If the cache is dirty we need to fetch new data from the network.
getTasksFromRemoteDataSource(callback);
} else {
// Query the local storage if available. If not, query the network.
mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
refreshCache(tasks);
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
@Override
public void onDataNotAvailable() {
getTasksFromRemoteDataSource(callback);
}
});
}
}
@Override
public void saveTask(@NonNull Task task) {
checkNotNull(task);
mTasksRemoteDataSource.saveTask(task);
mTasksLocalDataSource.saveTask(task);
// Do in memory cache update to keep the app UI up to date
if (mCachedTasks == null) {
mCachedTasks = new LinkedHashMap<>();
}
mCachedTasks.put(task.getId(), task);
}
@Override
public void completeTask(@NonNull Task task) {
checkNotNull(task);
mTasksRemoteDataSource.completeTask(task);
mTasksLocalDataSource.completeTask(task);
Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true);
// Do in memory cache update to keep the app UI up to date
if (mCachedTasks == null) {
mCachedTasks = new LinkedHashMap<>();
}
mCachedTasks.put(task.getId(), completedTask);
}
@Override
public void completeTask(@NonNull String taskId) {
checkNotNull(taskId);
completeTask(getTaskWithId(taskId));
}
@Override
public void activateTask(@NonNull Task task) {
checkNotNull(task);
mTasksRemoteDataSource.activateTask(task);
mTasksLocalDataSource.activateTask(task);
Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId());
// Do in memory cache update to keep the app UI up to date
if (mCachedTasks == null) {
mCachedTasks = new LinkedHashMap<>();
}
mCachedTasks.put(task.getId(), activeTask);
}
@Override
public void activateTask(@NonNull String taskId) {
checkNotNull(taskId);
activateTask(getTaskWithId(taskId));
}
@Override
public void clearCompletedTasks() {
mTasksRemoteDataSource.clearCompletedTasks();
mTasksLocalDataSource.clearCompletedTasks();
// Do in memory cache update to keep the app UI up to date
if (mCachedTasks == null) {
mCachedTasks = new LinkedHashMap<>();
}
Iterator<Map.Entry<String, Task>> it = mCachedTasks.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Task> entry = it.next();
if (entry.getValue().isCompleted()) {
it.remove();
}
}
}
@Override
public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
checkNotNull(taskId);
checkNotNull(callback);
Task cachedTask = getTaskWithId(taskId);
// Respond immediately with cache if available
if (cachedTask != null) {
callback.onTaskLoaded(cachedTask);
return;
}
// Load from server/persisted if needed.
// Is the task in the local data source? If not, query the network.
mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
@Override
public void onTaskLoaded(Task task) {
callback.onTaskLoaded(task);
}
@Override
public void onDataNotAvailable() {
mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
@Override
public void onTaskLoaded(Task task) {
callback.onTaskLoaded(task);
}
@Override
public void onDataNotAvailable() {
callback.onDataNotAvailable();
}
});
}
});
}
@Override
public void refreshTasks() {
mCacheIsDirty = true;
}
@Override
public void deleteAllTasks() {
mTasksRemoteDataSource.deleteAllTasks();
mTasksLocalDataSource.deleteAllTasks();
if (mCachedTasks == null) {
mCachedTasks = new LinkedHashMap<>();
}
mCachedTasks.clear();
}
@Override
public void deleteTask(@NonNull String taskId) {
mTasksRemoteDataSource.deleteTask(checkNotNull(taskId));
mTasksLocalDataSource.deleteTask(checkNotNull(taskId));
mCachedTasks.remove(taskId);
}
private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) {
mTasksRemoteDataSource.getTasks(new LoadTasksCallback() {
@Override
public void onTasksLoaded(List<Task> tasks) {
refreshCache(tasks);
refreshLocalDataSource(tasks);
callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
}
@Override
public void onDataNotAvailable() {
callback.onDataNotAvailable();
}
});
}
private void refreshCache(List<Task> tasks) {
if (mCachedTasks == null) {
mCachedTasks = new LinkedHashMap<>();
}
mCachedTasks.clear();
for (Task task : tasks) {
mCachedTasks.put(task.getId(), task);
}
mCacheIsDirty = false;
}
private void refreshLocalDataSource(List<Task> tasks) {
mTasksLocalDataSource.deleteAllTasks();
for (Task task : tasks) {
mTasksLocalDataSource.saveTask(task);
}
}
@Nullable
private Task getTaskWithId(@NonNull String id) {
checkNotNull(id);
if (mCachedTasks == null || mCachedTasks.isEmpty()) {
return null;
} else {
return mCachedTasks.get(id);
}
}
}
8.编写 StatisticsActivity.java文件,在里面实例化StatisticsFragment及StatisticsPresenter:
/**
* Show statistics for tasks.
*/
public class StatisticsActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.statistics_act);
// Set up the toolbar.
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
ab.setTitle(R.string.statistics_title);
ab.setHomeAsUpIndicator(R.drawable.ic_menu);
ab.setDisplayHomeAsUpEnabled(true);
// Set up the navigation drawer.
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerLayout.setStatusBarBackground(R.color.colorPrimaryDark);
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
if (navigationView != null) {
setupDrawerContent(navigationView);
}
StatisticsFragment statisticsFragment = (StatisticsFragment) getSupportFragmentManager()
.findFragmentById(R.id.contentFrame);
if (statisticsFragment == null) {
statisticsFragment = StatisticsFragment.newInstance();
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
statisticsFragment, R.id.contentFrame);
}
new StatisticsPresenter(
Injection.provideTasksRepository(getApplicationContext()), statisticsFragment);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// Open the navigation drawer when the home icon is selected from the toolbar.
mDrawerLayout.openDrawer(GravityCompat.START);
return true;
}
return super.onOptionsItemSelected(item);
}
private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.list_navigation_menu_item:
Intent intent =
new Intent(StatisticsActivity.this, TasksActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
break;
case R.id.statistics_navigation_menu_item:
// Do nothing, we're already on that screen
break;
default:
break;
}
// Close the navigation drawer when an item is selected.
menuItem.setChecked(true);
mDrawerLayout.closeDrawers();
return true;
}
});
}
}
缺陷
MVP的问题在于,由于我们使用了接口的方式去连接view层和presenter层,这样就导致了一个问题,如果你的项目中有一个逻辑很复杂的页面,你的接口会有很多,十几二十个都不足为奇。想象一个app中有很多个这样复杂的页面,维护接口的成本就会非常的大。
这个问题的解决方案就是你得根据自己的业务逻辑去斟酌着写接口。你可以定义一些基类接口,把一些公共的逻辑,比如网络请求成功失败,toast等等放在里面,之后你再定义新的接口的时候可以继承自那些基类型接口,这样会好不少。
另外,Presenter有大量的View->Model,Model->View的数据手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。这就促成了下面要讲的MVVM+Data Binding的出现。
MVVM
概念
MVVM模式包含了三个部分:
Model层:代表你的基本业务逻辑;
View 层:显示内容;
ViewModel 层:将前面两者联系在一起的对象;
一个ViewModel接口提供了两个东西:动作和数据。动作改变Model的下层(click listener,监听文字改变的listener等等),而数据则是Model的内容。
比如,为一个拍卖页面服务的ViewModel可能被作为数据呈现:item里的一张图片,标题,描述,价格。也可能作为动作呈现:投标,购买或者联系卖家。
在传统的安卓架构中,controller负责推送数据给view。在Activity中findview,然后在它上面设置内容。
在MVVM中,ViewModel在改变内容之后通知binding framework内容发生了改变。然后binding framework自动更新和那些内容绑定的view。这两个组件只是通过ViewModel松耦合在一起。
这种架构模式之所以牛逼,除了明显智能化了的View之外,还方便了测试。
因为ViewModel不在依赖于View了,你可以在没有View的情况下也能测试ViewModel。在合适的依赖注入的帮助下,测试就会变得非常简单。
例如,在一个测试用例中,我们不将VM和一个真实的View绑定,而是直接创建一个VM,给它一些数据,然后在上面调用操作,以确保数据的变化是正确的。比如刚刚的拍卖案例中,你可以创建一个带有模拟的API服务的VM,让它载入任意的item然后在上面调用拍卖的行为,以确认结果数据是正确的。所有这些工作都不必和一个具体的view交互。而在测试view的时候,我们可以创建一系列带有预先设置好数据(比如依赖于网络调用的或者数据库内容的)的模拟model,然后观察在不同的数据集合面前view是如何表现的。
(关于MVVM的单元测试方法,会在后面的文章单独讨论。)
应用范例
google提出了Data Binding 用于实现MVVM,其中ViewModel可以理解成是View的数据模型和Presenter的合体,ViewModel和View之间的交互通过 Data Binding完成,而Data Binding可以实现双向的交互,这就使得视图和控制层之间的耦合程度进一步降低,关注点分离更为彻底,同时减轻了Activity的压力。
下面我们看一个用Data Binding 实现MVVM的例子。
我们假设,我们要做一个标签应用,这个应用非常简单,就是一个recycler view,它其中的项只有各种label。
1.首先配置环境创建工程,必须使用使用的android studio 1.3以上的版本,如果要使用data binding的话在build.gradle里面添加:
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
dataBinding {
enabled = true
}
defaultConfig {
applicationId "com.chan.databindingdemo"
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dataBinding {
enable = true
}
2.在Model层,自定义一个类:
public class DemoItem {
private String m_label;
public DemoItem(String label) {
m_label = label;
public String getLabel() {
return m_label;
}
public void setLabel(String label) {
m_label = label;
}
}
代码非常简单,就一个label field,和一些getter & setter
3.在View层,对于recycler中的子项,显然是要自定义布局,比如:
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="data" type="com.chan.databindingdemo.DemoItem"/>
</data>
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{data.label}"/>
</LinearLayout>
</layout>
可以看到,这个layout的xml文件根节点是layout 并且,还定义了名为data类型为com.chan.databindingdemo.DemoItem的数据。它有一个data节点包裹,然后以variable的属性给出。
往下阅读便是RecyclerView的内容视图了,很简单 就一个TextView,可以看到对于TextView的android:text属性,它的值@{data.label}。也就是说这个 text view的内容由data的label属性给出。这时候,编译器便生成了一个ViewDataBinding类,这个类所在的包名是以 我们的工程包+binding决定的,比如现在的应用包名是com.chan.databindingdemo,那么这个编译器生成的代码便在 com.chan.databindingdemo.databinding下,对于这个包下面的类的名字呢是以上文的Layout文件名决定的,比如:我的 layout文件名是item_layout.xml,那么生成的类就为ItemLayoutBinding。
注意:ViewModel层的类由编译器自动生成的,本例中就是上面提到的生成类ItemLayoutBinding.
RecyclerView的使用方法乃基础内容,本文并不对基础的内容做讲解,不熟悉的可自行学习。
4.在View层,还要定义view holder:
/**
* Created by chan on 16/3/12.
*/public class RecyclerHolder extends RecyclerView.ViewHolder {
ItemLayoutBinding m_itemLayoutBinding;
public RecyclerHolder(View itemView, ViewDataBinding viewDataBinding) {
super(itemView);
m_itemLayoutBinding = (ItemLayoutBinding) viewDataBinding;
}
public void bind(DemoItem demoItem) {
m_itemLayoutBinding.setData(demoItem);
}
}
RecyclerHolder的构造函数多了一个viewDataBinding,并且这个类型的真实类型为ItemLayouBinding,就是刚刚编译器自动生成的那个类。一个ViewModel要和view进行 绑定,需要调用binding的set方法,具体的set方法名根据上文的layout文件的variable的name属性决定,所以我们这里是 m_itemLayoutBinding.setData(demoItem);
5.在View层,实现adapter:
/**
* Created by chan on 16/3/12.
*/public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerHolder> {
private List<DemoItem> m_demoItems;
public RecyclerAdapter(Context context, List<DemoItem> demoItems) {
m_layoutInflater = LayoutInflater.from(context);
m_demoItems = demoItems;
}
@Override
public RecyclerHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding viewDataBinding = DataBindingUtil.inflate(m_layoutInflater, R.layout.item_layout, parent, false);
return new RecyclerHolder(viewDataBinding.getRoot(), viewDataBinding);
}
@Override
public void onBindViewHolder(RecyclerHolder holder, int position) {
final int currentPosition = holder.getLayoutPosition();
holder.bind(m_demoItems.get(currentPosition));
}
@Override
public int getItemCount() {
return m_demoItems.size();
}
}
值得注意的是,如果我们要inflate视图,必须要通过DataBindingUtil来实现!!否则会抛出一个非捕获异常!!
也就是这里的:
ViewDataBinding viewDataBinding = DataBindingUtil.inflate(m_layoutInflater, R.layout.item_layout, parent, false);
到此,一个简单的demo出现了 ,不过我们刚刚讲过,如果数据改变了,view能自动刷新我们可以引入观察者模式。
还记得刚刚的model吗?我们只需要一个简单的修改就可以完成了。
/**
* Created by chan on 16/3/12.
*/public class DemoItem extends BaseObservable {
private String m_label;
public DemoItem(String label) {
m_label = label;
}
@Bindable
public String getLabel() {
return m_label;
}
public void setLabel(String label) {
m_label = label;
notifyPropertyChanged(com.chan.databindingdemo.BR.label);
}
}
如果一个应用要实现可观察,只要实现 Observable这个接口就好了,原文:A class implementing the Observable interface will allow the binding to attach a single listener to a bound object to listen for changes of all properties on that object.
不过库给我们实现了一个简单的对象BaseObservable,它可以满足我们的简单需求。
我们在getter上加了一个bindable注解,并且在setter里面,我们手动提醒了变化
有一些谜团,比如 com.chan.databindingdemo.BR.label是啥,我引入一下原文:
The Bindable annotation generates an entry in the BR class file during compilation. The BR class file will be generated in the module package. If the base class for data classes cannot be changed, the Observable interface may be implemented using the convenient PropertyChangeRegistry to store and notify listeners efficiently.
在编译时期,bindable注解会在BR里面生成一个项,这个项就是针对上文的getter。并且这个br类生成在项目的包下。
6.在View层中,activity完整的代码:
public class MainActivity extends AppCompatActivity {
private RecyclerView m_recyclerView;
private List<DemoItem> m_demoItems = new ArrayList<>();
private int m_index = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
m_recyclerView = (RecyclerView) findViewById(R.id.id_recycler);
m_recyclerView.setLayoutManager(new LinearLayoutManager(this));
for(int i = 0; i < 10; ++i) {
m_demoItems.add(new DemoItem("标签" + i));
}
RecyclerAdapter adapter = new RecyclerAdapter(this, m_demoItems);
m_recyclerView.setAdapter(adapter);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
m_demoItems.get(m_index).setLabel(System.currentTimeMillis() + "");
m_index = (m_index + 1) % m_demoItems.size();
}
});
}
@Override
// Inflate the menu; this adds items to the action bar if it is present.
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
总结
- 通过代码可以看出,明显可以发现MVP模式下,增加了大量的层间接口,代码量相对来说确实增加了很多,但是逻辑相对的更加清晰。MVP架构不是很适合小型的项目,小型项目使用MVC架构即可,但是如果是一个较大型的项目还是可以选用MVP架构来做开发,毕竟逻辑清晰,,方便进行单元测试,维护起来也比较方便。
- MVVM架构是Android往后发展的趋势,毕竟谷歌都推出了Datebinding,而且绑定的这种机制也确实带来了model,view与vm的分离,从逻辑上看也确实清晰了很多,问题是暂时只支持单向绑定,这个也要等谷歌后面的更新完善,而且代码的阅读性会下降很多,比较适合小型的项目,暂时不适合大型的项目。
参考资料
MVVM示例代码参考:http://www.wtoutiao.com/p/1edxr8q.html
部分论述参考:
http://www.androidchina.net/4800.html
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0602/2973.html
最后
以上就是寒冷微笑为你收集整理的MVC、MVP、MVVM深入理解与使用的全部内容,希望文章能够帮你解决MVC、MVP、MVVM深入理解与使用所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复