LoginSignup
82
53

More than 3 years have passed since last update.

[Android] NavigationでSafeArgsを使って引数付き画面遷移をする

Posted at

SafeArgsとは

みなさん,ActivityやFragment間の遷移で引数を受け渡しする時はどうしているでしょうか.
まだBundleを使っている方もいるかもしれませんね.

Navigation Componentの1機能であるSafeArgsを使えば,型安全な引数の受け渡しが実現できます.みんな使っているからいっぱい記事があるかと思いきや,alphaやbetaの記事ばかりなのでstable版としてまとめておきたいと思います.
(あれから便利な拡張関数がサイレントで登場したりしている)

Navigationの基本的な使い方は理解している前提から始めますので,微妙という方は以下の記事からキャッチアップしてきてください!

[Android] 10分で作る、Navigationによる画面遷移
https://qiita.com/tktktks10/items/7df56b4795d907a4cd31

作るもの

最終的な実装は下記レポジトリに置いてあります.

tks10/safeargs-example
https://github.com/tks10/safeargs-example

仕様はミニマムな感じでいきましょう.
MainActivityの上にFragmentを乗せて遷移をしていく,1Activtity多Fragmentなかんじです.
FirstFragmentにはEditTextとButtonがあり,ButtonをおしたらEditTextの内容とともにSecondFragmentに遷移してその内容を表示します.

左の画面でNextを押すと,右に画面になるやつですね.
 → 

ではやっていきましょう.

事前準備

以下をgradleに追記.

project/build.gradle
buildscript {
    ...
    dependencies {
        ...
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"
    }
}
app/build.gradle
apply plugin: "androidx.navigation.safeargs"

dependencies {
    ...
    implementation "android.arch.navigation:navigation-fragment:1.0.0"
    implementation "android.arch.navigation:navigation-ui:1.0.0"
    implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0"
    implementation "android.arch.navigation:navigation-ui-ktx:1.0.0"
}

初期状態

1から作っていては冗長なので,各Fragmentとnavigationグラフの初期配置が完了した時点から始めます.ちなみに現時点でnavigationグラフは下記のようになっており,FirstFragmentからSecondFragmentにただの遷移をするだけのactionが1つだけ定義されている状況です.

navigation_graph.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"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/navigation_graph"
        app:startDestination="@id/firstFragment">

    <fragment
            android:id="@+id/firstFragment"
            android:name="com.tks10.safeargsexample.FirstFragment"
            android:label="fragment_first"
            tools:layout="@layout/fragment_first">

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

    </fragment>

    <fragment
            android:id="@+id/secondFragment"
            android:name="com.tks10.safeargsexample.SecondFragment"
            android:label="fragment_second"
            tools:layout="@layout/fragment_second">
    </fragment>

</navigation>

FirstFragmentではSecondFragmentに遷移する処理だけを記述した状態です.

FirstFragment.kt
class FirstFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_first, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        view.nextButton.setOnClickListener {
            findNavController().navigate(R.id.action_first_to_second)
        }
    }
}

また,FisrtFragmentとSecondFragmentのxmlは下記のようになっています.

fragment_fisrt.xml
<?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"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/contentEditText"
            android:layout_width="240dp"
            android:layout_height="wrap_content"
            android:gravity="center"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/nextButton"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintVertical_chainStyle="packed"/>

    <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/nextButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Next"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@id/contentEditText"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
fragment_second.xml
<?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"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/resultTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

当然この状態では引数の受け渡しは行なっていないので,遷移しても何も表示されません.

実装

それではこれを改造してsafeargsを使っていきましょう!
safeargsを使うにはnavigationグラフにargumentタグを追加することから始めます.

この時argumentタグは,引数を受け取る側に追加します.下記のようにSecondFragment内にargumentタグを追加しましょう.この時指定するのは,変数名を示すandroid:nameと型を示すandroid:argTypeの2つです.

navigation_graph.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"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/navigation_graph"
        app:startDestination="@id/firstFragment">

    <fragment
            android:id="@+id/firstFragment"
            android:name="com.tks10.safeargsexample.FirstFragment"
            android:label="fragment_first"
            tools:layout="@layout/fragment_first">

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

    </fragment>

    <fragment
            android:id="@+id/secondFragment"
            android:name="com.tks10.safeargsexample.SecondFragment"
            android:label="fragment_second"
            tools:layout="@layout/fragment_second">

        <!-- ココに定義する -->
        <argument
                android:name="content"
                app:argType="string"/>

    </fragment>

