Androidアプリ開発初級編(Navigation)

今回は画面遷移を制御することができるNavigationについて学習します。udacityの本編ではaction barについても触れられていますが、今回は長くなるため省略とさせてください。気になる方はudacityを受けてみてください。

■事前準備

新しいEmpty Activityのプロジェクトを作成し、

次にbuild.gradle(プロジェクト)を編集します

    dependencies {
        classpath "com.android.tools.build:gradle:4.1.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        def nav_version = "2.3.2"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }

次にアプリ側のbuild.gradleを編集します

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id "androidx.navigation.safeargs"
}

android {
    ....
      dataBinding {
        enabled = true
    }
    ....
}

....

dependencies {
    ....
    def nav_version = "2.3.2"
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

■サンプルアプリの動作

■Navigationエディタを使用する

早速Navigationに入りたいところですが、まずはFragmentをA,B,C,Dの4つ作成します。

次にresディレクトリを右クリックしてNew Resource File から Resource typeからNavigationを選択して任意の名前でnavigationリソースを作成します。

res -> layout -> 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=".MainActivity">

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

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

<fragment>はその名の通りフラグメントを表示するものになります。ここで重要なのは

android:name=”androidx.navigation.fragment.NavHostFragment”

app:navGraph=”@navigation/navigation”

このように記載することで、NavigationControllerクラスを利用してNavigationリソースに定義した画面遷移を制御できるようになることです。

そして

app:defaultNavHost=”true”

このapp:defaultNavHostがtrueであればバックキーによりnavigationをハンドリングできますが、falseであれば影響を受けなくなりますので注意してください。

ここまで来たらNavigationを開いてエディタを使用しましょう。

下記のように画面遷移を定義してみてください。

argumentの設定を遷移先のフラグメントに設定することで値を渡すことが出来ます。(Bundle形式)

それではNavigationエディタで編集された下記のnavigation.xmlを見てみましょう。

<?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/navigation"
    app:startDestination="@id/fragmentA">

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.tomostudy.navigationtest.FragmentA"
        android:label="FragmentA" >
        <action
            android:id="@+id/action_fragmentA_to_fragmentB"
            app:destination="@id/fragmentB" />
    </fragment>
    <fragment
        android:id="@+id/fragmentB"
        android:name="com.tomostudy.navigationtest.FragmentB"
        android:label="FragmentB" >
        <action
            android:id="@+id/action_fragmentB_to_fragmentC"
            app:destination="@id/fragmentC"
            app:popUpTo="@id/fragmentB"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_fragmentB_to_fragmentD"
            app:destination="@id/fragmentD" />
    </fragment>
    <fragment
        android:id="@+id/fragmentC"
        android:name="com.tomostudy.navigationtest.FragmentC"
        android:label="FragmentC" >
        <argument
            android:name="destination_about"
            app:argType="string" />
    </fragment>
    <fragment
        android:id="@+id/fragmentD"
        android:name="com.tomostudy.navigationtest.FragmentD"
        android:label="FragmentD" >
        <argument
            android:name="destination_about"
            app:argType="string"
            android:defaultValue="not value" />
    </fragment>
</navigation>

<navigation>のstartDestinationが開始するフラグメントのidを指定しています。<fragment>の中で<action>が定義されており、こちらが画面遷移のアクションでdestinationのパラメーターで画面遷移先のidであるfragmentBを指定しています。

nameにはどのフラグメントかをパッケージ名+クラス名で指定しています。

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation"
    app:startDestination="@id/fragmentA">

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.tomostudy.navigationtest.FragmentA"
        android:label="FragmentA" >
        <action
            android:id="@+id/action_fragmentA_to_fragmentB"
            app:destination="@id/fragmentB" />
    </fragment>

fragmentBの<action>は新たにpopUpToが指定されています。popUpToで指定されたfragmentは遷移先のfragmentの戻り先となります。

ですが、popUpToInclusiveでtrueを指定すると、popUpToで指定したフラグメントの一つ前のフラグメントに戻ります。つまりfragmentAに戻ることになります。

adb コマンドでbackstackを確認するとわかりますが、fragmentCに遷移した際にfragmentBが無いことが分かると思います。

逆にfragmentDに遷移する際はすべてがスタックに積まれており、順番に戻ります。

    <fragment
        android:id="@+id/fragmentB"
        android:name="com.tomostudy.navigationtest.FragmentB"
        android:label="FragmentB" >
        <action
            android:id="@+id/action_fragmentB_to_fragmentC"
            app:destination="@id/fragmentC"
            app:popUpTo="@id/fragmentB"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_fragmentB_to_fragmentD"
            app:destination="@id/fragmentD" />
    </fragment>

fragmentCには<argumnet>が設定されています。つまりfragmentBからの遷移時に値を受け取ることが可能です。typeはbundleで渡せる範囲内のものになります。

    <fragment
        android:id="@+id/fragmentC"
        android:name="com.tomostudy.navigationtest.FragmentC"
        android:label="FragmentC" >
        <argument
            android:name="destination_about"
            app:argType="string" />
    </fragment>

fragmentDでは<argument>にdefaultValueとして初期値の指定が追加されています。実際の値を渡す部分の処理については次のセクションで説明します。

    <fragment
        android:id="@+id/fragmentD"
        android:name="com.tomostudy.navigationtest.FragmentD"
        android:label="FragmentD" >
        <argument
            android:name="destination_about"
            app:argType="string"
            android:defaultValue="not value" />
    </fragment>

■Navigation処理の実装

activityは今回特に何も編集していません。実際の画面遷移の起点となるFragmentAから見ていきます。

class FragmentA : Fragment() {
    lateinit var binding: FragmentABinding
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_a, container, false)
        binding.nextButton.setOnClickListener {
            it.findNavController().navigate(FragmentADirections.actionFragmentAToFragmentB())
        }
        return binding.root
    }
}

