Navigation Architecture Componentを使ったログイン機能実装方法
素振りの一環でTwitterクライアントのサンプルを作っています。(WIP)
starをもらえると喜びます。
github.com
そんなときに@AndroidDevのツイートで、Navigation Architecture Component(以下Navigation)を使った際のログインのCase Studyが紹介されていたので、Twitterクライアントにも必要であろうと実装してみました。
🗺️ Sail through Jetpack Navigation
— Android Developers (@AndroidDev) 2020年7月23日
Join @ianhlake as he covers the Jetpack Navigation Architecture Component. In this video, he shares some newly added functionality and dives into a case study on Login. #11WeeksOfAndroid
😎 Find your way → https://t.co/8wL9o6GLWW pic.twitter.com/zHWO7Dl1pU
ログイフロー概説
Navigationを用いる場合、まずはナビゲーショングラフに開始のディスティネーションを設定する必要があります。
ログインを必須とするアプリの場合、開始のディスティネーションにログイン画面を設定したくなりますが、これはよくないとされています。
(記事末にこのときの弊害について記載しておきます。)
ナビゲーションの原則に従いながら、ログイン画面への遷移を実現するためには、各画面から必要なときのみログイン画面に遷移する方法を取るのが好ましいようです。
詳しくはAndroid DevelopersのYouTube動画を御覧ください。
実装
前提として私が試した環境はこちらです。
- Android Studio 4.1 Beta 5
- Navigation 2.3.0
サンプルアプリ概略
一般的なTwitterクライアントを題材にしています。
アプリはタイムラインを表示する画面(以下HomeFragment)があり、こちらはログインをしているユーザのみ見れる想定です。また、今回は開始ディスティネーションをHomeFragmentとしています。
未ログインのユーザにはログイン画面(以下LoginFragment)へ遷移させるようにします。
順を追って説明します。
LoginFragmentへの遷移
未ログインの状態を判定し、ログイン画面へ遷移をさせます。
class HomeFragment : Fragment(R.layout.fragment_home) { ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { ... viewLifecycleOwner.lifecycleScope.launchWhenStarted { viewModel.authUser.collect { user -> if (user == null) navigateLogin() } } } ... }
今回は簡易的に、保存されたログインユーザがnullの場合に遷移する処理を実装しています。 ログインが必須の場合は、ディープリンク経由で遷移できる画面すべてに同様の処理を実装する必要がありそうです。
ログイン結果を遷移元(HomeFragment)に伝える
LoginFragmentは、必要なときに別の画面から呼び出されることを前提として作ります。
Navigation(ver2.3.0)ではpreviousBackStackEntry
を使い、遷移元のFragmentに値を伝えることができるので、これを活用します。
class LoginFragment : Fragment(R.layout.fragment_login) { ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { ... val navController = findNavController() val savedStateHandle = requireNotNull(navController.previousBackStackEntry).savedStateHandle val binding = FragmentLoginBinding.bind(view) binding.loginButton.setOnClickListener { viewModel.saveAuthUser() } viewModel.succeeded.observe(viewLifecycleOwner) { // ログインの成功を前の画面に伝える savedStateHandle.set(KEY_LOGIN_SUCCESSFUL, true) navController.popBackStack() } } ... }
サンプルではログインボタンを押したら適当なAuthUserを保存するようにしていますが、実際には認証処理を行い、その結果の成否によって処理を分ける必要があるでしょう。
ログインの成否を受け取る
HomeFragmentではcurrentBackStackEntry
を使い、先程LoginFragmentがセットした成否の値をもとに処理を行います。
class HomeFragment : Fragment(R.layout.fragment_home) { ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { ... val navController = findNavController() val savedStateHandle = requireNotNull(navController.currentBackStackEntry).savedStateHandle savedStateHandle.getLiveData<Boolean>(KEY_LOGIN_SUCCESSFUL) .observe(viewLifecycleOwner) { success -> // 成否の結果から処理を行う if (success) viewModel.getTweets() savedStateHandle.remove<Boolean>(KEY_LOGIN_SUCCESSFUL) } } ... }
アプリの性質によって成否の処理はうまく処理しましょう。
今回はログインが成功したときのみ前の画面に戻るように実装しているので、失敗したときの処理は実装していません。
開始ディスティネーションにログイン画面を設定したときの弊害
アプリの多くは、ディープリンクによってWebサイトやSNSアプリなどからアプリに遷移させることが多いかと思います。
Navigationでもディープリンクをハンドリングする機構が整っています。そして、Navigationではディープリンクの場合、手動ナビゲーションをシミュレーションするという性質があります。
つまり、リスト画面とリスト要素の詳細画面を持つアプリの場合、ディープリンク経由で詳細画面に遷移した場合も、バックスタックにはリスト画面も含まれているということになります。(詳しくは公式ドキュメントのナビゲーションの原則をご参照ください)
これにより、ログイン画面を開始ディスティネーションに設定した場合、ディープリンクでアプリ内のコンテンツにアクセスした際に、ログイン画面がバックスタックに積まれることになってしまいます。
もちろん、ログイン済みかどうかの判定をしてここの調整をするとは思いますが、少し煩雑になりますし、ナビゲーションの原則に沿っていません。