Navigation component の使い方
概要
Android Kotlin Fundamentals 03.x (Fragment と Navigation) の備忘録です。ポイントとなるコードのみを抜粋しています。
目次
確認環境
- AndroidStudio 3.5
- compileSdkVersion 28
- minSdkVersion 19
- Gradle 5.4.1
- Kotlin 1.3.11
参考情報
- Android Kotlin Fundamentals Course
- 03.1, 03.2, 03.3 の内容から抜粋
解説
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.xml
... <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 のファクトリメソッドに引数が追加される
navdrawer_menu.xml
... <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 の存在を検証し、存在しない場合はメニューを非表示にする