Todo-mvvm-databinding
- Model: 内存,本地,网络数据的实现与获取
- View: 对应Activity,Fragment,负责View的绘制,与用户交互,在xml中编写databinding对model层数据的引用绑定,
ViewModelHolder<VH>
作为非UI的Fragment持有ViewModel,并绑定到Activity的生命周期 - ViewModel: databinding框架绑定ViewModel,ViewModel持有View中引用的变量observable fields,以及数据管理类TasksRepository,数据或属性发生变化时,databinding自动更新UI
优缺点
- 低耦合,ViewModel的逻辑代码可重用性,解决MVP中,View,Presenter相互持有问题,响应界面操作无需由View传递,数据变换也无需Presenter调用View实现,开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。可测试,界面素来是比较难于测试的,而现在测试可以针对ViewModel来写
- ViewModel存在Model的依赖,databinding不方便调试
代码分析
TasksActivity,绑定TasksFragment以及ViewModelHolder(持有ViewModel的非UI的Fragment,绑定到Activity的生命周期) 获取TasksViewModel,并绑定到Activity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private TasksViewModel findOrCreateViewModel() {
@SuppressWarnings("unchecked")
ViewModelHolder<TasksViewModel> retainedViewModel =
(ViewModelHolder<TasksViewModel>) getSupportFragmentManager()
.findFragmentByTag(TASKS_VIEWMODEL_TAG);
if (retainedViewModel != null && retainedViewModel.getViewmodel() != null) {
// If the model was retained, return it.
return retainedViewModel.getViewmodel();
} else {
// There is no ViewModel yet, create it.
TasksViewModel viewModel = new TasksViewModel(
Injection.provideTasksRepository(getApplicationContext()),
getApplicationContext());
// and bind it to this Activity's lifecycle using the Fragment Manager.
ActivityUtils.addFragmentToActivity(
getSupportFragmentManager(),
ViewModelHolder.createContainer(viewModel),
TASKS_VIEWMODEL_TAG);
return viewModel;
}
}绑定Fragment和View,设置Activity的跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public interface TasksNavigator {
void addNewTask();
}
mViewModel = findOrCreateViewModel();
mViewModel.setNavigator(this);
// Link View and ViewModel
tasksFragment.setViewModel(mViewModel);
TasksViewModel
void setNavigator(TasksNavigator navigator) {
mNavigator = navigator;
}在
onDestory()
回收设置的跳转接口1
2
3
4
5
6
7
8
9
10
11@Override
protected void onDestroy() {
mViewModel.onActivityDestroyed();
super.onDestroy();
}
TasksViewModel
void onActivityDestroyed() {
// Clear references to avoid potential memory leaks.
mNavigator = null;
}TasksFragment,持有TasksViewModel,TasksFragBinding,TasksAdapter中持有TaskItemViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22@Override
public void onResume() {
super.onResume();
mTasksViewModel.start();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mTasksFragBinding = TasksFragBinding.inflate(inflater, container, false);
mTasksFragBinding.setView(this);
mTasksFragBinding.setViewmodel(mTasksViewModel);
setHasOptionsMenu(true);
View root = mTasksFragBinding.getRoot();
return root;
}设置SnackBar text属性的监听回调,属性改变时显示SnackBar,在onDestory()中移除回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18private void setupSnackbar() {
mSnackbarCallback = new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
SnackbarUtils.showSnackbar(getView(), mTasksViewModel.getSnackbarText());
}
};
mTasksViewModel.snackbarText.addOnPropertyChangedCallback(mSnackbarCallback);
}
@Override
public void onDestroy() {
mListAdapter.onDestroy();
if (mSnackbarCallback != null) {
mTasksViewModel.snackbarText.removeOnPropertyChangedCallback(mSnackbarCallback);
}
super.onDestroy();
}单个任务的抽象类TaskViewModel,暴露Task的观察者,设置标题,描述,Task,以及SnackBarText
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
125public abstract class TaskViewModel extends BaseObservable
implements TasksDataSource.GetTaskCallback {
public final ObservableField<String> snackbarText = new ObservableField<>();
public final ObservableField<String> title = new ObservableField<>();
public final ObservableField<String> description = new ObservableField<>();
private final ObservableField<Task> mTaskObservable = new ObservableField<>();
private final TasksRepository mTasksRepository;
private final Context mContext;
private boolean mIsDataLoading;
public TaskViewModel(Context context, TasksRepository tasksRepository) {
mContext = context.getApplicationContext(); // Force use of Application Context.
mTasksRepository = tasksRepository;
// Exposed observables depend on the mTaskObservable observable:
mTaskObservable.addOnPropertyChangedCallback(new OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
Task task = mTaskObservable.get();
if (task != null) {
title.set(task.getTitle());
description.set(task.getDescription());
} else {
title.set(mContext.getString(R.string.no_data));
description.set(mContext.getString(R.string.no_data_description));
}
}
});
}
public void start(String taskId) {
if (taskId != null) {
mIsDataLoading = true;
mTasksRepository.getTask(taskId, this);
}
}
public void setTask(Task task) {
mTaskObservable.set(task);
}
// "completed" is two-way bound, so in order to intercept the new value, use a @Bindable
// annotation and process it in the setter.
@Bindable
public boolean getCompleted() {
Task task = mTaskObservable.get();
return task != null && task.isCompleted();
}
public void setCompleted(boolean completed) {
if (mIsDataLoading) {
return;
}
Task task = mTaskObservable.get();
// Notify repository and user
if (completed) {
mTasksRepository.completeTask(task);
snackbarText.set(mContext.getResources().getString(R.string.task_marked_complete));
} else {
mTasksRepository.activateTask(task);
snackbarText.set(mContext.getResources().getString(R.string.task_marked_active));
}
}
@Bindable
public boolean isDataAvailable() {
return mTaskObservable.get() != null;
}
@Bindable
public boolean isDataLoading() {
return mIsDataLoading;
}
// This could be an observable, but we save a call to Task.getTitleForList() if not needed.
@Bindable
public String getTitleForList() {
if (mTaskObservable.get() == null) {
return "No data";
}
return mTaskObservable.get().getTitleForList();
}
@Override
public void onTaskLoaded(Task task) {
mTaskObservable.set(task);
mIsDataLoading = false;
notifyChange(); // For the @Bindable properties
}
@Override
public void onDataNotAvailable() {
mTaskObservable.set(null);
mIsDataLoading = false;
}
public void deleteTask() {
if (mTaskObservable.get() != null) {
mTasksRepository.deleteTask(mTaskObservable.get().getId());
}
}
public void onRefresh() {
if (mTaskObservable.get() != null) {
start(mTaskObservable.get().getId());
}
}
public String getSnackbarText() {
return snackbarText.get();
}
@Nullable
protected String getTaskId() {
return mTaskObservable.get().getId();
}
}TasksItemViewModel,持有Navigator的弱引用,在task_item.xml中,绑定点击事件
android:onClick="@{() -> viewmodel.taskClicked()}"
,以及Task激活状态android:checked="@={viewmodel.completed}"
,标题android:text="@{viewmodel.titleForList}"
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
29public class TaskItemViewModel extends TaskViewModel {
// This navigator is s wrapped in a WeakReference to avoid leaks because it has references to an
// activity. There's no straightforward way to clear it for each item in a list adapter.
@Nullable
private WeakReference<TaskItemNavigator> mNavigator;
public TaskItemViewModel(Context context, TasksRepository tasksRepository) {
super(context, tasksRepository);
}
public void setNavigator(TaskItemNavigator navigator) {
mNavigator = new WeakReference<>(navigator);
}
/**
* Called by the Data Binding library when the row is clicked.
*/
public void taskClicked() {
String taskId = getTaskId();
if (taskId == null) {
// Click happened before task was loaded, no-op.
return;
}
if (mNavigator != null && mNavigator.get() != null) {
mNavigator.get().openTaskDetails(taskId);
}
}
}TasksViewModel,主要是任务列表的数据加载,UI更新
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
36public class TasksViewModel extends BaseObservable {
// These observable fields will update Views automatically
public final ObservableList<Task> items = new ObservableArrayList<>();
public final ObservableBoolean dataLoading = new ObservableBoolean(false);
public final ObservableField<String> currentFilteringLabel = new ObservableField<>();
public final ObservableField<String> noTasksLabel = new ObservableField<>();
public final ObservableField<Drawable> noTaskIconRes = new ObservableField<>();
public final ObservableBoolean tasksAddViewVisible = new ObservableBoolean();
final ObservableField<String> snackbarText = new ObservableField<>();
private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS;
private final TasksRepository mTasksRepository;
private final ObservableBoolean mIsDataLoadingError = new ObservableBoolean(false);
private Context mContext; // To avoid leaks, this must be an Application Context.
private TasksNavigator mNavigator;
public TasksViewModel(
TasksRepository repository,
Context context) {
mContext = context.getApplicationContext(); // Force use of Application Context.
mTasksRepository = repository;
// Set initial state
setFiltering(TasksFilterType.ALL_TASKS);
}tasks_frag.xml中绑定TasksFragment,TasksViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13<data>
<import type="android.view.View" />
<variable
name="view"
type="com.example.android.architecture.blueprints.todoapp.tasks.TasksFragment" />
<variable
name="viewmodel"
type="com.example.android.architecture.blueprints.todoapp.tasks.TasksViewModel" />
</data>自定义设置可滑动子View
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25ublic class ScrollChildSwipeRefreshLayout extends SwipeRefreshLayout {
private View mScrollUpChild;
public ScrollChildSwipeRefreshLayout(Context context) {
super(context);
}
public ScrollChildSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean canChildScrollUp() {
if (mScrollUpChild != null) {
return mScrollUpChild.canScrollVertically(-1);
// return ViewCompat.canScrollVertically(mScrollUpChild, -1);
}
return super.canChildScrollUp();
}
public void setScrollUpChild(View view) {
mScrollUpChild = view;
}
}ScrollChildSwipeRefreshLayout刷新时加载方法
android:onRefresh="@{viewmodel}"
,自定义setter1
2
3
4
5
6
7
8
9
10
11
12
13public class SwipeRefreshLayoutDataBinding {
@BindingAdapter("android:onRefresh")
public static void setSwipeRefreshLayoutOnRefreshListener(ScrollChildSwipeRefreshLayout view,
final TasksViewModel viewModel) {
view.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
viewModel.loadTasks(true);
}
});
}
}app:items="@{viewmodel.items}"
替换ListView的items1
2
3
4
5
6
7
8
9
10
11public class TasksListBindings {
@SuppressWarnings("unchecked")
@BindingAdapter("app:items")
public static void setItems(ListView listView, List<Task> items) {
TasksFragment.TasksAdapter adapter = (TasksFragment.TasksAdapter) listView.getAdapter();
if (adapter != null)
{
adapter.replaceData(items);
}
}
}TasksAapter,TasksItemBinding绑定ViewModel,ListView优化策略达到重复利用Binding,在item的snackbarText发生改变时,改变TasksViewModel的snackbarText,然后显示SnackBar的提示信息
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
86public static class TasksAdapter extends BaseAdapter {
@Nullable private TaskItemNavigator mTaskItemNavigator;
private final TasksViewModel mTasksViewModel;
private List<Task> mTasks;
private TasksRepository mTasksRepository;
public TasksAdapter(List<Task> tasks, TasksActivity taskItemNavigator,
TasksRepository tasksRepository,
TasksViewModel tasksViewModel) {
mTaskItemNavigator = taskItemNavigator;
mTasksRepository = tasksRepository;
mTasksViewModel = tasksViewModel;
setList(tasks);
}
public void onDestroy() {
mTaskItemNavigator = null;
}
public void replaceData(List<Task> tasks) {
setList(tasks);
}
@Override
public int getCount() {
return mTasks != null ? mTasks.size() : 0;
}
@Override
public Task getItem(int i) {
return mTasks.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
Task task = getItem(i);
TaskItemBinding binding;
if (view == null) {
// Inflate
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
// Create the binding
binding = TaskItemBinding.inflate(inflater, viewGroup, false);
} else {
// Recycling view
binding = DataBindingUtil.getBinding(view);
}
final TaskItemViewModel viewmodel = new TaskItemViewModel(
viewGroup.getContext().getApplicationContext(),
mTasksRepository
);
viewmodel.setNavigator(mTaskItemNavigator);
binding.setViewmodel(viewmodel);
// To save on PropertyChangedCallbacks, wire the item's snackbar text observable to the
// fragment's.
viewmodel.snackbarText.addOnPropertyChangedCallback(
new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
mTasksViewModel.snackbarText.set(viewmodel.getSnackbarText());
}
});
viewmodel.setTask(task);
return binding.getRoot();
}
private void setList(List<Task> tasks) {
mTasks = tasks;
notifyDataSetChanged();
}
}