Java
Android
AndroidStudio
MVVM
architecture-components

Android Architecture Components 初級 ( MVVM + LiveData 編 )

GodActivity駆動アプリ から MVVM 設計リフォーム に向けて


来月から、神Activity(責任範囲が広範の膨大なActivity)で運用されるアプリケーションの設計をJavaMVVMにリフォームをすることになるかも...と言われ震えています。

ということで、短期間でAndroidのMVVM設計 + Android Architecture Componentsを学習するにあたり、参考になったものなど、自分の復習も兼ねてサンプルと照らし合わせながら、この設計の軽い流れをおさらいしていけたらなと思います。

MVVM( Model-View-ViewModel )設計


まず、MVVM(Model-View-ViewModel)設計とはなんだ、と。

私みたいなMVC( Model-View-Controller )のWebフレームワークしか触ったことのなかった人間は、某カンファレンスでこの設計について熱弁する方々を目の当たりにしても、その意義がよくわかっておりませんでした。

ざっくり言うと、MVVMは上記MVCの派生パターンであり、MVVMを考慮してアプリケーションを開発する目的は、他のMVC系のパターンと同様にアプリケーションの「プレゼンテーション層(ユーザーが見て触れられる層)とドメイン(ビジネスロジック)を分離」することです。

そのアーキテクチャをAndroidでDataBindingライブラリが登場したことで、利用可能になったよと、そして、 Android Architecture Components の登場により、より扱いやすくなったよ、ということのようです。

大まかなメリットとしては、よくあげられるのは

  • 関心の分離
  • 依存関係の切り離し
  • 画面回転問題

などなど、また、依存関係が

Model
(リモートとローカル問わず、データリソースを操作する領域)


:point_up_2_tone2:

ViewModel
(DataBindingライブラリを通し、Viewに表示するデータの監視、取得をする領域)


:point_up_2_tone2:

View
(xml/Activity/Fragment)


と依存関係が単方向になることで、保守性も向上します。
設計についてはもっと詳しい記事が世の中に溢れているので、詳細は以下に紹介いたします。

主要な設計への理解に関しては、以下の記事がとてもわかりやすく参考になりました。
Androidアーキテクチャことはじめ ― 選定する意味と、MVP、Clean Architecture、MVVM、Fluxの特徴を理解する

一番お世話になっているのはこの本。
Android アプリ設計パターン入門

設計ってなんのためにするの?と言うところからAndroidの歴史、有名なアプリケーションで使われる設計の解説、経緯、と盛りだくさんで、スルメのように何度読んでも発見があります。

Android Architecture Components


Google I/O 2017で発表されたAndroid Architecture ComponentsはGoogleが推奨するデザインパターンを扱いやすくしたライブラリ群です。
4つに分かれてます。(発表当初)(公式ドキュメントから参照/引用)

Google I/O 2018では、Android Jetpackに、SupportLibraryやその他最新鋭便利ツールとがっちゃんこされることが発表されました。

Google I/O 2018

なので、現時点ではComponents群の中で以下の3つも新たにサポートされております。

進化はええええ...

さて、今回お世話になる、ViewModelはModelの変更を監視し、データをViewにバインドし、View操作を伝達するクラスです。Viewとライフサイクルを共に歩むので、画面回転などユーザの想定外の操作に強くなります。
LiveDataは、Android Architecture Componentsが提供する、ライフサイクルと連動した監視が可能な、データホルダーのクラスです。

【Android Architecture Components】Guide to App Architecture 和訳
LiveDataの基礎的な性質を整理する。

サンプル


参考:MVVM architecture, ViewModel and LiveData (Part 1)
とても参考になった記事です。

ここで紹介されているサンプル(よく見るあるユーザのGithubリポジトリをずらっと表示するだけのクライアントアプリ)の実装を、順番に見ていければなと思います。

これだけ
https://gyazo.com/cb86e446446cafc7043b46cabb630c3e

ソース : https://github.com/Tsutou/GithubClient

まず、当たり前ですが、依存関係のない方向に従い ( Model-> ViewModel -> View ) の順で実装していったほうが自分はやりやすかったです。

設計にこれといった答えはなく、これが正解というわけではないです。色々なサンプルを触って、最もシンプルでわかりやすかったので、参考にさせて頂いてます。

ざっくりイメージ

Untitled mvvmDiagram (1).png

Gradle

build.gradle
android {

    //…

    dataBinding { 
        enabled = true 
    } 
}

データバインディングライブラリのお力を借りるため、dataBinding要素をtrueにしましょう。

ライブラリ

