【Kotlin】NavigationとFragmentで画面遷移を管理する【Android】

2020.09.02


どうも、むつたくです。
今回はAndroidで使ってみて実際に便利だったと思ったコントローラを紹介します。

 

紹介するコントローラはNavigationです。
SwiftでiOSアプリを開発しているときはStoryBoardがあり、視覚的に画面遷移がわかったので、他の人が作成したプロジェクトでも動きが容易に理解できました。そしてそれをAndroidでもやりたい場合、近しいことを実現するためにNavigationを使用します。

 

Navigationですが、ActivityやFragmentの画面遷移を視覚的に表示できますし、画面間の値渡しを型安全にできる等良いこと尽くしです。

開発環境

  • Mac Catalina 10.15.3
  • Android Studio 3.6.2
  • Xperia 5
  • Android OS 10

 

ここで注意ですが、Nagigationを使用するにあたり、Android Studioのバージョンが「3.3」以上が必要になります。

今回やること

  1. Navigationの特徴
  2. Fragmentの作成
  3. Navigationをプロジェクトに導入
  4. Navgation Graphの作成
  5. Navgation Graphへ適用
  6. 画面遷移の実装
  7. 補足

Navigation Controllerのスタートガイドは こちら になります。

Navagationの特徴

Navigationには以下の特徴があります。

 

  1. Fragmentで画面遷移を実装する場合は、FragmentTransactionの処理をNavigationが行ってくれる。
  2. 画面遷移の処理(進む、戻るアクション)を正しく処理できる
  3. 画面遷移の際に使用するアニメーションを簡単に設定できる
  4. Navigation Controllerを使用しているActivity・Fragmentは型安全に値のやりとりができる
  5. 画面遷移を視覚的に表現することができる
  6. 視覚的に表現できるEditorも簡単に作成できる

 

他にもいろいろ使用することによって恩恵はあります。詳細は公式の こちら で。

 

ただ、原則として以下のことにも注意して下さい。

  1. 起動する画面を必ず1つ決め固定し、戻るアクションでアプリ終了する時も起動画面を通過して終了する
  2. システムナビゲーションバーの「戻る(<)」ボタンと、アプリバーの「上へ」ボタンは同じ機能にする
  3. アプリバーにある「上へ」ボタンではアプリを終了させない

詳細は公式の こちら を参照して下さい。

 

Fragmentの作成

Fragmentを3つ作成します。Fragment名をそれぞれ「FirstFragment」、「SecondFragment」、「ThirdFragment」とします。

FirstFragment.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".ui.FirstFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent">

        <TextView
           android:id="@+id/textFirst"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@string/text_first_fragment"
           android:textSize="30sp"
           android:foregroundTint="@color/colorBlack"
           app:layout_constraintBottom_toBottomOf="parent"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintStart_toStartOf="parent"
           app:layout_constraintTop_toTopOf="parent"
           tools:ignore="MissingConstraints" />

        <Button
           android:id="@+id/buttonNext"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@string/button_text_next"
           android:foregroundTint="@color/colorBlack"
           android:textSize="20sp"
           android:layout_marginEnd="60dp"
           app:layout_constraintBottom_toBottomOf="parent"
           app:layout_constraintEnd_toEndOf="parent"
           app:layout_constraintTop_toBottomOf="@+id/textFirst" />

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

SecondFragment.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.FirstFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/textSecond"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/text_first_fragment"
            android:textSize="30sp"
            android:foregroundTint="@color/colorBlack"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:ignore="MissingConstraints" />

        <Button
            android:id="@+id/buttonBack"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button_text_back"
            android:foregroundTint="@color/colorBlack"
            android:textSize="20sp"
            android:layout_marginStart="60dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textSecond" />

        <Button
            android:id="@+id/buttonNext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button_text_next"
            android:foregroundTint="@color/colorBlack"
            android:textSize="20sp"
            android:layout_marginEnd="60dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textSecond" />

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

