web-dev-qa-db-ja.com

Android MVVMパターンとフラグメントの使用方法は?

まず、英語が上手ではないことをお詫びします。

私は何年も開発してきたJava SEソフトウェアで、以前はMVC設計パターンを使用していました。今はAndroidアプリを開発していますが、 Android=は既にMVCパターンを使用しており、xmlファイルがビューとして機能することを示しています。

私はウェブ上で多くの調査を行いましたが、このトピックについては全会一致ではないようです。 MVCパターンを使用するものもあれば、MVPパターンを使用するものもありますが、私は全会一致ではありません。

最近私は本( Android Best Practices、Godfrey Nolan、Onur Cinar、David Truxallから )を購入しました。第2章では、MVC、MVVM、Dependency Injectionパターンについて説明しています。それらすべてを試した後、私のアプリと私の作業モードではMVVMパターンが最適だと思います。

このパターンはアクティビティを使用してプログラミングするときに非常に使いやすいと思いますが、フラグメントを使用してプログラミングするときにそれを使用する方法について混乱しています。 「Android Best Practices」本のウェブサイトからダウンロードした簡単な「todoアプリ」に適用したMVVMパターンの例を再現します。

ビュー(アクティビティ)

   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でアクティビティへの参照が非常に頻繁に必要になることがわかりました(アクティビティのビューへの参照ではなく、アクティビティ自体への参照)。

または、たとえば、フラグメントを伴うアクティビティが、モデル(データベースまたはRESTサービス)からではなく、インテントから受信したデータを処理していると想像してください。すると、モデルはいらないと思います。アクティビティでインテントを受け取ったときにモデルを作成できるかもしれませんが、これは正しくないと感じています(ビューにはモデルとの関係はなく、viewmodelのみが必要です...)。

フラグメントを使用する場合、AndroidでMVVMパターンを使用する方法について誰かが私に説明を提供できますか?

前もって感謝します。

18
R. Campos

注:以下は古くなっているので、お勧めしません。これは主に、この設定でViewsmodelをテストすることが難しいためです。 Googleアーキテクチャブループリントをご覧ください。

古い答え:

個人的に、私は別の設定を好みます:

モデル

あなたのモデル。変更する必要はありません(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インターフェイスを実装することを強制します。フラグメントがアクティビティから切り離されると、デフォルトの実装がデリゲートとして設定されます。これにより、アクティビティにアタッチされていないフラグメントのインスタンスがある場合にエラーが発生するのを防ぐことができます(そうなります)。

これが私の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(アクティビティ)

アクティビティを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("");
            }
        });
    }
}

追加

新しいビューまたは別のビューの追加は、アクティビティで処理する必要があります。これで、構成の変更をリッスンし、特別なフラグメントを別の方向に交換できます。

8
Entreco

私は RoboBinding の寄稿者です= Androidプラットフォーム用のデータバインディングプレゼンテーションモデル(MVVM)フレームワーク。ここで理解を深めます。MVVMは一般的にマーティンファウラーの プレゼンテーションモデル から実際に生まれたMicrosoftコミュニティ。MVVMパターンの簡略化された図は、ビュー-同期メカニズム(またはデータバインディング)->ビューモデル->モデルです。主な動機MVVMを使用する利点は、ViewModelがユニットテスト可能な純粋なPOJOになることです(NOT Androidユニットテスト、時間がかかります。)。Androidでは、MVVMを適用するための可能な方法は次のとおりです。 (レイアウト+アクティビティ)---->同期メカニズム(またはデータバインディング)-> ViewModel(pure POJO)->モデル(ビジネスモデル)。矢印の方向も依存関係を示します。ビューでビジネスモデルをインスタンス化できますレイヤー化してからViewModelに渡しますが、アクセスフローは常にView to ViewModel、およびViewModel to Business Modelです。Roboの下に単純な Android MVVMサンプルアプリ があります。バインド。そして、Martin Fowlerの プレゼンテーションモデル に関する元の記事を読むことをお勧めします。

MVVMを適用するには、実装する必要のある同期メカニズムモジュールがあり、サードパーティのlibがない場合は複雑になる可能性があります。サードパーティのlibに依存したくない場合は、 MVP(Passive View) を適用してみてください。ただし、ビューにTest Doubleを使用することに注意してください。両方のパターンの動機は、ViewModelまたはPresenterがビューに依存しない(または直接依存しない)ようにして、通常のユニットテストができるようにすることです(NOT Androidユニットテスト)。

7
Cheng

フラグメントでのデータバインディングの手順は次のとおりです。フラグメントとデータをバインドする例の両方で、デザインと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;
    }

}

データバインディングの詳細についての開発者向けページ

4
Ramkailash

私は最初のOPのアプローチが本当に好きで、それに対する即興のアプローチを望んでいます。 @Entrecoの答えの問題は、ViewModelがPOJOではなくなったことです。テストが非常に簡単になるので、ViewModelを単純なPOJOとして持つことには大きなメリットがあります。それをアクティビティとして持つと、フレームワークに少し依存するようになる可能性があります。これは、いくつかの点で、MVVM分離パターンの意図に戻ります。

3
Kaushik Gopal