build.gradle
dependencies {

   //…

   // retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'

   // gson 
    implementation 'com.google.code.gson:gson:2.8.4'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

    // Android Architecture Components
    implementation "android.arch.lifecycle:extensions:1.1.1"
    annotationProcessor "android.arch.lifecycle:compiler:1.1.1"

    // Other 
    implementation "com.squareup.okhttp3:okhttp:3.10.0" 
    implementation "com.squareup.okhttp3:logging-interceptor:3.9.0" 
}

まず当たり前のように、Javaのインターフェース形式でAPIを定義できるRetrofitと、JSONをいい感じにJavaオブジェクトに変換してくれるGsonを導入します。

そして、Android Architecture Componentsに、ViewModelとLiveDataを扱うため、お力を借ります。

Model

Projectモデル


手始めに、今回扱うProjectモデルを定義します。
(Githubプロジェクトの型)

Project.java
public class Project {
    public long id;
    public String name;
    public String full_name;
    public User owner;
    public String html_url;
    public String description;
    public String url;
    public Date created_at;
    public Date updated_at;
    public Date pushed_at;
    public String git_url;
    public String ssh_url;
    public String clone_url;
    public String svn_url;
    public String homepage;
    public int stargazers_count;
    public int watchers_count;
    public String language;
    public boolean has_issues;
    public boolean has_downloads;
    public boolean has_wiki;
    public boolean has_pages;
    public int forks_count;
    public int open_issues_count;
    public int forks;
    public int open_issues;
    public int watchers;
    public String default_branch;

    public Project() {
    }

    public Project(String name) {
        this.name = name;
    }
}

GithubService


Retrofitに取り扱ってもらうAPIインターフェースを定義します。

GithubService.java
interface GithubService {

    //Retrofitインターフェース(APIリクエストを管理)
    String HTTPS_API_GITHUB_URL = "https://api.github.com/";

    //一覧
    @GET("users/{user}/repos")
    Call<List<Project>> getProjectList(@Path("user") String user);

    //詳細
    @GET("/repos/{user}/{reponame}")
    Call<Project> getProjectDetails(@Path("user") String user,@Path("reponame") String projectName);
}

Repository


ViewModelに対するデータプロバイダです。
ViewModelから呼び出され。レスポンスをLiveData Objectにラップします。
イベントリスナーなどは定義せず、RetrofitがAPIデータを扱うビジネスロジックのみここには存在させます。

ProjectRepository.java
public class ProjectRepository {

    //Retrofitインターフェース
    private GithubService githubService;

    //staticに提供できるRepository
    private static ProjectRepository projectRepository;

    //コンストラクタでRetrofitインスタンスを生成
    private ProjectRepository() {

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(GithubService.HTTPS_API_GITHUB_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        githubService = retrofit.create(GithubService.class);
    }

    //singletonでRepositoryインスタンスを取る
    //synchronized : オブジェクトに鍵をかけて、他のスレッドに邪魔されないようにして作業をする
    public synchronized static ProjectRepository getInstance() {
        if (projectRepository == null) {
            projectRepository = new ProjectRepository();
        }
        return projectRepository;
    }

    //APIにリクエストし、レスポンスをLiveDataで返す(一覧)
    public LiveData<List<Project>> getProjectList(String userId) {
        final MutableLiveData<List<Project>> data = new MutableLiveData<>();

        //Retrofitで非同期リクエスト->Callbackで(自分で実装したModel)型ListのMutableLiveDataにセット
        githubService.getProjectList(userId).enqueue(new Callback<List<Project>>(){
            @Override
            public void onResponse(Call<List<Project>> call,@Nullable Response<List<Project>> response){
                data.setValue(response.body());
            }

            @Override
            public void onFailure(Call<List<Project>> call, Throwable t){
                //TODO: null代入良くない + エラー処理
                data.setValue(null);
            }
        });

        return data;
    }

    //APIにリクエストし、レスポンスをLiveDataで返す(詳細)
    public LiveData<Project> getProjectDetails(String userID,String projectName) {
        final MutableLiveData<Project> data = new MutableLiveData<>();

        githubService.getProjectDetails(userID,projectName).enqueue(new Callback<Project>() {
            @Override
            public void onResponse(Call<Project> call, Response<Project> response) {
                simulateDelay();
                data.setValue(response.body());
            }

            @Override
            public void onFailure(Call<Project> call, Throwable t) {
                //TODO: null代入良くない + エラー処理
                data.setValue(null);
            }
        });
        return data;
    }

    //…

}

ViewModel

ProjectListViewModel


トップページのプロジェクトリストのRepositoryから送られてくるデータまた、リストに対する操作とUIイベントに責務を持つViewModelです。
ビューのコンポーネントを操作することも、データを扱うこともしません。黒子のような位置(雑感)

ProjectListViewModel
public class ProjectListViewModel extends AndroidViewModel {