ThirdFragment.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".ui.ThirdFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/textThird"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/text_third_fragment"
            android:textSize="30sp"
            android:foregroundTint="@color/colorBlack"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:ignore="MissingConstraints" />

        <Button
            android:id="@+id/buttonBack"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button_text_back"
            android:foregroundTint="@color/colorBlack"
            android:textSize="20sp"
            android:layout_marginStart="60dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textThird" />

        <Button
            android:id="@+id/buttonNext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button_text_top"
            android:foregroundTint="@color/colorBlack"
            android:textSize="20sp"
            android:layout_marginEnd="60dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textThird" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</FrameLayout>

 

Navigationをプロジェクトに導入

では、早速プロジェクトにNavigationを導入していきましょう。
appのbuild.gradleに以下を追加します。

1
2
3
4
5
dependencies {
    def nav_version = "2.3.0"
    implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version"
    implementation "android.arch.navigation:navigation-ui-ktx:$nav_version"
}

バージョンアップのことも考慮してバージョンは変数にしてます。もちろん、変数にせず直接指定するのも問題ないです。
そして「Sync Now」を実行して同期しましょう。
これで導入は完了です。

 

Navgation Graphの作成

次にNavgation Graphを作成します。
[ res or app を右クリック > New > Android Resource File ]を選択します。
もしくは[ ツールバー > File > New > Android Resource File ]を選択します。

そうしましたら[ New Resource File ]ダイアログが表示されますので、以下の様に作成します。
File nameは何でも構いません。
Resource Typeは必ず[ Navigation ]にします。

完了したら[ OK ]ボタンをクリックします。そうしますと、resディレクトリに新たに[ navigation ]ディレクトリが作成され、その中に[ nav_host.xml ]が作成されます。

Navigation Graphへの適用

Fragmentの作成が完了したら、Navigation Graphに作成した3つのFragmentを追記します。
First → Second → Thirdの順に遷移させるように設定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/nav_host"
   app:startDestination="@id/firstFragment">

    <fragment
       android:id="@+id/firstFragment"
       android:name="com.swallow_incubate.navisample.ui.FirstFragment"
       android:label="fragment_first"
       tools:layout="@layout/fragment_first" >
        <action
           android:id="@+id/action_firstFragment_to_secondFragment"
           app:destination="@id/secondFragment" />
    </fragment>

    <fragment
       android:id="@+id/secondFragment"
       android:name="com.swallow_incubate.navisample.ui.SecondFragment"
       android:label="fragment_second"
       tools:layout="@layout/fragment_second" >
        <action
           android:id="@+id/action_secondFragment_to_thirdFragment"
           app:destination="@id/thirdFragment" />
    </fragment>

    <fragment
       android:id="@+id/thirdFragment"
       android:name="com.swallow_incubate.navisample.ui.ThirdFragment"
       android:label="fragment_third"
       tools:layout="@layout/fragment_third" />
</navigation>

 

navigationタグapp:startDestinationに最初に表示するFragmentのIDを設定することを忘れずに。
fragmentタグの中のidは、一意のIDを記載します。またnameに関しては、先ほど作成したFragmentを記述します。そうすることにより、NavigtionとFragmentの紐付けができる様になります。

 

パッケージ名.ディレクトリ名.フラグメント名の様になります。ディレクトリ名(ここではui)は、作成していないと[ com.swallow_incubate.navisample.FirstFragment ]の形になるので、注意してください。

 

fragmentタグ中のactionタグですが、こちらのidも一意となるidを作成します。
destinationには、遷移先の[ fragment id ]を記述します。

画面遷移の実装

main_activity.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/main_nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:navGraph="@navigation/nav_host"
        app:defaultNavHost="true" />
</androidx.constraintlayout.widget.ConstraintLayout>

