Подтвердить что ты не робот

Как использовать шаблон Android MVVM с фрагментами?

Сначала я прошу извиниться за мой не хороший английский.

Я разработал много лет программное обеспечение Java SE, и я использовал шаблон проектирования MVC. Теперь я разрабатываю приложения для Android, и я не доволен аргументом, в котором говорится, что андроид уже использует шаблон MVC, а файлы xml действуют как представление.

Я провел много исследований в Интернете, но, похоже, в этой теме нет единодушия. Некоторые используют шаблон MVC, другие - шаблон MVP, но, на мой взгляд, нет единодушия.

Недавно я купил книгу (Android Best Practices от Godfrey Nolan, Onur Cinar и David Truxall), а в главе 2 вы могут найти шаблоны MVC, MVVM и зависимостей впрыска. Попробовав их все, я думаю, что для моих приложений и моего режима работы лучшим является шаблон MVVM.

Я считаю, что этот шаблон очень прост в использовании при программировании с действиями, но я смущен тем, как его использовать при программировании с фрагментами. Я воспроизведу пример шаблона MVVM, применяемого к простому "todo app", загруженному с веб-сайта книги "Лучшие 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);
    }
   }

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

Проблема в том, что когда я пытаюсь воспроизвести этот шаблон при использовании фрагментов, я не уверен, как это сделать. Могу ли я иметь модель представления и модель для каждого фрагмента или только для активности, содержащей эти фрагменты?

С классическим подходом к фрагменту (фрагмент является внутренним классом внутри действия), легко взаимодействовать с активностью или обращаться к диспетчеру фрагментов, чтобы делать изменения, но если я отключаю код и логика моей программы вне деятельности, я видел, что мне очень часто нужно ссылаться на активность в моей модели ViewModel (не ссылки на представления активности, а ссылки на сами действия).

Или, например, представьте, что активность с фрагментами работает с данными, полученными от намерения, а не с помощью модели (базы данных или службы отдыха). Тогда я чувствую, что мне не нужна модель. Может быть, я смогу создать модель, когда получаю намерение в своей деятельности, но я чувствую, что это неверно (представление не должно иметь отношения к модели, только viewmodel...).

Может кто-нибудь предложить мне объяснение того, как использовать шаблон MVVM с андроидом при использовании фрагментов?

Спасибо заранее.

4b9b3361

Ответ 1

ПРИМЕЧАНИЕ. Следующее устарело, и я бы не рекомендовал его больше. В основном из-за того, что в этой настройке сложно проверить Viewsmodel. Взгляните на чертежи Google Architecture Blueprints.

Старый ответ:

Лично я предпочитаю альтернативную настройку:

Модель

Ваша модель. Не нужно менять (красота использования MVVM:))

Вид (фрагмент)

Немного отличается. В представлении (фрагменте) есть ссылка на 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();
}

Таким образом, ваш просмотр (фрагмент) обеспечивает, чтобы активность, к которой она присоединена, реализует интерфейс ITaskListManager. Когда фрагмент отделен от 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("");
            }
        });
    }
}

Extra

В этой деятельности необходимо обрабатывать добавление новых представлений или разных представлений. Это приятно, так как теперь вы можете слушать изменения конфигурации и обмениваться специальным фрагментом для другой ориентации...

Ответ 2

Я являюсь вкладчиком RoboBinding - рамки модели представления данных (MVVM) для платформы Android. Я предложу свое понимание здесь. MVVM обычно используется в сообществе Microsoft, который на самом деле происходит от Martin Fowler Модель представления. Упрощенная картина шаблона MVVM - механизм просмотра - синхронизации (или привязка данных) → Модель просмотра → Модель. Основным мотивом и преимуществом использования MVVM является то, что ViewModel становится чистым POJO, который может быть Unit Test (NOT Android Unit Tests, который занимает много времени). В Android возможным способом применения MVVM является: View (Layout + Activity) ---- > механизм синхронизации (или привязка данных) → ViewModel (чистый POJO) → модель (бизнес-модель). Направления стрелок также указывают на зависимости. Вы можете создавать свои бизнес-модели в режиме просмотра, а затем переходить в ViewModel, но поток доступа - это всегда View to ViewModel и ViewModel для бизнес-модели. В RoboBinding есть простой пример Android MVVM sample. И я рекомендую вам прочитать оригинальную статью Мартина Фаулера о Модель презентации.

Чтобы применить MVVM, необходим модуль механизма синхронизации, который может быть сложным, если нет сторонней библиотеки. Если вы не хотите зависеть от сторонней библиотеки, вы можете попробовать применить MVP (пассивный просмотр). Но учтите, что использование Test Double для просмотров. Мотив обоих шаблонов пытается заставить ViewModel или Presenter не зависеть от (или не напрямую зависеть) View, чтобы они могли быть обычным модулем (NOT Android Unit Tested).

Ответ 3

Мне очень нравится первоначальный подход OP и предпочтет импровизированный подход к этому. Проблема с ответом @Entreco заключается в том, что ViewModel больше не является POJO. Там огромное преимущество в том, что ViewModel является простым POJO, поскольку это делает тестирование очень простым. Имея это как действие, может сделать его чуть более зависимым от структуры, что в некотором роде снова повторяет намерение шаблона изоляции MVVM.

Ответ 4

Вы можете выполнить следующие шаги для DataBinding в фрагментах: Я опубликовал проект и класс 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;
    }

}

Страница разработчика для данных привязки данных