素振りの一環でTwitterクライアントのサンプル を作っています。(WIP)
starをもらえると喜びます。
github.com
そんなときに@AndroidDev のツイートで、Navigation Architecture Component(以下Navigation)を使った際のログインのCase Studyが紹介されていたので、Twitter クライアントにも必要であろうと実装してみました。
ログイフロー概説
Navigationを用いる場合、まずはナビゲーショングラフに開始のディスティネーションを設定する必要があります。
ログインを必須とするアプリの場合、開始のディスティネーションにログイン画面を設定したくなりますが、これはよくないとされています。
(記事末にこのときの弊害について記載しておきます。)
ナビゲーションの原則に従いながら、ログイン画面への遷移を実現するためには、各画面から必要なときのみログイン画面に遷移する方法を取るのが好ましいようです。
詳しくはAndroid DevelopersのYouTube動画 を御覧ください。
VIDEO www.youtube.com
実装
前提として私が試した環境はこちらです。
サンプルアプリ概略
一般的な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ではディープリンクの場合、手動ナビゲーションをシミュレーションする という性質があります。
つまり、リスト画面とリスト要素の詳細画面を持つアプリの場合、ディープリンク 経由で詳細画面に遷移した場合も、バックスタックにはリスト画面も含まれているということになります。(詳しくは公式ドキュメントのナビゲーションの原則 をご参照ください)
これにより、ログイン画面を開始ディスティネーションに設定した場合、ディープリンク でアプリ内のコンテンツにアクセスした際に、ログイン画面がバックスタックに積まれることになってしまいます。
もちろん、ログイン済みかどうかの判定をしてここの調整をするとは思いますが、少し煩雑になりますし、ナビゲーションの原則に沿っていません。
References