    //監視対象のLiveData
    private final LiveData<List<Project>> projectListObservable;

    public ProjectListViewModel(Application application){
        super(application);

        //Repositoryからインスタンスを取得し、getProjectListを呼び出し、LiveDataオブジェクトに。
        //変換が必要な場合、これをTransformationsクラスで単純に行うことができます。
        projectListObservable = ProjectRepository.getInstance().getProjectList("任意のGithubユーザネーム");
    }

    //UIが観察できるようにコンストラクタで取得したLiveDataを公開する
    public LiveData<List<Project>> getProjectListObservable() {
        return projectListObservable;
    }
}

ProjectViewModel


こちらは詳細ページのViewModelです。
違いはRepositoryに識別子(project_id)を、DI(依存性注入)で伝えているという特徴があります。

ProjectViewModel.java
public class ProjectViewModel extends AndroidViewModel {

    private final LiveData<Project> projectObservable;
    private final String projectID;

    public ObservableField<Project> project = new ObservableField<>();

    public ProjectViewModel(@NonNull Application application, final String projectID) {
        super(application);
        this.projectID = projectID;
        projectObservable = ProjectRepository.getInstance().getProjectDetails("任意のGithubユーザネーム",projectID);
    }

    //ゲッター(リポジトリのレスポンスを取得)
    public LiveData<Project> getObservableProject(){
        return projectObservable;
    }

    //セッター
    public void setProject(Project project){
        this.project.set(project);
    }


    //…

ここで、インナークラスで依存性注入するstaticなクラスが登場します。
インスタンスを外部で作ってあげて、依存性を切り離します。

FactoryはArchitecture ComponentsとDagger2の合わせ技です。
Architecture Components を Dagger2 と併用する際の ViewModelProvider.Factory について

ProjectViewModel.java
    /**
     *IDの(DI)依存性注入クラス
     */
    public static class Factory extends ViewModelProvider.NewInstanceFactory {
        @NonNull
        private final Application application;

        private final String projectID;

        public Factory(@NonNull Application application, String projectID) {
            this.application = application;
            this.projectID = projectID;
        }

        @Override
        public <T extends ViewModel> T create(Class<T> modelClass) {
            //noinspection unchecked
            return (T) new ProjectViewModel(application, projectID);
        }

View

MainActivity


ActivityではもはやFragmentの生成、画面遷移にしか責任を持ちません。

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            //プロジェクト一覧のFragment
            ProjectListFragment fragment = new ProjectListFragment();

            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.fragment_container, fragment, ProjectListFragment.TAG)
                    .commit();
        }
    }

    //詳細画面への遷移
    public void show(Project project) {
        ProjectFragment projectFragment = ProjectFragment.forProject(project.name);

        getSupportFragmentManager()
                .beginTransaction()
                .addToBackStack("project")
                .replace(R.id.fragment_container, projectFragment, null)
                .commit();
    }
}

ProjectListFragment


Viewの形成また、DataBindingを紐付けます。
データバインディングライブラリのDataBindingUtilにより、Databindingするビューファイルの設定をします。
RecyclerViewのアダプターをセットしたり、loadの制御、また、ViewModelのLiveDataを監視します。

ProjectListFragment.java
public class ProjectListFragment extends Fragment {
    public static final String TAG = "ProjectListFragment";
    private ProjectAdapter projectAdapter;
    private FragmentProjectListBinding binding;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        //dataBinding用のレイアウトリソースをセット
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_project_list, container, false);

        //イベントのcallbackをadapterに伝達
        projectAdapter = new ProjectAdapter(projectClickCallback);
        //上記adapterをRecyclerViewに適用
        binding.projectList.setAdapter(projectAdapter);
        //Loading開始
        binding.setIsLoading(true);
        //rootViewを取得
        return binding.getRoot();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final ProjectListViewModel viewModel =
                ViewModelProviders.of(this).get(ProjectListViewModel.class);

        observeViewModel(viewModel);
    }

observe関数で、LiveDataのActive状態を監視します。
データが更新されたらアップデートするように、LifecycleOwnerを紐付け、ライフサイクル内にオブザーバを追加します。
オブザーバは、STARTEDRESUMED状態である場合にのみ、イベントを受信します。