viewからfindNavControllerでナビゲーションコントローラを取得し、navigateメソッドにFragmentADirections.actionFragmentAToFragmentB()とすることで画面遷移のアクションを指定しています。

次にFragmentBです。

class FragmentB : Fragment() {
    lateinit var binding: FragmentBBinding
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_b, container, false)
        binding.nextButton.setOnClickListener {
            doNextFragment(it)
        }
        return binding.root
    }

    private fun doNextFragment(view: View) {
        binding.apply {
            when (radioGroup.checkedRadioButtonId) {
                R.id.select_c -> view.findNavController()
                    .navigate(FragmentBDirections.actionFragmentBToFragmentC("pop backstack Fragment B"))
                R.id.select_d -> view.findNavController()
                    .navigate(
                        FragmentBDirections.actionFragmentBToFragmentD()
                            .setDestinationAbout("backstack Fragment B")
                    )
                else -> view.findNavController()
                    .navigate(
                        FragmentBDirections.actionFragmentBToFragmentD()
                    )
            }
        }
    }
}

doNextFragmentメソッドが画面遷移処理の本体になっており、ラジオボタンで選択されたfragmentに遷移するようになっています。FragmentBDirectionsのアクションに引数を指定することでargumentの値を渡しています。

actionFragmentBtoFragmentDでは初期値がありますので引数無し、もしくはsetDestinationAboutに値を設定することで任意の値を渡すことが出来ます。

argmentは複数指定できますので、その際は値の数に応じて引数が増えます。

次にFragmentCです。

class FragmentC : Fragment() {
    val args: FragmentCArgs by navArgs()
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val binding: FragmentCBinding =
            DataBindingUtil.inflate(inflater, R.layout.fragment_c, container, false)
        binding.resultText.text = args.destinationAbout
        return binding.root
    }
}

args.destinationAboutとすることで先ほど引数で渡された値を取得できます。

こちらはfragmentのインスタンスが生成されargsが設定された後に使用可能ですので注意してください。その前にアクセスするとIllegalStateExceptionが投げられます。

FragmentDはFragmentCと変わらないので割愛します。

■まとめ

・androidx.navigation.fragment.NavHostFragment を指定する

・app:navGraphにnavigationリソースを指定する

・Navigationエディタを駆使して画面遷移やargument、backStackの制御を視覚的に行うことが可能

一見、Navigationは面倒なように感じますがエディタを使用することで画面遷移を視覚的に構築できるのは、コード上で管理するよりも物凄く楽です。Navigationを使っていろいろな画面遷移を試してみてはいかがでしょうか。

それでは。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です