</navigation>

なんとnavigationグラフの変更はこれで終わりです.
それでは次に行く・・・,前にRebuild Projectをしておきましょう.
現状DataBindingのようにリアルタイムにコードが生成されず,自動生成系のクラスが見つからん!などとハマります.

さて,RebuildがおわったらFirstFragmentをいじっていきます.先ほどはただnavigateしていただけの部分を,以下のように書き換えてみましょう.

FirstFragment.kt
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        view.nextButton.setOnClickListener {
            // EditTextの中身を取り出す
            val content = view.contentEditText.text.toString()

            // 生成されたクラスに引数を渡して遷移
            val action = FirstFragmentDirections.actionFirstToSecond(content)
            findNavController().navigate(action)
        }
    }

先ほどargumentタグをSecondFragment側に追加したことにより,SecondFragmentをDestinationとするactionのクラスとメソッドが自動生成されているはずです.今回だと,
FirstFragmentDirections#actionFirstToSecond
ですね.actionタグにつけたidから自動的にメソッド名をつけてくれます.このメソッドの引数に先ほど追加したargumentを受け取るようになっていることがわかります.これで実行時に名前が違って落ちた...なんてことがなくなりますね.

さてここで満足して終わりそうですが,引数を受け取る側の処理が残っています.
SecondFragmentで引数を受け取ってみましょう.といっても,実は1行で終わります.

それではSecondFragmentの頭の方に以下の1行を加えてみましょう.

SecondFragment.kt
class SecondFragment : Fragment() {
    private val args: SecondFragmentArgs by navArgs()

    ....
}

navArgs()というFragmentに対する拡張関数が気がついたら提供されており,
args.content
みたいなかんじで受け取った引数にアクセスできるようになっています.

あとはこれをTextViewにセットしてあげるだけです.

SecondFragment.kt
class SecondFragment : Fragment() {
    private val args: SecondFragmentArgs by navArgs()

    ....

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.resultTextView.text = args.content
    }
}

お疲れ様です!

複数の引数を指定する

なるほどわかった,でも複数渡すことだって多いでしょ?
と突っ込んきた人のために書くこととしましょう.

先ほど受け取った引数に加えてもう1つ値をEditTextに入力し,計2つの引数を次のFragmentに渡したいと思います.全く同じだとなんなので,整数にしましょう.下記のような感じです.

 → 

流れは先ほどと全く同じで,目的地であるThirdFragment側にargumentタグを追加します.何も考えずに並列に追加するだけで問題ありません.そして忘れずにRebuildをします

あと,SecondFragmentからThirdFragmentへのaction定義も忘れないようにしましょう.

navigation_graph.xml

    ...
    <fragment
            android:id="@+id/secondFragment"
            android:name="com.tks10.safeargsexample.SecondFragment"
            android:label="fragment_second"
            tools:layout="@layout/fragment_second">

        <argument
                android:name="content"
                app:argType="string"/>

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

    </fragment>

    <fragment
            android:id="@+id/thirdFragment"
            android:name="com.tks10.safeargsexample.ThirdFragment"
            android:label="fragment_third"
            tools:layout="@layout/fragment_third">

        <argument
                android:name="content"
                app:argType="string"/>
        <argument
                android:name="value"
                app:argType="integer"/>

    </fragment>

そうしたら,SecondFragmentを改造していきます.viewのxmlは以下のようになり,しれっとEditTextとButtonを追加しています.

fragment_second.xml
<?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"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/resultTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:layout_marginBottom="120dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/valueEditText"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintVertical_chainStyle="packed"/>

    <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/valueEditText"
            android:layout_width="240dp"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:inputType="number"
            app:layout_constraintTop_toBottomOf="@id/resultTextView"
            app:layout_constraintBottom_toTopOf="@id/nextButton"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

    <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/nextButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Next"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@id/valueEditText"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

さて,プログラムの方は以下のように改造してみましょう.

SecondFragment.kt
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view.resultTextView.text = args.content
        view.nextButton.setOnClickListener {
            val content = args.content
            val value = view.valueEditText.text.toString().toInt()

            // argumentを増やすと,このメソッドの引数も対応して増える
            val action = SecondFragmentDirections.actionSecondToThird(content, value)
            findNavController().navigate(action)
        }
    }

