banner
jzman

jzman

Coding、思考、自觉。
github

Android JetpackコンポーネントのViewModelに関する記事

前面学習した LiveData と Lifecycle アーキテクチャコンポーネントの使用:

ViewModel はライフサイクルを意識しており、UI に関連するデータを自動的に保存および管理します。デバイスの構成が変わった後でもデータは存在し続けるため、onSaveInstanceState でデータを保存したり、onCreate でデータを復元したりする必要がありません。ViewModel を使用することで、この部分の作業は不要になり、視覚とロジックをうまく分離できます。

  1. ViewModel ライフサイクル
  2. ViewModel のソースコード分析
  3. ViewModelStore とは
  4. ViewModelStoreOwner とは
  5. Fragment 間の通信を簡素化する方法

ViewModel ライフサイクル#

image

OnCreate から ViewModel を取得すると、それは ViewModel がバインドされている View が完全に onDestroy されるまで存在し続けます。

ViewModel のソースコード分析#

今回のプロジェクト作成は Android Studio を 3.2.1 にアップグレードしたため、プロジェクト内の依存パッケージを androidx の対応パッケージに置き換えました。主な設定は以下の通りです:

//gradleプラグイン
dependencies {
    classpath 'com.android.tools.build:gradle:3.2.1'
}

// ViewModel と LiveData のバージョン
def lifecycle_version = "2.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"

//gradle-wrapper.propertiesファイル
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip

ViewModel の作成は以下の通りです:

/**
 * 
 * Context を使用する必要がある場合は AndroidViewModel を継承することができます
 * Powered by jzman.
 * Created on 2018/12/13 0013.
 */
public class MViewModel extends ViewModel {

    private MutableLiveData<List<Article>> data;

    public LiveData<List<Article>> getData(){
        if (data == null){
            data = new MutableLiveData<>();
            data.postValue(DataUtil.getData());
        }
        return data;
    }
}

Context を使用する必要がある場合は AndroidViewModel を継承することができますが、ここでは ViewModel を継承するだけで大丈夫です。そして、Activity で使用することができます。具体的には以下の通りです:

MViewModel mViewModel = ViewModelProviders.of(this).get(MViewModel.class);
mViewModel.getData().observe(this, new Observer<List<Article>>() {
    @Override
    public void onChanged(List<Article> articles) {
        for (Article article : articles) {
            Log.i(TAG,article.getDesc());
        }
    }
});

呼び出しプロセスを見てみましょう。ViewModelProviders から始まり、ViewModelProviders は対応する ViewModelProvider を取得するための 4 つの静的メソッドを提供します。4 つの静的メソッドは以下の通りです:

public static ViewModelProvider of(@NonNull Fragment fragment)
public static ViewModelProvider of(@NonNull FragmentActivity activity)
public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory)
public static ViewModelProvider of(@NonNull FragmentActivity activity, @Nullable Factory factory)

2 番目のメソッドを例にとると、その実装は以下の通りです:

@NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
        @Nullable Factory factory) {
    Application application = checkApplication(activity);
    if (factory == null) {
        factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }
    return new ViewModelProvider(activity.getViewModelStore(), factory);
}

デフォルトの AndroidViewModelFactory を使用することも、自分で Factory をカスタマイズすることもできます。上記のいずれかのメソッドを呼び出して ViewModelProvider を作成するだけです:

次に ViewModelProvider を見てみましょう。ViewModelProvider の 2 つの重要な属性:

private final Factory mFactory;
private final ViewModelStore mViewModelStore;

ViewModelProvider を作成するとき、mFactory と mViewModelStore はすでに初期化されています。次は get () メソッドで、ソースコードは以下の通りです:

@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    //クラス名を取得し、内部クラス名を取得する際には getName と区別されます
    //getCanonicalName-->xx.TestClass.InnerClass
    //getName-->xx.TestClass$InnerClass
    String canonicalName = modelClass.getCanonicalName();
    if (canonicalName == null) {
        throw new IllegalArgumentException("ローカルおよび匿名クラスは ViewModels になれません");
    }
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

