【问题标题】:How to use Android MVVM pattern with fragments?如何将 Android MVVM 模式与片段一起使用?
【发布时间】:2014-04-11 14:30:13
【问题描述】:

首先我请求为我的英语不好向你道歉。

我开发Java SE软件很多年了,我曾经使用过MVC设计模式。现在我开发 android 应用程序,我对说 android 已经使用 MVC 模式,xml 文件充当视图的说法不满意。

我在网上做了很多研究,但似乎对这个话题没有一致意见。有些使用 MVC 模式,有些使用 MVP 模式,但我个人认为,没有一致意见。

最近我买了一本书(Android Best Practices, from Godfrey Nolan, Onur Cinar and David Truxall),在第二章你可以找到MVC、MVVM和依赖注入模式的解释。在尝试了所有这些之后,我认为对于我的应用程序和我的工作模式来说,最好的是 MVVM 模式。

我发现这种模式在使用 Activity 编程时非常容易使用,但是在使用 Fragment 进行编程时我对如何使用它感到困惑。我将重现应用于简单“todo app”的 MVVM 模式示例,该示例是从“Android 最佳实践”一书的网站下载的。

视图(活动)

   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);
    }
   }

模型

 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);
    }
   }

视图模型

 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("");
            }
        });
    }
   }

问题是,当我尝试在使用片段时重现此模式时,我不知道如何继续。我可以为每个片段或仅为包含这些片段的活动提供视图模型和模型吗?

使用经典的fragment方式(fragment是activity内部的一个内部类),很容易和activity交互,或者访问fragment manager做修改,但是如果我解耦代码,把我的程序在活动之外的逻辑,我发现我经常需要在我的 ViewModel 中引用活动(不是对活动视图的引用,而是对活动本身的引用)。

或者例如,假设带有片段的活动正在处理从意图接收的数据,而不是来自模型(数据库或休息服务)的数据。然后,我觉得我不需要模型。也许我可以在activity中收到intent时创建模型,但我觉得这样不正确(视图不应该和模型有关系,只有viewmodel...)。

谁能给我解释一下在使用片段时如何在android中使用MVVM模式?

提前致谢。

【问题讨论】:

    标签: android mvvm android-fragments


    【解决方案1】:

    注意:以下内容已过时,我不再推荐。主要是因为在此设置中很难测试 Viewsmodel。查看 Google 架构蓝图。

    旧答案:

    就个人而言,我更喜欢另一种设置:

    模型

    你的模特。不需要更改(使用 MVVM 的美妙之处:))

    视图(片段)

    略有不同。 View (Fragment) 在我的设置中有对 ViewModel ( Activity ) 的引用。而不是像这样初始化您的委托:

    // Old way -> I don't like it
    this.delegate = new TodoViewModel(this);
    

    我建议你使用众所周知的 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();
    }
    

    这样,您的视图(片段)强制它所附加的 Activity 实现 ITaskListManager 接口。当 Fragment 与 Activity 分离时,一些默认实现被设置为委托。这可以防止在您拥有未附加到 Activity 的片段实例时出现错误(是的,这可能会发生)。

    这是我的 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;
        }
    }
    

    ViewModel(活动)

    使用 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("");
                }
            });
        }
    }
    

    额外

    应在活动中处理添加新视图或不同视图。这很好,因为您现在可以监听配置更改,并为不同的方向交换一个特殊的 Fragment...

    【讨论】:

      【解决方案2】:

      我真的很喜欢最初的 OP 的方法,并且更喜欢即兴的方法。 @Entreco 的答案的问题是 ViewModel 不再是 POJO。将 ViewModel 作为简单的 POJO 有很大的好处,因为这使测试变得非常容易。将它作为一个 Activity 可能会使它更加依赖于框架,这在某些方面再次符合 MVVM 隔离模式的意图。

      【讨论】:

      • 没有考虑到在这种情况下,Activity 至少混合了两组完全不同的关注点:1)响应各种 Activity/Fragment 生命周期事件,2)扮演 ViewModel 部分。在 SoC 方面似乎不是很好的想法
      • 是的,将视图模型行为抽象为一个独立的“简单”对象是整个 MVVM 方法的关键优势,否则就会丢失。
      【解决方案3】:

      我是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(业务模型)。箭头方向还指示依赖关系。您可以在 View Layer 中实例化您的业务模型,然后传递给 ViewModel,但访问流程始终是 View 到 ViewModel,以及 ViewModel 到 Business Model。 RoboBinding 下有一个简单的Android MVVM sample app。我建议你阅读 Martin Fowler 关于Presentation Model 的原创文章。

      要应用MVVM,需要实现一个同步机制模块,在没有第三方lib的情况下会比较复杂。如果不想依赖第三方库,可以尝试申请MVP(Passive View)。但请注意使用 Test Double 进行视图。这两种模式的动机都是试图让 ViewModel 或 Presenter 不依赖(或不直接依赖)View,以便它们可以进行普通单元测试(不是 Android 单元测试)。

      【讨论】:

        【解决方案4】:

        您可以按照以下步骤在 Fragments 中进行数据绑定: 我已经在片段中绑定数据的示例中发布了设计和 java 类。

        布局 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>
        

        片段类

        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;
            }
        
        }
        

        Developer page for databinding details

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-03-10
          • 1970-01-01
          • 1970-01-01
          • 2011-12-05
          • 2013-01-24
          • 2021-06-11
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多