앱 탐색을 설계할 때 조건부 로직에 따라 이동하는 대상을 다르게 할 수 있습니다. 예를 들어 사용자가 로그인해야 하는 대상의 딥 링크를 따라가거나, 게임에서 플레이어의 승패에 따라 이동하는 대상이 여러 개인 경우입니다.
사용자 로그인
이 예에서는 사용자가 인증이 필요한 프로필 화면으로 이동하려고 합니다. 인증이 필요한 작업이므로 아직 인증되지 않은 사용자는 로그인 화면으로 리디렉션해야 합니다.
이 예의 탐색 그래프는 다음과 같을 수 있습니다.
![로그인 흐름은 앱의 기본 탐색 흐름과 별도로 처리됩니다.](https://developer.android.google.cn/static/images/guide/navigation/navigation-conditional-login.png?authuser=5&hl=ko)
인증하려면 앱에서 login_fragment
로 이동해야 합니다. 여기서 사용자는 사용자 이름과 비밀번호를 입력하여 인증을 받을 수 있습니다. 승인되면 사용자를 profile_fragment
화면으로 다시 보냅니다. 승인되지 않으면 Snackbar
를 사용해 사용자에게 사용자 인증 정보가 잘못되었다고 알립니다.
사용자가 로그인하지 않고 프로필 화면으로 다시 이동하면 사용자를 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
에는 사용자가 클릭하여 프로필을 볼 수 있는 버튼이 포함되어 있습니다.
사용자가 프로필 화면을 보려면 먼저 인증을 받아야 합니다. 이 상호작용은 개별 프래그먼트 두 개를 사용하여 모델링되지만 공유 사용자 상태에 따라 달라집니다. 이 상태 정보는 두 프래그먼트 중 하나의 소관이 아니며 공유 UserViewModel
에 더 적절하게 보관됩니다.
이 ViewModel
은 ViewModelStoreOwner
를 구현하는 활동으로 범위를 지정하여 프래그먼트 간에 공유됩니다. 다음 예에서는 MainActivity
에서 ProfileFragment
를 호스팅하므로 requireActivity()
가 MainActivity
로 확인됩니다.
Kotlin
class ProfileFragment : Fragment() { private val userViewModel: UserViewModel by activityViewModels() ... }
Java
public class ProfileFragment extends Fragment { private UserViewModel userViewModel; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class); ... } ... }
UserViewModel
의 사용자 데이터는 LiveData
를 통해 노출되므로 이동할 위치를 결정하려면 이 데이터를 관찰해야 합니다. ProfileFragment
로 이동하면 앱은 사용자 데이터가 있는 경우 환영 메시지를 표시합니다. 사용자 데이터가 null
이면 사용자가 프로필을 보기 전에 인증되어야 하므로 LoginFragment
로 이동합니다. 다음 예와 같이 ProfileFragment
에서 결정 로직을 정의합니다.
Kotlin
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() { ... } }
Java
public class ProfileFragment extends Fragment { private UserViewModel userViewModel; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class); final NavController navController = Navigation.findNavController(view); userViewModel.user.observe(getViewLifecycleOwner(), (Observer<User>) user -> { if (user != null) { showWelcomeMessage(); } else { navController.navigate(R.id.login_fragment); } }); } private void showWelcomeMessage() { ... } }
사용자 데이터가 ProfileFragment
에 도달할 때 null
이면 LoginFragment
로 리디렉션됩니다.
NavController.getPreviousBackStackEntry()
를 사용하여 이전 대상의 NavBackStackEntry
를 검색할 수 있으며 이는 NavController
관련 대상 상태를 캡슐화합니다. LoginFragment
는 이전 NavBackStackEntry
의 SavedStateHandle
을 사용하여 사용자가 성공적으로 로그인했는지 나타내는 초깃값을 설정합니다. 이는 사용자가 즉시 시스템 뒤로 버튼을 누르면 반환할 상태입니다. SavedStateHandle
을 사용하여 이 상태를 설정하면 프로세스가 종료될 때까지 상태가 계속 유지됩니다.
Kotlin
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) } }
Java
public class LoginFragment extends Fragment { public static String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL" private UserViewModel userViewModel; private SavedStateHandle savedStateHandle; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class); savedStateHandle = Navigation.findNavController(view) .getPreviousBackStackEntry() .getSavedStateHandle(); savedStateHandle.set(LOGIN_SUCCESSFUL, false); } }
사용자가 사용자 이름과 비밀번호를 입력하면 인증을 위해 UserViewModel
로 전달됩니다. 인증에 성공하면 UserViewModel
에서 사용자 데이터를 저장합니다. 그러면 LoginFragment
는 SavedStateHandle
의 LOGIN_SUCCESSFUL
값을 업데이트하고 백 스택에서 사라집니다.
Kotlin
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 } }
Java
public class LoginFragment extends Fragment { public static String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL" private UserViewModel userViewModel; private SavedStateHandle savedStateHandle; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class); savedStateHandle = Navigation.findNavController(view) .getPreviousBackStackEntry() .getSavedStateHandle(); savedStateHandle.set(LOGIN_SUCCESSFUL, false); EditText usernameEditText = view.findViewById(R.id.username_edit_text); EditText passwordEditText = view.findViewById(R.id.password_edit_text); Button loginButton = view.findViewById(R.id.login_button); loginButton.setOnClickListener(v -> { String username = usernameEditText.getText().toString(); String password = passwordEditText.getText().toString(); login(username, password); }); } private void login(String username, String password) { userViewModel.login(username, password).observe(viewLifecycleOwner, (Observer<LoginResult>) result -> { if (result.success) { savedStateHandle.set(LOGIN_SUCCESSFUL, true); NavHostFragment.findNavController(this).popBackStack(); } else { showErrorMessage(); } }); } private void showErrorMessage() { // Display a snackbar error message } }
인증과 관련된 모든 로직은 UserViewModel
내에 보관됩니다. 사용자가 인증되는 방법을 결정하는 것은 LoginFragment
또는 ProfileFragment
의 책임이 아니므로 이는 중요합니다. 로직을 ViewModel
로 캡슐화하면 공유하기 쉬울 뿐 아니라 테스트도 쉬워집니다. 탐색 로직이 복잡하면 특별히 테스트를 통해 이 로직을 확인해야 합니다. 테스트 가능한 구성요소를 중심으로 앱 아키텍처를 구성하는 방법을 자세히 알아보려면 앱 아키텍처 가이드를 참고하세요.
다시 ProfileFragment
에서 SavedStateHandle
에 저장된 LOGIN_SUCCESSFUL
값을 onCreate()
메서드로 관찰할 수 있습니다. 사용자가 ProfileFragment
로 돌아오면 LOGIN_SUCCESSFUL
값을 확인합니다. 값이 false
이면 사용자는 MainFragment
로 다시 리디렉션될 수 있습니다.
Kotlin
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) } }) } ... }
Java
public class ProfileFragment extends Fragment { ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); NavController navController = NavHostFragment.findNavController(this); NavBackStackEntry navBackStackEntry = navController.getCurrentBackStackEntry(); SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); savedStateHandle.getLiveData(LoginFragment.LOGIN_SUCCESSFUL) .observe(navBackStackEntry, (Observer<Boolean>) success -> { if (!success) { int startDestination = navController.getGraph().getStartDestination(); NavOptions navOptions = new NavOptions.Builder() .setPopUpTo(startDestination, true) .build(); navController.navigate(startDestination, null, navOptions); } }); } ... }
사용자가 성공적으로 로그인하면 ProfileFragment
에서 환영 메시지를 표시합니다.
여기서 결과를 확인하는 데 사용된 기법을 통해 두 가지 사례를 구별할 수 있습니다.
- 사용자가 로그인되어 있지 않고 로그인 요청을 받아야 하는 초기 사례입니다.
- 사용자가
false
의 결과로 로그인하지 않기로 선택했으므로 로그인되어 있지 않습니다.
이러한 사용 사례를 구별하면 사용자에게 반복적인 로그인 요청을 하지 않을 수 있습니다. 실패 사례를 처리하는 비즈니스 로직은 개발자가 결정해야 하며 이전 코드 예의 사례와 같이 여기에는 사용자가 로그인해야 하는 이유를 설명하는 오버레이를 표시하는 것, 전체 활동을 완료하는 것 또는 사용자를 로그인하지 않아도 되는 대상으로 리디렉션하는 것이 포함될 수 있습니다.