次に、引数 key を持つ get メソッドを呼び出します:

 public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            //noinspection unchecked
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if (viewModel != null) {
                // TODO: 警告をログに記録します。
            }
        }

        //ViewModelを作成
        viewModel = mFactory.create(modelClass);
        //mViewModelStore から key に基づいて対応する ViewModel を取得して返します
        mViewModelStore.put(key, viewModel);
        //noinspection unchecked
        return (T) viewModel;
    }

この時点で、ViewModel が作成されました。では、ViewModel はどのように作成されるのでしょうか。mFactory の具体的な実装はデフォルトの AndroidViewModelFactory であり、その作成時に反射を使用してコンストラクタを取得して作成されます。重要なコードは以下の通りです:

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    //AndroidViewModel が modelClass の親クラスまたはインターフェースであるかを判断します
    if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
        //...
        //反射を使用して ViewModel を作成し、返します
        return modelClass.getConstructor(Application.class).newInstance(mApplication);
    }
    return super.create(modelClass);
}

具体的な ViewModel オブジェクトが作成された後は、具体的な ViewModel 内のメソッドを自由に呼び出すことができます。前述のソースコードでは、ViewModelStore、ViewModelStoreOwner、AndroidViewModelFactory などのさまざまなラッパークラスに出会うことになります。以下で紹介します。

ViewModelStore とは#

ViewModelStore は、デバイスの構成が変わったときに ViewModel の状態を保存するために使用されます。たとえば、現在の画面が再作成または破棄された場合などです。対応する新しい ViewModelStore は、古い ViewModelStore と同様に、対応する ViewModel のすべての情報を保存する必要があります。対応する clear () メソッドが呼び出されるまで、この ViewModel は使用されていないことが通知されません。その場合、対応する ViewModelStore も関連情報を保存しなくなります。

このクラスは実際には HashMap を使用して対応する ViewModel を保存します。非常にシンプルです:

public class ViewModelStore {
    private final HashMap<String, ViewModel> mMap = new HashMap<>();
    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.onCleared();
        }
        mMap.clear();
    }
}

ViewModelStoreOwner とは#

これはインターフェースで、対応する ViewModel の ViewModelStore を取得するためのメソッド getViewModelStore () を定義しています。同様に、ViewModelStoreOwner の clear () メソッドを呼び出すと、対応する ViewModelStore を取得できなくなります。ソースコードは以下の通りです:

public interface ViewModelStoreOwner {
    /**
     * 所有する {@link ViewModelStore} を返します
     *
     * @return {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}

もちろん、具体的には実装クラスになります。実際には FragmentActivity や Fragment などがこのインターフェースを間接的または直接的に実装しています。この点は LifecycleOwner と同様です。ソースコードの参考は以下の通りです:

  • Activity の間接実装:
public ViewModelStore getViewModelStore() {
    if (getApplication() == null) {
        throw new IllegalStateException("アクティビティがまだアプリケーションインスタンスにアタッチされていません。onCreate 呼び出し前に ViewModel をリクエストすることはできません。");
    }
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            // NonConfigurationInstances から ViewModelStore を復元します
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}
  • Fragment の直接実装:
@Override
public ViewModelStore getViewModelStore() {
    if (mFragmentManager == null) {
        throw new IllegalStateException("切り離されたフラグメントから ViewModels にアクセスできません");
    }
    return mFragmentManager.getViewModelStore(this);
}

ViewModelStore の保存プロセスは Activity または Fragment の上層実装で完了します。ViewModelStoreOwner インターフェースを理解するのはここまでで大丈夫です。

Fragment 間の通信を簡素化する方法#

Fragment 間の通信は以前はインターフェースを使用してホスト Activity に転送して実現していましたが、現在は同じ ViewModel を使用して 2 つの Fragment 間の通信を行うことができます。1 つ覚えておいてほしいのは、ViewModel を使用して 2 つの Fragment 間の通信を行う場合、ViewModel をホスト Activity を使用して作成することです。実装プロセスは以下の通りです。まず、ViewModel を作成します:

/**
 * Powered by jzman.
 * Created on 2018/12/14 0014.
 */
public class FViewModel extends ViewModel {
    private MutableLiveData<String> mSelect = new MutableLiveData<>();
    public void selectItem(String item) {
        mSelect.postValue(item);
    }
    public LiveData<String> getSelect() {
        return mSelect;
    }
}

次に、LeftFragment を作成します:

public class LeftFragment extends Fragment {
    private FViewModel mViewModel;
    private FragmentTitleBinding titleBinding;
    public LeftFragment() {
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_title, container, false);
        titleBinding = DataBindingUtil.bind(view);
        mViewModel = ViewModelProviders.of(getActivity()).get(FViewModel.class);
        RvAdapter adapter = new RvAdapter(getActivity(), new RvAdapter.OnRecycleItemClickListener() {
            @Override
            public void onRecycleItemClick(String info) {
                mViewModel.selectItem(info);
            }
        });
        titleBinding.rvData.setLayoutManager(new LinearLayoutManager(getActivity()));
        titleBinding.rvData.setAdapter(adapter);
        return view;
    }
}

