Navigation component の使い方

概要

Android Kotlin Fundamentals 03.x (Fragment と Navigation) の備忘録です。ポイントとなるコードのみを抜粋しています。

目次

確認環境

  • AndroidStudio 3.5
    • compileSdkVersion 28
    • minSdkVersion 19
  • Gradle 5.4.1
  • Kotlin 1.3.11

参考情報

解説

build.gradle

buildscript {
    ext {
        kotlin_version = '1.3.11'
        archLifecycleVersion = '1.1.1'
        gradleVersion = '3.5.0'
        supportlibVersion = '1.0.0-rc03'
        navigationVersion = '1.0.0-rc02'
        dataBindingCompilerVersion = gradleVersion // Always need to be the same.
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradleVersion"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
    }
}
  • Safe Args Gradle plugin を classpath に追加

app/build.gradle

...
apply plugin: 'androidx.navigation.safeargs'
...
dependencies {
    ...
    implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion"
    implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion"
    ...
}
  • Safe Args Gradle plugin を有効化
  • Navigation components 関連のライブラリを追加

activity_main.xml

...
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/drawerLayout"
        ...>

        <!-- サンプルでは LinearLayout の子になっている -->
        <fragment
            android:id="@+id/myNavHostFragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            app:navGraph="@navigation/navigation"
            app:defaultNavHost="true"
            ... />

        <com.google.android.material.navigation.NavigationView
            android:id="@+id/navView"
            android:layout_gravity="start"
            app:headerLayout="@layout/nav_header"
            app:menu="@menu/navdrawer_menu"
            ... />

    </androidx.drawerlayout.widget.DrawerLayout>
</layout>
  • Drawer を使用しない場合は DrawerLayout は不要
  • Navigation components を使って Fragment を切り替えるため NavHostFragment を使用
  • defaultNavHost を true に設定した Fragment が Back ボタンをインターセプト
  • NavigationView は Drawer に表示するナビゲーション
...
<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"
    app:startDestination="@id/titleFragment">

    <fragment
        android:id="@+id/titleFragment"
        android:name="com.example.android.navigation.TitleFragment"
        android:label="fragment_title"
        tools:layout="@layout/fragment_title" >
        <action
            android:id="@+id/action_titleFragment_to_gameFragment"
            app:destination="@id/gameFragment" />
    </fragment>

    <fragment
        android:id="@+id/gameFragment"
        android:name="com.example.android.navigation.GameFragment"
        android:label="fragment_game"
        tools:layout="@layout/fragment_game" >
        <action
            android:id="@+id/action_gameFragment_to_gameOverFragment"
            app:destination="@id/gameOverFragment"
            app:popUpTo="@+id/gameFragment"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_gameFragment_to_gameWonFragment"
            app:destination="@id/gameWonFragment"
            app:popUpTo="@+id/gameFragment"
            app:popUpToInclusive="true" />
    </fragment>

    <fragment
        android:id="@+id/gameOverFragment"
        android:name="com.example.android.navigation.GameOverFragment"
        android:label="fragment_game_over"
        tools:layout="@layout/fragment_game_over" >
        <action
            android:id="@+id/action_gameOverFragment_to_gameFragment"
            app:destination="@id/gameFragment"
            app:popUpTo="@+id/titleFragment"
            app:popUpToInclusive="false" />
    </fragment>

    <fragment
        android:id="@+id/gameWonFragment"
        android:name="com.example.android.navigation.GameWonFragment"
        android:label="fragment_game_won"
        tools:layout="@layout/fragment_game_won" >
        <action
            android:id="@+id/action_gameWonFragment_to_gameFragment"
            app:destination="@id/gameFragment"
            app:popUpTo="@+id/titleFragment"
            app:popUpToInclusive="false" />
        <argument
            android:name="numQuestions"
            app:argType="integer" />
        <argument
            android:name="numCorrect"
            app:argType="integer" />
    </fragment>

    <fragment
        android:id="@+id/aboutFragment"
        android:name="com.example.android.navigation.AboutFragment"
        android:label="fragment_about"
        tools:layout="@layout/fragment_about" />
    <fragment
        android:id="@+id/rulesFragment"
        android:name="com.example.android.navigation.RulesFragment"
        android:label="fragment_rules"
        tools:layout="@layout/fragment_rules" />
