Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Android NavigationとSharedViewModel

More than 1 year has passed since last update.

はじめに

Navigation ComponentはAndroid Jetpackに含まれているコンポーネントです。
AndroidアプリにおけるActivityやFragment間の画面遷移をシンプルに実装することができます。
1つのActivityをホストとし、複数のFragmentを管理するように設計されています。

複数のFragmentで共通する処理をActivityに任せたくなるシーンに出会いませんか?
色々な実現方法があると思いますが、今回はSharedViewModelを用いた方法を紹介します。

言語はKotlinでバージョンは1.3.50です。

概要

Navigationを利用し、FragmentからFragmentへ値渡しをする場合、通常Safe Argsを用いた型安全の値渡しをします。
しかし、ホストしているActivityと各Fragmentで何かを共有する場合工夫が必要です。
また、ToolbarのTitleの変更や、Snackbarの表示は各Fragmentで行うと、同じような処理を複数記述することになるので、まとめたいです。

このような課題をSharedViewModelを用いて解決します。
図で表すとこのようになります。

ActivityでSharedViewModelのLiveDataをobserveし、各FragmentでpostValueするだけでActivityに処理を任せられるようになります。

必要となるライブラリの導入

app/build.gradle
// ...

dependencies {

    // ...

    // Fragment
    implementation "androidx.fragment:fragment:1.2.0-rc01"

    // Lifecycle
    def arch_lifecycle_version = '2.1.0'
    implementation "androidx.lifecycle:lifecycle-runtime:$arch_lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-extensions:$arch_lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-reactivestreams:$arch_lifecycle_version"

    // Navigation
    def arch_navigation_version = '2.2.0-rc01'
    implementation "androidx.navigation:navigation-fragment:$arch_navigation_version"
    implementation "androidx.navigation:navigation-fragment-ktx:$arch_navigation_version"
    implementation "androidx.navigation:navigation-ui:$arch_navigation_version"
    implementation "androidx.navigation:navigation-ui-ktx:$arch_navigation_version"
}

SharedViewModelの作成

今回作成するSharedViewModelは非常にシンプルです。
どのFragmentを表示しているかを共有するためのFragmentType型のMutableLiveDataを保有しています。

SharedViewModel.kt
class SharedViewModel : ViewModel(){
    val fragmentType = MutableLiveData<FragmentType>().apply {
        this.value = FragmentType.FIRST
    }
}

FragmentTypeをenumを用いて下記のように定義しました。

FragmentType
enum class FragmentType(val type: String) {
    FIRST("first"),
    SECOND("second"),
    THIRD("third"),
    FOURTH("fourth")
}

ホストとなるActivityの作成

Navigationを利用する際の各FragmentをホストするActivityを作成します。
今回はSharedViewModelのfragmentTypeが変更された時にSnackbarが表示される処理を入れています。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding =
            DataBindingUtil.setContentView<ActivitySubBinding>(this, R.layout.activity_main)

        val sharedViewModel = ViewModelProviders.of(this).get(SharedViewModel::class.java)

        sharedViewModel.fragmentType.observe(this, Observer { value ->
            value?.let {
                Snackbar.make(
                    findViewById(android.R.id.content), it.type, Snackbar.LENGTH_SHORT
                ).show()
            }
        })
    }
}

SharedViewModelのfragmentTypeをobserveしています。
これにより、Fragment側でSharedViewModelのfragmentTypeをpostValueするだけで、Activity側でSnackbarを表示することができます。

また、レイアウトは下記になります。

activity_main.xml
<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".activity.SubActivity">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/fragment_container_view"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/activity_navigation" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

FragmentContainerViewをコンテナとして利用しています。
このnavGraph属性に次に説明するnavigationレイアウトを指定します。

FragmentContainerViewについて気になる方はこちらの記事をご覧ください。
Android FragmentContainerViewとは

navigationレイアウトの作成

Fragmentを4つ(FirstFragment、SecondFragment、ThirdFragment、FourthFragment)用意しました。
navigation要素のstartDestination属性に最初に表示するfragment要素のidを指定しています。
今回はそれぞれのFragmentが「First→Second」「Second→Third」「Third→Fourth」「Fourth→First」という画面遷移させたかったので、action属性を下記のように記載しています。

activity_navigation
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_navigation"
    app:startDestination="@id/firstFragment">

    <fragment
        android:id="@+id/firstFragment"
        android:name="e.yoppie.sample.fragment.FirstFragment"
        android:label="FirstFragment">

        <action
            android:id="@+id/action_first_to_second"
            app:destination="@id/secondFragment" />

    </fragment>

    <fragment
        android:id="@+id/secondFragment"
        android:name="e.yoppie.sample.fragment.SecondFragment"
        android:label="SecondFragment">

        <action
            android:id="@+id/action_second_to_third"
            app:destination="@id/thirdFragment" />

    </fragment>

    <fragment
        android:id="@+id/thirdFragment"
        android:name="e.yoppie.sample.fragment.ThirdFragment"
        android:label="ThirdFragment">

        <action
            android:id="@+id/action_third_to_fourth"
            app:destination="@id/fourthFragment" />

    </fragment>

    <fragment
        android:id="@+id/fourthFragment"
        android:name="e.yoppie.sample.fragment.FourthFragment"
        android:label="FourthFragment">

        <action
            android:id="@+id/action_fourth_to_first"
            app:destination="@id/firstFragment" />

    </fragment>

</navigation>

デザインタブで見ると下記になります。
スクリーンショット 2019-11-01 0.39.01.png

4つのFragmentの作成

前述のFragmentを作成します。
同じ処理を持つFragmentなのでFirstFragmentのみ説明します。
Fragmentには、次へボタンを押下すると、次のFragmentへ遷移し、
Snackbarボタンを押すと、SharedViewModelのfragmentTypeをpostValueし値を変更する処理を作成しました。

FirstFragment
class FirstFragment : Fragment() {

    private lateinit var sharedViewModel: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedViewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class.java)
    }

    @SuppressLint("CheckResult")
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)

        val binding = DataBindingUtil.inflate<FragmentFirstBinding>(
            inflater,
            R.layout.fragment_first,
            container,
            false
        )
        binding.apply {
            lifecycleOwner = this@FirstFragment
            destinationButton.clicks().subscribe {
                findNavController().navigate(R.id.action_first_to_second)
            }
            snackbarButton.clicks().subscribe {
                sharedViewModel.fragmentType.postValue(FragmentType.FIRST)
            }
        }

        return binding.root
    }
}

Activity側でSharedViewModelのfragmentTypeをobserveしているので、Fragment側ではpostValueするだけでSnackbarが表示されます。

今回作成したアプリの一連の流れ

yoppie_x
Androidに関する本を販売してるのでよかったら覗いてください。 BOOTH「http://tex12.booth.pm」、Kindle「https://www.amazon.co.jp/dp/B07YRH3DFV」
https://yoppiex.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away