[ fragment ]タグを追加します。
idはいつもの様に一意のIDを設定します。
nameはNavHost実装のクラス名を入力します。(決まり文句の様なもの)
navGraphには先ほど作成したNavigation GraphのIDを入力します。
defaultNavHostは基本trueで問題ないかと。trueの場合は、Navigation Graph内のスタックされたFragmentに戻ることができ、falseの場合はFragmentがスタックされていても戻ることはできません。一方通行のイメージです。

 

これでアプリを起動すると[ FirstFragment ]が表示されます(app:startDestinationに設定したFragment)。
あとは各Fragmentに遷移処理を記載するだけです。方法は、クリックリスナを作成し、Navigation Graphで作成したactionを記述すればOKです。

 

FirstFragment.kt
1
2
3
4
5
6
7
8
9
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    val view = inflater.inflate(R.layout.fragment_first, container, false)

    view.buttonNext.setOnClickListener {
        findNavController().navigate(R.id.action_firstFragment_to_secondFragment)
    }

    return view
}

FirstFragmentからSecondFragmentに遷移したいので使用するaction idはFirstFragmentとSecondFragmentを結んでいる[ action_firstFragment_to_secondFragment ]になります。

 

SecondFragment.kt
1
2
3
4
5
6
7
8
9
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    val view = inflater.inflate(R.layout.fragment_second, container, false)

    view.buttonNext.setOnClickListener {
        findNavController().navigate(R.id.action_secondFragment_to_thirdFragment)
    }

    return view
}

今度はSecondFragmentからThirdFragmentに遷移したいので、使用するaction idはSecondFragmentとThirdFragmentを結んでいる[ action_secondFragment_to_thirdFragment ]になります。

 

ひとまずこれでFirst→Second→Thridの順に画面遷移が出来ました🙌🎉

前画面に戻る

システムの戻るボタンを使用せずに前画面に戻る

もしかしたら、システムの戻るボタンを使わせたくない時があるかもしれません。あまりないと思いますが・・・。
そんな時は、Navigation Graphのfragmentタグの中に新しいアクションを作成します。
対象はSecondFragmentおよび、ThirdFragmentになります。

nav_host.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    <fragment
       android:id="@+id/secondFragment"
       android:name="com.swallow_incubate.navisample.ui.SecondFragment"
       android:label="fragment_second"
       tools:layout="@layout/fragment_second" >
        <action
           android:id="@+id/action_secondFragment_to_thirdFragment"
           app:destination="@id/thirdFragment" />
        <!-- 下記actionを追加 -->
        <action
           android:id="@+id/action_back_to_firstFragment"
           app:destination="@id/firstFragment" />
    </fragment>
    <fragment
       android:id="@+id/thirdFragment"
       android:name="com.swallow_incubate.navisample.ui.ThirdFragment"
       android:label="fragment_third"
       tools:layout="@layout/fragment_third">
        <!-- 下記actionを追加 -->
        <action
           android:id="@+id/action_back_to_secondFragment"
           app:destination="@id/secondFragment" />
    </fragment>
</navigation>

 

SecondFragment.kt

下記のコードを[ onCreateView ]に追加します。

1
2
3
view.buttonBack.setOnClickListener {
    findNavController().navigate(R.id.action_back_to_firstFragment)
}

 

ThirdFragment.kt
1
2
3
4
5
6
7
8
9
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    val view = inflater.inflate(R.layout.fragment_third, container, false)

    view.buttonBack.setOnClickListener {
        findNavController().navigate(R.id.action_back_to_secondFragment)
    }

    return view
}

これでSecond、ThirdFragmentのBACKボタンをタップすることにより、スタックされた前画面に遷移することができます。ただし、この遷移方法もスタックするので、
[ First(NEXT) > Secound(NEXT) > Third(BACK) > Second ]と遷移し、システムの戻るボタンをタップすると、FirstFragmentではなく、ThirdFragmentに戻ることに注意してください。

システムの戻るボタンで指定したFragmentまで戻る

activity_main.xmlにて指定した[ app:defaultNavHost ]がtrueの場合、システムの戻るボタンをタップすることでスタックされた前画面に戻ることができます。

 