</navigation>
  • tools:layout は Design editor でレイアウトを表示するために指定
  • app:popUpTo は Back 時の遷移先
  • app:popUpToInclusive が false の場合は popUoTo の Fragment に遷移する
  • app:popUpToInclusive が true の場合は popUoTo の Fragment もバックスタックから除かれ、もう一つ前の Fragment に遷移する
    • titleFragment → gameFragment → gameOverFragment と順にスタックに積まれているので、popUpTo=gameFragment , inclusive=true だと titleFragment に遷移する
  • Safe Args が有効の場合は XXXFragmentDirections クラス (NavDirections を実装) が生成される
    • argument を指定すると XXXFragmentArgs クラスが生成され、遷移元の NavDirections のファクトリメソッドに引数が追加される
...
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/rulesFragment"
        android:title="@string/rules" />
    <item
        android:id="@+id/aboutFragment"
        android:title="@string/about" />
</menu>
  • item の id と navigation.xml で指定した fragment の id を合わせるとライブラリが良きに計らってくれる

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var drawerLayout: DrawerLayout

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        drawerLayout = binding.drawerLayout
        val navController = findNavController(R.id.myNavHostFragment)
        NavigationUI.setupWithNavController(binding.navView, navController)
        NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
    }

    override fun onSupportNavigateUp(): Boolean {
        return NavigationUI.navigateUp(findNavController(R.id.myNavHostFragment), drawerLayout)
    }
}
  • setupWithNavController はスライド操作で Drawer を表示するために必要
  • setupActionBarWithNavController はアクションバーで Drawer の表示を行うために必要
  • onSupportNavigateUp メソッドはアクションバーで戻る操作を行うために必要

TitleFragment.kt

class TitleFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val binding = DataBindingUtil.inflate<FragmentTitleBinding>(inflater, R.layout.fragment_title, container, false)
        binding.playButton.setOnClickListener { view ->
            view.findNavController().navigate(TitleFragmentDirections.actionTitleFragmentToGameFragment())
        }
        setHasOptionsMenu(true)
        return binding.root
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.options_menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        return NavigationUI.onNavDestinationSelected(item!!, view!!.findNavController())
                || super.onOptionsItemSelected(item)
    }
}
  • navigate メソッドの引数は navigation.xml における action の id でもよい
  • Safe Args を使用している場合は navigate メソッドの引数に NavDirections を使用できる
  • onNavDestinationSelected は menu の item id から navigation の fragment id を特定して遷移する

GameFragment.kt

class GameFragment : Fragment() {
    ...

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        ...
                        view.findNavController().navigate(
                                GameFragmentDirections.actionGameFragmentToGameWonFragment(
                                        numQuestions,
                                        questionIndex
                                ))
        ...
    }

    ...
}
  • Safe Args を使用しており、navigation の fragment に argument が指定されている場合には NavDirections のファクトリメソッドに引数が追加される

GameWonFragment.kt

class GameWonFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        ...
        val args = GameWonFragmentArgs.fromBundle(arguments!!)
        Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()
        setHasOptionsMenu(true)
        return binding.root
    }

    private fun getShareIntent() : Intent {
        val args = GameWonFragmentArgs.fromBundle(arguments!!)
        return Intent(Intent.ACTION_SEND)
                .setType("text/plain")
                .putExtra(Intent.EXTRA_TEXT, getString(R.string.share_success_text, args.numCorrect, args.numQuestions))
    }

    private fun shareSuccess() {
        startActivity(getShareIntent())
    }

    override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater?.inflate(R.menu.winner_menu, menu)
        if (getShareIntent().resolveActivity(activity!!.packageManager) == null) {
            menu?.findItem(R.id.share)?.isVisible = false
        }
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item?.itemId) {
            R.id.share -> shareSuccess()
        }
        return super.onOptionsItemSelected(item)
    }
  • XXXFragmentArgs.fromBundle を使えば型安全を得た状態で値を受け取れる
  • onCreateOptionsMenu では ACTION_SEND に対応する Activity の存在を検証し、存在しない場合はメニューを非表示にする