ProjectListFragment.java
    //observe開始
    private void observeViewModel(ProjectListViewModel viewModel){

        viewModel.getProjectListObservable().observe(this, new Observer<List<Project>>() {
            @Override
            public void onChanged(@Nullable List<Project> projects) {
                if(projects != null){
                    binding.setIsLoading(false);
                    projectAdapter.setProjectList(projects);
                }
            }
        });
    }

    //callbackに操作イベントを設定
    private final ProjectClickCallback projectClickCallback = new ProjectClickCallback() {
        @Override
        public void onClick(Project project) {
            if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
                ((MainActivity) getActivity()).show(project);
            }
        }
    };

}

そして、データバインディング。
慣例の通り、ルートビューをlayoutにしてあげます。そしてdata要素でbindするデータを定義してあげます。

fragmant_project_list.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="isLoading" type="boolean" />
    </data>

    <!--中略-->

今回の場合、モデルのデータをRecyclerViewに渡すのはAdapterを通してなので、こちらでは以下CustomBindingAdapterで定義したカスタムセッターのload判定の処理のみがお仕事をします。

CustomBindingAdapter.java
public class CustomBindingAdapter {
    //xmlに定義する際のBindingAdapter
    @BindingAdapter("visibleGone")
    public static void showHide(View view, boolean show) {
        view.setVisibility(show ? View.VISIBLE : View.GONE);
    }
}
fragmant_project_list.xml
    <LinearLayout
        //…>

        <TextView
            //…
            app:visibleGone="@{isLoading}"/>

        <LinearLayout
            //…
            app:visibleGone="@{!isLoading}">

            <android.support.v7.widget.RecyclerView

                //…

                />

        </LinearLayout>

    </LinearLayout>

そして、RecyclerViewに対するAdapterの実装がこちら。
ミソは、DiffUtilです。Support Library 24.2.0で追加された、2つのListの差分を計算するユーティリティー、です。
差分を計算しつつ、いい感じにアニメーションなどをやってくれます。

Support Library 24.2.0で追加されたDiffUtilを試してみた

ProjectAdapter.java
public class ProjectAdapter extends RecyclerView.Adapter<ProjectAdapter.ProjectViewHolder> {

    List<? extends Project> projectList;

    @Nullable
    private final ProjectClickCallback projectClickCallback;

    public ProjectAdapter(@Nullable ProjectClickCallback projectClickCallback) {
        this.projectClickCallback = projectClickCallback;
    }

    //現状との差分をListとしてRecyclerViewにセットする
    public void setProjectList(final List<? extends Project> projectList) {

        if (this.projectList == null) {
            this.projectList = projectList;

            //positionStartの位置からitemCountの範囲において、データの変更があったことを登録されているすべてのobserverに通知する。
            notifyItemRangeInserted(0, projectList.size());
        } else {
            //2つのListの差分を計算するユーティリティー。Support Library 24.2.0で追加された。
            DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                @Override
                public int getOldListSize() {
                    return ProjectAdapter.this.projectList.size();
                }

                @Override
                public int getNewListSize() {
                    return projectList.size();
                }

                @Override
                public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                    return ProjectAdapter.this.projectList.get(oldItemPosition).id == projectList.get(newItemPosition).id;
                }

                @Override
                public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                    Project project = projectList.get(newItemPosition);
                    Project old = projectList.get(oldItemPosition);

                    return project.id == old.id && Objects.equals(project.git_url, old.git_url);
                }
            });
            this.projectList = projectList;

            //DiffUtilのメソッド=>差分を元にRecyclerViewAderpterのnotify系が呼ばれ、いい感じにアニメーションなどをやってくれます。
            result.dispatchUpdatesTo(this);
        }
    }

    //継承したインナークラスのViewholderをレイアウトとともに生成
    //bindするビューにコールバックを設定 -> ビューホルダーを返す
    @Override
    public ProjectViewHolder onCreateViewHolder(ViewGroup parent, int viewtype) {
        ProjectListItemBinding binding = DataBindingUtil
                .inflate(LayoutInflater.from(parent.getContext()), R.layout.project_list_item, parent, false);

        binding.setCallback(projectClickCallback);

        return new ProjectViewHolder(binding);
    }

    //ViewHolderをDataBindする
    @Override
    public void onBindViewHolder(ProjectViewHolder holder, int position) {
        holder.binding.setProject(projectList.get(position));
        holder.binding.executePendingBindings();
    }

    //リストのサイズを返す
    @Override
    public int getItemCount() {
        return projectList == null ? 0 : projectList.size();
    }

    //インナークラスにViewHolderを継承し、project_list_item.xml に対する Bindingを設定
    static class ProjectViewHolder extends RecyclerView.ViewHolder {

        final ProjectListItemBinding binding;

        public ProjectViewHolder(ProjectListItemBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }
}

ProjectFragment