しかし、ここで特定の画面にいる時、指定した画面に戻させたい場合があると思います。ThirdFragmentにいるとき、システムの戻るボタンをタップしてFirstFragmentに戻りたい場合、何も制御しないとSecondFragmentに戻ってしまいます。

 

こういったケースを回避するために、[ popUpTo ]という属性がactionタグにあります。
これは、指定した画面に戻ることができ、今までスタックしてきた画面をすべて破棄することができます。

 

TOP画面→入力画面→入力確認画面→登録完了画面という画面フローがあった場合、登録完了画面からシステムの戻るボタンをタップすると、入力確認画面に戻り、さらに戻るボタンをタップすると入力画面に戻ってしまいます。そう言ったケースを回避し、TOP画面に戻すことができます。

 

nav_host.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
    <fragment
       android:id="@+id/secondFragment"
       android:name="com.swallow_incubate.navisample.ui.SecondFragment"
       android:label="fragment_second"
       tools:layout="@layout/fragment_second" >
        <action
           android:id="@+id/action_secondFragment_to_thirdFragment"
           app:destination="@id/thirdFragment"
           app:popUpTo="@id/firstFragment"/> <!-- ★この行を追加 -->
        <action
           android:id="@+id/action_back_to_firstFragment"
           app:destination="@id/firstFragment" />
    </fragment>

肝は、戻る処理を制御したい画面の前画面で設定することです。この場合、ThirdFragmentからFirstFragmentに戻したいので、ThirdFragmentの前画面である、SecondFragmentのThirdFragmentに遷移させるactionタグに記述します。ちょっとややこしいですね。
前画面で制御することを覚えておけば、大丈夫です。

Global Action

Navigation GraphにすべてのFragmentタグからアクセスできる[ Global Action ]というものがあります。
どこの画面からもアクセスができる画面があった場合に重宝するやり方です。

 

例えば、どの画面からもFirstFragmentに戻りたい場合、今回の様に3枚のFragmentでしたらSecond / Thridに戻るactionを記述すればいいですが(この場合も管理が面倒)、何十枚もあった場合、管理が大変です。このようなケースを回避するために、Global Actionを使用します。
ここでは、ThridFragmentからFirstFragmetに遷移するためのGlobal Actionを作成します。

 

nav_host.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_host"
    app:startDestination="@id/firstFragment">

    <!-- 下記actionを追加 -->
    <action
        android:id="@+id/global_action_back_to_first"
        app:destination="@id/firstFragment"
        app:popUpTo="@id/firstFragment"/>

    <fragment
...
</navigation>

 

ThirdFragment.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    val view = inflater.inflate(R.layout.fragment_third, container, false)

    view.buttonBack.setOnClickListener {
        findNavController().navigate(R.id.action_back_to_secondFragment)
    }

    // 下記リスナ追加
    view.buttonTop.setOnClickListener {
       findNavController().navigate(R.id.global_action_back_to_first) // ここでGlobal Actionを指定する
    }

    return view
}

これでTOPボタンをタップするとFirstFragmentに遷移することができ、またpopUpTo属性があるので、システムの戻るボタンでアプリが終了します。

 

 

補足

ここまで長々と説明しましたが、これまでのことをワンクリックでできる方法があります(Global Actionはなかったですが)。
新規プロジェクトを作成した際に、プロジェクトテンプレートが選択できるダイアログが表示されます。
そのダイアログで[ Basic Activity ]を選択すると、1枚のActivityと、2枚のFragment、そしてそのFragmentの遷移をNavigationで実装したプロジェクトが作成されます!


Navigationを一から作成するのが億劫な人にとっては、有り難いテンプレートですので、こちらも是非覚えておくとちょっと幸せになるかもしれません。

 

 


以上となります。
Navigationを使用して、画面遷移を簡単に実装する方法でした!(管理も楽に!)
それではまたの機会に!


Top