シンプルですね.actionのメソッドが受け取る引数が素直に増えます.
ここにEditTextから取得した値を渡してあげましょう.

そして忘れずに受け取る側も書きます.
xmlとプログラムは下記の通りです.

ThirdFragment.kt
class ThirdFragment : Fragment() {
    private val args: ThirdFragmentArgs by navArgs()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_third, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        view.contentTextView.text = args.content
        view.valueTextView.text = args.value.toString()
    }
}
fragment_third.xml
<?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"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/contentTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/valueTextView"
            app:layout_constraintHorizontal_chainStyle="packed"/>

    <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/valueTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:layout_marginStart="16dp"
            app:layout_constraintBaseline_toBaselineOf="@id/contentTextView"
            app:layout_constraintStart_toEndOf="@id/contentTextView"
            app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

お疲れ様です!

自作クラスを指定する

ん,じゃぁ自作で作ったdata classはどうするんだと.では書きましょう.
結論から言うと,Parcelableを継承していればsafeargsを利用することができます.

では今回は,下記のクラスをsafeargsで渡すことを考えてみましょう.

MyData.kt
data class MyData(
    val content: String,
    val value: Int,
    val message: String
)

完成予想図はこんな感じです.(messageには適当に文字列をいれています)

 → 

まずはじめに,このクラスにParcelableを実装しましょう,,,と言ってもめんどくさいので,@Parcelizeアノテーションを使います.Experimentalをenableにしましょう.

app/build.gradle
android {
    ...
    androidExtensions {
        experimental = true
    }
}

MyDataに対して以下のようにアノテーションをつけます.

MyData.kt
@Parcelize
data class MyData(
    val content: String,
    val value: Int,
    val message: String
): Parcelable

あとはこれまでの流れと同じになります.まずはnavigationグラフに追加しましょう.

navigation_graph.xml
    ...
    <fragment
            android:id="@+id/thirdFragment"
            android:name="com.tks10.safeargsexample.ThirdFragment"
            android:label="fragment_third"
            tools:layout="@layout/fragment_third">

        <argument
                android:name="content"
                app:argType="string"/>
        <argument
                android:name="value"
                app:argType="integer"/>

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

    </fragment>

    <fragment
            android:id="@+id/fourthFragment"
            android:name="com.tks10.safeargsexample.FourthFragment"
            android:label="fragment_fourth"
            tools:layout="@layout/fragment_fourth">

        <argument
                android:name="myData"
                app:argType="com.tks10.safeargsexample.MyData"/>

    </fragment>

プリミティブなクラス(stringとかintegerとか)以外はフルパスでargTypeを指定してあげましょう.たとえばParcelableを実装しているBitmapとかも渡せたりします.

もうお察しでしょうが,ThirdFragmentでは以下のように記述します.

ThirdFragment.kt
class ThirdFragment : Fragment() {
    private val args: ThirdFragmentArgs by navArgs()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_third, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        view.contentTextView.text = args.content
        view.valueTextView.text = args.value.toString()

        view.nextButton.setOnClickListener {
            val myData = MyData(
                args.content,
                args.value,
                "LGTM!!"
            )

            val action = ThirdFragmentDirections.actionThirdToFourth(myData)
            findNavController().navigate(action)
        }
    }
}

受け取るFourthFragmentは以下のようになります.
(xmlは省略しますが,messageを表示するTextViewが増えただけです)

FourthFragment.kt
class FourthFragment : Fragment() {
    private val args: FourthFragmentArgs by navArgs()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_fourth, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val myData = args.myData

        view.contentTextView.text = myData.content
        view.valueTextView.text = myData.value.toString()
        view.messageTextView.text = myData.message
    }
}

おわりに

以上,SafeArgsによる引数の受け渡しでした.複数の受け渡しや自作クラスの受け渡しもできるようになったかと思います.
これまでalphaやbetaでコロコロと記述が変わりましたが,ようやくstableになって固定された感があります.
型安全になるだけでもかなりメリットですし,navigationグラフをみたときに引数が一覧できるのも良いと思います.
ぜひ既存のプロジェクトでも部分的に取り入れていきたいところですね.

82
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
82
53