LeftFragment のレイアウトファイルは RecycleView だけで、そのアイテムのレイアウトファイルは以下の通りです:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="itemData"
            type="String"/>
        <variable
            name="onItemClick"
            type="com.manu.archsamples.fragment.RvAdapter.OnRecycleItemClickListener"/>
    </data>
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:onClick="@{() -> onItemClick.onRecycleItemClick(itemData)}">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{itemData}"
            android:padding="10dp"/>
    </LinearLayout>
</layout>

RecyclerView の Adapter は以下の通りです:

public class RvAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private Context mContext;
    private List<String> mData;
    private OnRecycleItemClickListener mOnRecycleItemClickListener;
    public RvAdapter(Context mContext,OnRecycleItemClickListener itemClickListener) {
        this.mContext = mContext;
        mData = DataUtil.getDataList();
        mOnRecycleItemClickListener = itemClickListener;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.recycle_item,null);
        view.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        ));
        return new MViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        MViewHolder mHolder = (MViewHolder) holder;
        mHolder.bind(mData.get(position),mOnRecycleItemClickListener);
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    private static class MViewHolder extends RecyclerView.ViewHolder{
        RecycleItemBinding itemBinding;
        MViewHolder(@NonNull View itemView) {
            super(itemView);
            itemBinding = DataBindingUtil.bind(itemView);
        }

        void bind(String info, OnRecycleItemClickListener itemClickListener){
            itemBinding.setItemData(info);
            itemBinding.setOnItemClick(itemClickListener);
        }
    }

    public interface OnRecycleItemClickListener {
        void onRecycleItemClick(String info);
    }
}

次に、RightFragment を作成します:

public class RightFragment extends Fragment {
    private static final String TAG = RightFragment.class.getName();
    private FragmentContentBinding contentBinding;
    private FViewModel mViewModel;
    public RightFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_content, container, false);
        contentBinding = DataBindingUtil.bind(view);
        mViewModel = ViewModelProviders.of(getActivity()).get(FViewModel.class);
        mViewModel.getSelect().observe(this, new Observer<String>() {
            @Override
            public void onChanged(String s) {
                //LeftFragment のアイテムクリックイベントの値を受け取ります
                contentBinding.setData(s);
            }
        });
        return view;
    }
}

RightFragment のレイアウトファイルは以下の通りです:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="data"
            type="String"/>
    </data>

    <FrameLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".fragment.LeftFragment">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:layout_marginStart="12dp"
            android:layout_marginTop="10dp"
            android:text="@{data,default=def}"/>
    </FrameLayout>
</layout>

実装方法は比較的シンプルで、特に多くの説明は必要ありません。ViewModel を使用すると、ホスト Activity は非常にクリーンになり、Fragment の切り替えだけを担当すればよくなります。テスト効果は以下の通りです:

jzman-blog

ViewModel を使用する利点は以下の通りです:

  1. Activity は子 Fragment 間の通信に介入しなくなり、責任がより単純になります。
  2. Fragment 間は同じ ViewModel のインスタンスを使用する以外は互いに異なり、どの Fragment も独立して動作できます。
  3. 各 Fragment は独自のライフサイクルを持ち、自由に置き換えたり削除したりしても、他の Fragment の正常な動作に影響を与えません。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。