오늘도 더 나은 코드를 작성하였습니까?

Conditional navigation(조건부 네비게이션) 본문

Android Jetpack Architecture/Navigation

Conditional navigation(조건부 네비게이션)

hik14 2021. 11. 26. 19:38

앱에 대한 탐색을 디자인할 때 조건부 논리를 기반으로 한 destination과 다른 destination을 탐색할 수 있습니다.

예를 들어 사용자가 로그인해야 하는 destination에 대한 딥 링크를 따라갈 수 있거나 플레이어가 이기거나 지는 게임에서 다른 destination이 있을 수 있습니다.

 

User login

사용자가 인증이 필요한 프로필 화면으로 이동하려고 합니다.  인증이 필요하므로 사용자가 아직 인증되지 않은 경우 로그인 화면으로 리디렉션되어야 합니다.

로그인하려면 앱에서 사용자 이름과 비밀번호를 입력하여 인증할 수 있는 login_fragment로 이동해야 합니다

 

로그인이되면 사용자는 profile_fragment 화면으로 다시 전송됩니다.

로그인이되지 않으면 스낵바를 사용하여 자격 증명이 유효하지 않음을 사용자에게 알립니다.

사용자가 로그인하지 않고 프로필 화면으로 돌아가면 main_fragment 화면으로 보내집니다.

 

<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/nav_graph"
        app:startDestination="@id/main_fragment">
    <fragment
            android:id="@+id/main_fragment"
            android:name="com.google.android.conditionalnav.MainFragment"
            android:label="fragment_main"
            tools:layout="@layout/fragment_main">
        <action
                android:id="@+id/navigate_to_profile_fragment"
                app:destination="@id/profile_fragment"/>
    </fragment>
    <fragment
            android:id="@+id/login_fragment"
            android:name="com.google.android.conditionalnav.LoginFragment"
            android:label="login_fragment"
            tools:layout="@layout/login_fragment"/>
    <fragment
            android:id="@+id/profile_fragment"
            android:name="com.google.android.conditionalnav.ProfileFragment"
            android:label="fragment_profile"
            tools:layout="@layout/fragment_profile"/>
</navigation>

MainFragment에는 사용자가 프로필을 보기 위해 클릭할 수 있는 버튼이 포함되어 있습니다.

사용자가 프로필 화면을 보려면 먼저 인증해야 합니다

 

두 개의 fragment 을 사용하여 모델링되지만 사용자 상태에 따라 다릅니다.

 

1. login_fragment  --> profile_fragment

2. profile_fragment

 

이 상태 정보는  두개의  fragment 중 하나의 책임이 아니며 sharedViewModel인 UserViewModel에 더 적절하게 보관됩니다.

 

UserViewModel은 ViewModelStoreOwner를 implement하는 Activity를 범위를 지정하여 fragment 간에 공유됩니다

 

 MainActivity가 ProfileFragment를 호스팅하기 때문에 requireActivity()가 MainActivity가 된다.

class ProfileFragment : Fragment() {
    private val userViewModel: UserViewModel by activityViewModels()
    ...
}

 

 

UserViewModel의 user Data는 LiveData를 통해 노출되고, login_fragment / profile_fragment 중 어디로 이동할지를 결정하려면 user Data를 observe해야 합니다.

 

ProfileFragment로 이동할 때 user Data가 있는 경우 앱에 환영 메시지가 표시됩니다.user Data 가 null이면 사용자가 프로필을 보기 전에 인증해야 하므로 LoginFragment로 이동합니다.

 

 ProfileFragment에서 어디로 갈지 결정하는 로직을  작성한다.

class ProfileFragment : Fragment() {
    private val userViewModel: UserViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val navController = findNavController()
        userViewModel.user.observe(viewLifecycleOwner, Observer { user ->
            if (user != null) {
                showWelcomeMessage()
            } else {
                navController.navigate(R.id.login_fragment)
            }
        })
    }

    private fun showWelcomeMessage() {
        ...
    }
}

 

ProfileFragment에 도달할 때 user data가 null이면 LoginFragment로 리디렉션됩니다.

 

NavController.getPreviousBackStackEntry() : previous destionation에 대한 NavBackStackEntry(destination 에 대한 NavController 관련 상태를 캡슐화하여 보관)를 가져올수 있음.

 