こちらは詳細ページ用Fragment、一覧と同じくbindするViewを紐付けます。
先ほど定義したFactoryクラス(DI)はここで使われています。

ProjectFragment.java
public class ProjectFragment extends Fragment {

    private static final String KEY_PROJECT_ID = "project_id";
    private FragmentProjectDetailsBinding binding;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        // DataBinding対象のレイアウトをinflateする
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_project_details, container, false);

        return binding.getRoot();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        //DI
        ProjectViewModel.Factory factory = new ProjectViewModel.Factory(
                getActivity().getApplication(), getArguments().getString(KEY_PROJECT_ID)
        );

        //project_idをキーに注入してViewModelインスタンスを取得
        final ProjectViewModel viewModel = ViewModelProviders.of(this, factory).get(ProjectViewModel.class);

        //ViewにViewModelをセット
        binding.setProjectViewModel(viewModel);
        //app:visibleGone="@{isLoading}"をtrueに
        binding.setIsLoading(true);

        //データ監視を開始 -> 差分を監視して、ViewModelに伝える
        observeViewModel(viewModel);

    }

    //Modelのデータを監視するメソッド
    public void observeViewModel(final ProjectViewModel viewModel){
       viewModel.getObservableProject().observe(this, new Observer<Project>() {
           @Override
           public void onChanged(@Nullable Project project) {
               if (project != null){
                   binding.setIsLoading(false);

                   viewModel.setProject(project);
               }
           }
       });
    }

    // Activityでの遷移の際にidをFragmentToFragmentで渡す
    public static ProjectFragment forProject(String projectID) {
        ProjectFragment fragment = new ProjectFragment();
        Bundle args = new Bundle();

        args.putString(KEY_PROJECT_ID, projectID);
        fragment.setArguments(args);

        return fragment;
    }
}

そして、データバインディング。
こっちはViewModelを直接指定してあげます。

fragmant_project_details.xml
<data>
    <variable name="isLoading" type="boolean" />
    <variable name="projectViewModel" type="com.example.XXXXXXXX.easyclient_mvvm.viewModel.ProjectViewModel"/>
</data>

すると、ViewModelを通し、Repositoryを介してAPIから取得したデータを、バインドします。

fragmant_project_details.xml
<TextView
    android:id="@+id/name"
    //…
    android:text="@{projectViewModel.project.name}"
    />

<TextView
    android:id="@+id/project_desc"
    //…
    android:text="@{projectViewModel.project.description}"/>

<TextView
    android:id="@+id/languages"
    //…
    android:text="@{String.format(@string/languages, projectViewModel.project.language)}"/>

<TextView
    android:id="@+id/project_watchers"
    //…
    android:text="@{String.format(@string/watchers, projectViewModel.project.watchers)}"/>

<TextView
    android:id="@+id/project_open_issues"
    //…
    android:text="@{String.format(@string/openIssues, projectViewModel.project.open_issues)}"/>

<TextView
    android:id="@+id/project_created_at"
    //…
    android:text="@{String.format(@string/created_at, projectViewModel.project.created_at)}"/>

<TextView
    android:id="@+id/project_updated_at"
    //…
    android:text="@{String.format(@string/updated_at, projectViewModel.project.updated_at)}"/>

<TextView
    android:id="@+id/clone_url"
    //…
    android:text="@{String.format(@string/clone_url, projectViewModel.project.clone_url)}"/>

まとめ

すごく理にかなった、すっきりとした設計だなと思っております。
ですが、学習コスト、実装コストが少し高いような気もしています。

ViewModelsとLiveData:パターン+アンチパターン

自社で足並みそろえてスケールさせていくプロジェクトに最初から導入すると、実装範囲も分担しやすくソースコードの品質も統一でき(個人の色が出にくい)、何よりライフサイクルに気を使ってあれやこれやと、力技のオンパレードからのメモリリークがあーだこーだとか、Commonな最強の巻物の如しActivity/Utilを読み進めるとかしなくて良くなるのはとても良いなと思います。成長させていくアプリケーションにはとてもいい設計なんだろうなと思いました。

ただ反対に、スピード感求められる少人数での受託プロジェクトには、学習コストも高く、足並みを揃えるのに時間を取られてしまいそうで、合わないような気もします。頻繁にメンバーが変わるプロジェクトだと、この設計を理解せず入ってしまい、結局Activity肥大化してるしViewModel形骸化してるし、同じようなUtilがたくさんあるよ、みたいな事態にもなりそうです。

設計を理解した人が、責任もって最後まで見守ることが大事ですね。(雑感)

まだ理解していない部分も多々あるので、ツッコミ、編集リクエスト歓迎しております。
長々と読んでいただき、ありがとうございました!