如何使用带有片段的 Android MVVM 模式?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/22267099/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to use Android MVVM pattern with fragments?
提问by R. Campos
First I request apologizes about my not to good English.
首先,我请求为我的英语不好而道歉。
I have developed a lot of years Java SE software, and I used to use the MVC design pattern. Now I develop android apps, and I'm not happy with the argument that says that android already uses an MVC pattern, with the xml files acting as the view.
我已经开发了很多年的 Java SE 软件,我曾经使用过 MVC 设计模式。现在我开发 android 应用程序,我对 android 已经使用 MVC 模式的论点不满意,xml 文件充当视图。
I did a lot of research on the web, but it seems that there is not unanimity about this topic. Some use the MVC pattern, others the MVP pattern, but I'm my opinion, there is no unanimity.
我在网上做了很多研究,但似乎对这个话题并没有达成一致。有些使用 MVC 模式,有些使用 MVP 模式,但我认为,没有一致意见。
Recently I bought a book (Android Best Practices, from Godfrey Nolan, Onur Cinar and David Truxall), and in the chapter two, you can find the MVC, the MVVM and the Dependency Injection patterns explained. After trying all of them, I think that for my apps and my work mode the best is the MVVM pattern.
最近我买了一本书(Android Best Practices,来自 Godfrey Nolan、Onur Cinar 和 David Truxall),在第二章中,你可以找到 MVC、MVVM 和依赖注入模式的解释。在尝试了所有这些之后,我认为对于我的应用程序和我的工作模式来说,最好的是 MVVM 模式。
I find this pattern very easy to use when programming with activities, but I'm confused about how to use it when programming with fragments. I will reproduce the example of the MVVM pattern applied to simple "todo app", downloaded from the website of the "Android Best Practices" book.
我发现这种模式在用活动编程时很容易使用,但我对如何在用片段编程时使用它感到困惑。我将重现 MVVM 模式应用于简单“todo 应用程序”的示例,该示例从“Android 最佳实践”一书的网站上下载。
The View (activity)
视图(活动)
package com.example.mvvm;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
public class TodoActivity extends Activity
{
public static final String APP_TAG = "com.logicdrop.todos";
private ListView taskView;
private Button btNewTask;
private EditText etNewTask;
private TaskListManager delegate;
/*The View handles UI setup only. All event logic and delegation
*is handled by the ViewModel.
*/
public static interface TaskListManager
{
//Through this interface the event logic is
//passed off to the ViewModel.
void registerTaskList(ListView list);
void registerTaskAdder(View button, EditText input);
}
@Override
protected void onStop()
{
super.onStop();
}
@Override
protected void onStart()
{
super.onStart();
}
@Override
public void onCreate(final Bundle bundle)
{
super.onCreate(bundle);
this.setContentView(R.layout.main);
this.delegate = new TodoViewModel(this);
this.taskView = (ListView) this.findViewById(R.id.tasklist);
this.btNewTask = (Button) this.findViewById(R.id.btNewTask);
this.etNewTask = (EditText) this.findViewById(R.id.etNewTask);
this.delegate.registerTaskList(taskView);
this.delegate.registerTaskAdder(btNewTask, etNewTask);
}
}
The Model
该模型
package com.example.mvvm;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
final class TodoModel
{
//The Model should contain no logic specific to the view - only
//logic necessary to provide a minimal API to the ViewModel.
private static final String DB_NAME = "tasks";
private static final String TABLE_NAME = "tasks";
private static final int DB_VERSION = 1;
private static final String DB_CREATE_QUERY = "CREATE TABLE " + TodoModel.TABLE_NAME + " (id integer primary key autoincrement, title text not null);";
private final SQLiteDatabase storage;
private final SQLiteOpenHelper helper;
public TodoModel(final Context ctx)
{
this.helper = new SQLiteOpenHelper(ctx, TodoModel.DB_NAME, null, TodoModel.DB_VERSION)
{
@Override
public void onCreate(final SQLiteDatabase db)
{
db.execSQL(TodoModel.DB_CREATE_QUERY);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
final int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS " + TodoModel.TABLE_NAME);
this.onCreate(db);
}
};
this.storage = this.helper.getWritableDatabase();
}
/*Overrides are now done in the ViewModel. The Model only needs
*to add/delete, and the ViewModel can handle the specific needs of the View.
*/
public void addEntry(ContentValues data)
{
this.storage.insert(TodoModel.TABLE_NAME, null, data);
}
public void deleteEntry(final String field_params)
{
this.storage.delete(TodoModel.TABLE_NAME, field_params, null);
}
public Cursor findAll()
{
//Model only needs to return an accessor. The ViewModel will handle
//any logic accordingly.
return this.storage.query(TodoModel.TABLE_NAME, new String[]
{ "title" }, null, null, null, null, null);
}
}
The ViewModel
视图模型
package com.example.mvvm;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class TodoViewModel implements TodoActivity.TaskListManager
{
/*The ViewModel acts as a delegate between the ToDoActivity (View)
*and the ToDoProvider (Model).
* The ViewModel receives references from the View and uses them
* to update the UI.
*/
private TodoModel db_model;
private List<String> tasks;
private Context main_activity;
private ListView taskView;
private EditText newTask;
public TodoViewModel(Context app_context)
{
tasks = new ArrayList<String>();
main_activity = app_context;
db_model = new TodoModel(app_context);
}
//Overrides to handle View specifics and keep Model straightforward.
private void deleteTask(View view)
{
db_model.deleteEntry("title='" + ((TextView)view).getText().toString() + "'");
}
private void addTask(View view)
{
final ContentValues data = new ContentValues();
data.put("title", ((TextView)view).getText().toString());
db_model.addEntry(data);
}
private void deleteAll()
{
db_model.deleteEntry(null);
}
private List<String> getTasks()
{
final Cursor c = db_model.findAll();
tasks.clear();
if (c != null)
{
c.moveToFirst();
while (c.isAfterLast() == false)
{
tasks.add(c.getString(0));
c.moveToNext();
}
c.close();
}
return tasks;
}
private void renderTodos()
{
//The ViewModel handles rendering and changes to the view's
//data. The View simply provides a reference to its
//elements.
taskView.setAdapter(new ArrayAdapter<String>(main_activity,
android.R.layout.simple_list_item_1,
getTasks().toArray(new String[]
{})));
}
public void registerTaskList(ListView list)
{
this.taskView = list; //Keep reference for rendering later
if (list.getAdapter() == null) //Show items at startup
{
renderTodos();
}
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id)
{ //Tapping on any item in the list will delete that item from the database and re-render the list
deleteTask(view);
renderTodos();
}
});
}
public void registerTaskAdder(View button, EditText input)
{
this.newTask = input;
button.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(final View view)
{ //Add task to database, re-render list, and clear the input
addTask(newTask);
renderTodos();
newTask.setText("");
}
});
}
}
The problem is that when I try to reproduce this pattern when using fragments, I'm no sure how to proceed. May I have a view model and a model for each fragment or only for the activity that contains those fragments?
问题是,当我尝试在使用片段时重现此模式时,我不确定如何继续。我可以为每个片段创建一个视图模型和一个模型,还是只为包含这些片段的活动创建一个模型?
With the classic approach to fragment (the fragment is a inner class inside the activity), it is easy to interact with the activity, or to access the fragment manager to do changes, but if I decouple the code, and put the logic of my program outside the activity, I have seen that I need very often references to the activity in my ViewModel (not references to the views of the activity, but references to the activity itself).
用经典的fragment方式(fragment是activity内部的一个内部类),很容易和activity交互,或者访问fragment manager做修改,但是如果我把代码解耦,把我的逻辑放在在活动之外的程序中,我已经看到我经常需要引用我的 ViewModel 中的活动(不是对活动视图的引用,而是对活动本身的引用)。
Or for example, imagine that that the activity with fragments, is working with data received from an intent, not from a model (database or rest service). Then, I feel that I don't need a model. Maybe I can create the model when I receive the intent in the activity, but I feel that this is not correct (the view should not have relation with the model, only the viewmodel...).
或者例如,假设带有片段的活动正在处理从意图接收的数据,而不是从模型(数据库或休息服务)接收的数据。然后,我觉得我不需要模型。也许我可以在活动中收到意图时创建模型,但我觉得这不正确(视图应该与模型没有关系,只有视图模型......)。
May anybody offer me an explanation about how to use the MVVM pattern with android when using fragments?
有人可以向我解释一下在使用片段时如何将 MVVM 模式与 android 一起使用吗?
Thanks in advance.
提前致谢。
采纳答案by Entreco
NOTE: The following is outdated, and I would not recommend it anymore. Mainly because it's difficult to test the Viewsmodel in this setup. Have a look at Google Architecture Blueprints.
注意:以下内容已过时,我不再推荐它。主要是因为在这种设置中很难测试 Viewsmodel。看看谷歌架构蓝图。
Old Answer:
旧答案:
Personally, I prefer an alternate setup:
就个人而言,我更喜欢另一种设置:
The Model
该模型
Your model. Doesn't need to be changed (beauty of using MVVM :) )
你的模型。不需要更改(使用 MVVM 的美感 :) )
The View (fragment)
视图(片段)
Slightly different. The View (Fragment) has a reference to the ViewModel ( Activity ) in my setup. Instead of initializing your delegate like:
稍微不一样。View (Fragment) 在我的设置中有对 ViewModel ( Activity ) 的引用。而不是像这样初始化您的委托:
// Old way -> I don't like it
this.delegate = new TodoViewModel(this);
I suggest you use a well-known Android pattern:
我建议你使用一个众所周知的 Android 模式:
@Override
public void onAttach(final Activity activity) {
super.onAttach(activity);
try {
delegate = (ITaskListManager) activity;
} catch (ClassCastException ignore) {
throw new IllegalStateException("Activity " + activity + " must implement ITaskListManager");
}
}
@Override
public void onDetach() {
delegate = sDummyDelegate;
super.onDetach();
}
This way, your View (Fragment) enforces that the Activity to which it is attached, implemets the ITaskListManager interface. When the Fragment is detached from the Activity, some default implementation is set as the delegate. This prevents getting errors when you have an instance of a fragment which is not attached to an Activity (yes, this can happen).
这样,您的视图(片段)强制它所附加到的 Activity 实现 ITaskListManager 接口。当 Fragment 从 Activity 分离时,一些默认实现被设置为委托。当您有一个未附加到 Activity 的片段实例时,这可以防止出现错误(是的,这可能发生)。
Here's the complete code for my ViewFragment:
这是我的 ViewFragment 的完整代码:
public class ViewFragment extends Fragment {
private ListView taskView;
private Button btNewTask;
private EditText etNewTask;
private ITaskListManager delegate;
/**
* Dummy delegate to avoid nullpointers when
* the fragment is not attached to an activity
*/
private final ITaskListManager sDummyDelegate = new ITaskListManager() {
@Override
public void registerTaskList(final ListView list) {
}
@Override
public void registerTaskAdder(final View button, final EditText input) {
}
};
/*
* The View handles UI setup only. All event logic and delegation
* is handled by the ViewModel.
*/
public static interface ITaskListManager {
// Through this interface the event logic is
// passed off to the ViewModel.
void registerTaskList(ListView list);
void registerTaskAdder(View button, EditText input);
}
@Override
public void onAttach(final Activity activity) {
super.onAttach(activity);
try {
delegate = (ITaskListManager) activity;
} catch (ClassCastException ignore) {
throw new IllegalStateException("Activity " + activity + " must implement ITaskListManager");
}
}
@Override
public void onDetach() {
delegate = sDummyDelegate;
super.onDetach();
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.activity_view_model, container, false);
taskView = (ListView) view.findViewById(R.id.tasklist);
btNewTask = (Button) view.findViewById(R.id.btNewTask);
etNewTask = (EditText) view.findViewById(R.id.etNewTask);
delegate.registerTaskList(taskView);
delegate.registerTaskAdder(btNewTask, etNewTask);
return view;
}
}
The ViewModel (activity)
ViewModel(活动)
Using an Activity as your ViewModel is almost the same. Instead, you only need to make sure that you create the Model here, and that you add your View (Fragment) to the activity...
使用 Activity 作为您的 ViewModel 几乎相同。相反,您只需要确保在此处创建模型,并将您的视图(片段)添加到活动...
public class ViewModelActivity extends ActionBarActivity implements ITaskListManager {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_model);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction().add(R.id.container, new ViewFragment()).commit();
}
initViewModel();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.view_model, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final 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();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
private Model db_model;
private List<String> tasks;
private ListView taskView;
private EditText newTask;
/**
* Initialize the ViewModel
*/
private void initViewModel() {
tasks = new ArrayList<String>();
db_model = new Model(this);
}
private void deleteTask(final View view) {
db_model.deleteEntry("title='" + ((TextView) view).getText().toString() + "'");
}
private void addTask(final View view) {
final ContentValues data = new ContentValues();
data.put("title", ((TextView) view).getText().toString());
db_model.addEntry(data);
}
private void deleteAll() {
db_model.deleteEntry(null);
}
private List<String> getTasks() {
final Cursor c = db_model.findAll();
tasks.clear();
if (c != null) {
c.moveToFirst();
while (c.isAfterLast() == false) {
tasks.add(c.getString(0));
c.moveToNext();
}
c.close();
}
return tasks;
}
private void renderTodos() {
// The ViewModel handles rendering and changes to the view's
// data. The View simply provides a reference to its
// elements.
taskView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, getTasks().toArray(new String[] {})));
}
@Override
public void registerTaskList(final ListView list) {
taskView = list; // Keep reference for rendering later
if (list.getAdapter() == null) // Show items at startup
{
renderTodos();
}
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id) { // Tapping on any
// item in the list
// will delete that
// item from the
// database and
// re-render the list
deleteTask(view);
renderTodos();
}
});
}
@Override
public void registerTaskAdder(final View button, final EditText input) {
newTask = input;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View view) { // Add task to database, re-render list, and clear the input
addTask(newTask);
renderTodos();
newTask.setText("");
}
});
}
}
Extra
额外的
Adding new Views, or different views, should be handled in the activity. This is nice, since you can now listen for configuration changes, and swap in a special Fragment for a different orientation...
添加新视图或不同的视图应在活动中处理。这很好,因为您现在可以监听配置更改,并交换一个特殊的 Fragment 以获得不同的方向......
回答by Cheng
I am a contributor of RoboBinding- A data-binding Presentation Model(MVVM) framework for the Android platform. I will offer my understanding here. MVVM is commonly used in Microsoft community, which is actually originated from Martin Fowler's Presentation Model. The simplified picture of MVVM pattern is View--synchronization mechanism(or data binding)-->View Model-->Model. The major motive and benefit of using MVVM is that ViewModel becomes pure POJO, which can be Unit Tested(NOT Android Unit Tests, which takes ages.). In Android, a possible way to apply MVVM is: View(Layout+Activity)---->synchronization mechanism(or data binding)-->ViewModel(pure POJO)-->Model(Business Model). The arrow directions also indicate the dependencies. You can instantiate your business models in View Layer and then pass into ViewModel, but the access flow is always View to ViewModel, and ViewModel to Business Model. There is an simple Android MVVM sample appunder RoboBinding. And I recommend you to read Martin Fowler's original article about Presentation Model.
我是RoboBinding- Android 平台的数据绑定表示模型 (MVVM) 框架的贡献者。我将在这里提供我的理解。微软社区常用MVVM,其实起源于Martin Fowler的Presentation Model. MVVM模式的简化图是View--同步机制(或数据绑定)-->View Model-->Model。使用 MVVM 的主要动机和好处是 ViewModel 变成了纯 POJO,可以进行单元测试(而不是 Android 单元测试,这需要很长时间。)。在Android中,一种可能的应用MVVM的方式是:View(Layout+Activity)---->同步机制(或数据绑定)-->ViewModel(纯POJO)-->Model(Business Model)。箭头方向还指示相关性。你可以在View层实例化你的业务模型,然后传入ViewModel,但是访问流程始终是View到ViewModel,ViewModel到Business Model。RoboBinding 下有一个简单的Android MVVM 示例应用程序。我建议你阅读 Martin Fowler'
To apply MVVM, there is a synchronization mechanism module you need to implement, which can be complicated when there is no third-party lib. If you don't want to depend on a third-party lib, you can try to apply MVP(Passive View). But note that the use of Test Double for views. The motive of both pattern are trying to make ViewModel or Presenter not to depend on(or not to directly depend on) View so that they can be Normal Unit tested(NOT Android Unit Tested).
要应用MVVM,需要实现一个同步机制模块,如果没有第三方库,可能会很复杂。如果不想依赖第三方库,可以尝试应用MVP(Passive View)。但要注意对视图使用 Test Double。两种模式的动机都是试图让 ViewModel 或 Presenter 不依赖(或不直接依赖)View,以便它们可以进行普通单元测试(非 Android 单元测试)。
回答by Ramkailash
You can follow these steps for DataBinding in Fragments: I have posted design and java class both in Example for Binding Data in Fragment.
您可以按照以下步骤在 Fragment 中进行数据绑定:我已经在 Fragment 中的数据绑定示例中发布了设计和 java 类。
Layout XML
布局 XML
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data class=".UserBinding">
<variable name="user" type="com.darxstudios.databind.example.User"/>
</data>
<RelativeLayout
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivityFragment">
<TextView android:text='@{user.firstName+" "+user.lastName}' android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Button"
android:id="@+id/button"
android:layout_below="@+id/textView"
android:layout_toEndOf="@+id/textView"
android:layout_marginStart="40dp"
android:layout_marginTop="160dp" />
</RelativeLayout>
</layout>
Fragment class
片段类
public class MainActivityFragment extends Fragment {
public MainActivityFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final User user = new User();
user.setFirstName("Michael");
user.setLastName("Cameron");
UserBinding binding = DataBindingUtil.inflate(inflater,R.layout.fragment_main, container, false);
binding.setUser(user);
View view = binding.getRoot();
final Button button = (Button) view.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
user.setFirstName("@Darx");
user.setLastName("Val");
}
});
return view;
}
}
回答by Kaushik Gopal
I really like the initial OP's approach and would prefer an improvised approach to that. The problem with the @Entreco's answer is that the ViewModel is not longer a POJO. There's huge benefit in having a ViewModel as a simple POJO, as that makes testing really easy. Having it as an Activity, may make it a tad bit more framework dependent, which in some ways goes again the intention of the MVVM isolation pattern.
我真的很喜欢最初的 OP 方法,并且更喜欢即兴的方法。@Entreco 的答案的问题在于 ViewModel 不再是 POJO。将 ViewModel 作为一个简单的 POJO 有很大的好处,因为这使得测试变得非常容易。将其作为 Activity 使用,可能会使其更加依赖于框架,这在某些方面再次符合 MVVM 隔离模式的意图。