それがいいことの序章です

楽しいことならいっぱい夢見ることならめいっぱい

Navigation Architecture Componentを使ったログイン機能実装方法

素振りの一環でTwitterクライアントのサンプルを作っています。(WIP)
starをもらえると喜びます。 github.com

そんなときに@AndroidDevのツイートで、Navigation Architecture Component(以下Navigation)を使った際のログインのCase Studyが紹介されていたので、Twitterクライアントにも必要であろうと実装してみました。

ログイフロー概説

Navigationを用いる場合、まずはナビゲーショングラフに開始のディスティネーションを設定する必要があります。
ログインを必須とするアプリの場合、開始のディスティネーションにログイン画面を設定したくなりますが、これはよくないとされています。
(記事末にこのときの弊害について記載しておきます。)

ナビゲーションの原則に従いながら、ログイン画面への遷移を実現するためには、各画面から必要なときのみログイン画面に遷移する方法を取るのが好ましいようです。

詳しくはAndroid DevelopersのYouTube動画を御覧ください。

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