LoginFragment는 previous NavBackStackEntry의 SavedStateHandle을 사용하여 사용자가 성공적으로 로그인했는지 여부를 나타내는 초기 값을 설정합니다.

 

사용자가 즉시 system back button 누르면 반환하는 상태입니다.

SavedStateHandle을 사용하여 이 상태를 설정하면 프로세스가 종료된 후에도 상태가 유지됩니다

class LoginFragment : Fragment() {
    companion object {
        const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL"
    }

    private val userViewModel: UserViewModel by activityViewModels()
    private lateinit var savedStateHandle: SavedStateHandle

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle
        savedStateHandle.set(LOGIN_SUCCESSFUL, false)
    }
}

사용자가 id과 pw를 입력하면 인증을 위해 UserViewModel에 전달됩니다.

인증에 성공하면 UserViewModel은 사용자 데이터를 저장합니다.

 

그런 다음 LoginFragment는 SavedStateHandle의 LOGIN_SUCCESSFUL 값을 업데이트하고 back stack에서 popUp시킨다. 

class LoginFragment : Fragment() {
    companion object {
        const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL"
    }

    private val userViewModel: UserViewModel by activityViewModels()
    private lateinit var savedStateHandle: SavedStateHandle

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle
        savedStateHandle.set(LOGIN_SUCCESSFUL, false)

        val usernameEditText = view.findViewById(R.id.username_edit_text)
        val passwordEditText = view.findViewById(R.id.password_edit_text)
        val loginButton = view.findViewById(R.id.login_button)

        loginButton.setOnClickListener {
            val username = usernameEditText.text.toString()
            val password = passwordEditText.text.toString()
            login(username, password)
        }
    }

    fun login(username: String, password: String) {
        userViewModel.login(username, password).observe(viewLifecycleOwner, Observer { result ->
            if (result.success) {
                savedStateHandle.set(LOGIN_SUCCESSFUL, true)
                findNavController().popBackStack()
            } else {
                showErrorMessage()
            }
        })
    }

    fun showErrorMessage() {
        // Display a snackbar error message
    }
}

인증과 관련된 모든 논리는 UserViewModel 내에서 유지됩니다. 

사용자 인증 방법을 결정하는 것은 LoginFragment 또는 ProfileFragment의 책임이 아니기 때문에 이것은 중요합니다.

ViewModel에서 로직을 캡슐화하면 공유하기 쉬울 뿐만 아니라 테스트하기도 더 쉽습니다.

  navigation logic이 복잡한 경우 특히 테스트를 통해 이 논리를 확인해야 합니다.

 

ProfileFragment로 돌아가서 SavedStateHandle에 저장된 LOGIN_SUCCESSFUL 값은 onCreate() 메서드에서 관찰할 수 있습니다. 사용자가 ProfileFragment로 돌아오면 LOGIN_SUCCESSFUL 값이 확인됩니다.

값이 false이면 사용자를 MainFragment로 다시 리디렉션할 수 있습니다.

class ProfileFragment : Fragment() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navController = findNavController()

        val currentBackStackEntry = navController.currentBackStackEntry!!
        val savedStateHandle = currentBackStackEntry.savedStateHandle
        savedStateHandle.getLiveData<Boolean>(LoginFragment.LOGIN_SUCCESSFUL)
                .observe(currentBackStackEntry, Observer { success ->
                    if (!success) {
                        val startDestination = navController.graph.startDestination
                        val navOptions = NavOptions.Builder()
                                .setPopUpTo(startDestination, true)
                                .build()
                        navController.navigate(startDestination, null, navOptions)
                    }
                })
    }

    ...
}

사용자가 성공적으로 로그인하면 ProfileFragment에 환영 메시지가 표시됩니다.

 

 결과를 확인하는 데 사용된 기술을 사용하면 두 가지 다른 경우를 구별할 수 있습니다.

 - 사용자가 로그인되어 있지 않고 로그인을 요청해야 하는 경우

 - 사용자가 로그인하지 않기로 선택했기 때문에 로그인되지 않았습니다(false의 결과)

 

 usecase를 잘 구분하면 사용자에게 반복적으로 로그인을 요청하는 것을 피할 수 있습니다.