MVVMでNavigationViewの表示を切り替える方法

Android

はじめに

いきなりですが、

Viewとの通信はデータバインディング機構のような仕組みを通じて行うため、ViewModelの変更は開発者から見て自動的にViewに反映される。

引用元:https://ja.wikipedia.org/wiki/Model_View_ViewModel

さて、MVVMではViewへの表示をDataBindingになるべく任せたいですね。
ですが、Androidには様々なViewが存在します。
DataBindingを使うことで、Viewへの表示がとても簡単になりましたが、一部のViewはそのまま値を渡すことが難しいです。
ex) RecyclerView, NavigationView, TabLayoutなど

今回はNavigationViewへの反映についてご紹介します。

ユーザーのログイン状態でメニューの表示を切り替える

今回のサンプルでは、TwitterKitを利用してログイン中ならユーザー情報を表示させます。

イメージ画像です。

Android_login Android_image

では、コードを見ていきましょう。まずはViewModelです。

ViewModel

class TopViewModel {

    val mRepository: UserRepository = UserRepository()

    var user: ObservableField<User> = ObservableField()

    fun getUser() {
        launch(UI) {
            val session = Twitter.getSessionManager().activeSession
            if (session != null) {
                val (status, user) = async(context + CommonPool) { mRepository.getUser(session) }.await()
                when (status) {
                    Status.SUCCESS -> {
                        this@TopViewModel.user.set(user)
                    }
                }
            }
        }
    }
}

getUserでRepositoryからユーザー情報を取得します。
ログインされていない状態では、sessionがnullなので、userも空の状態となります。

また、userをObservableFieldにすることで、値が変化した際にViewに反映されるようにします。

ヘッダーレイアウト

NavigationViewのヘッダー部分のレイアウトです。こちらもDataBindingに表示を任せます。

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >

    <data>

        <variable
            name="viewModel"
            type="com.helmos.app.features.top.TopViewModel"
            />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >

        <ImageView
            android:id="@+id/user_background_image"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:scaleType="centerCrop"
            app:imageUrl="@{viewModel.user != null ? viewModel.user.bannerUrl : ``}"
            />

        <View
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/baseBlack_alpha2"
            />

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:layout_marginBottom="8dp"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            >

            <ImageView
                android:id="@+id/user_profile_image"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_marginEnd="8dp"
                android:scaleType="centerCrop"
                app:imageUrl="@{viewModel.user != null ? viewModel.user.imageUrlHttps : ``}"
                />

            <TextView
                android:id="@+id/user_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_toEndOf="@id/user_profile_image"
                android:text="@{viewModel.user != null ? viewModel.user.name : ``}"
                android:textColor="@color/baseWhite"
                android:textStyle="bold"
                />

            <TextView
                android:id="@+id/user_screen_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/user_name"
                android:layout_toEndOf="@id/user_profile_image"
                android:text="@{viewModel.user != null ? `@` + viewModel.user.screenName : ``}"
                android:textColor="@color/baseWhite"
                />
        </RelativeLayout>
    </FrameLayout>
</layout>

メニュー

NavigationViewのメニュー部分です。いつも通りな感じで。

<menu
    xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/menu_image_search"
            android:icon="@android:drawable/ic_menu_gallery"
            android:title="@string/image_search"
            />
    </group>
    <item android:title="@string/other">
        <menu android:checkableBehavior="single">
            <item
                android:id="@+id/menu_license"
                android:title="@string/license"
                />
            <item
                android:id="@+id/menu_login"
                android:title="@string/login"
                />
            <item
                android:id="@+id/menu_logout"
                android:title="@string/logout"
                android:visible="false"
                />
        </menu>
    </item>
</menu>

Activityのレイアウト

NavigationViewの部分のみ記載します。その他はご自由に。

   <android.support.design.widget.NavigationView
       android:id="@+id/main_navigation"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_gravity="start"
       app:headerLayout="@layout/navigation_header"
       app:menu="@menu/drawer_menu"
       app:topNavigation="@{viewModel.user}"
       />

BindingAdapterを作る

NavigationViewの表示を切り替えるためにBindingAdapterを作ります。

デフォルトのDataBinding機構だと足りない箇所を補ってあげるイメージです。

object NavigationViewBindingAdapter {
    @JvmStatic
    @BindingAdapter("bind:topNavigation")
    fun setupTopNavigation(view: NavigationView, user: User?) {
        view.menu.findItem(R.id.menu_login).isVisible = user == null
        view.menu.findItem(R.id.menu_logout).isVisible = user != null
    }
}

BindingAdapterを利用して、メニューのログイン・ログアウトの表示を切り替えます。

まとめ

公式のDataBindingによって、AndroidでもMVVMを採用しやすくなったと感じます。
しかし、HTMLなどと違い、Viewによってはクラスを別に作成する必要があったり(Recyclerなど)と、そのままではバインドできないことが多いです。
そういった箇所で、BindingAdapterを利用して、ViewModelからViewへの反映を自動でさせられるようにしましょう。

ViewModelにViewを持たせて、Viewのメソッドを呼んでしまうと、MVPチックになってしまうので注意しましょう。(体験談)