앱 탐색을 설계할 때 조건부 로직에 따라 이동하는 대상을 다르게 할 수 있습니다. 예를 들어 사용자가 로그인해야 하는 대상의 딥 링크를 따라가거나, 게임에서 플레이어의 승패에 따라 이동하는 대상이 여러 개인 경우입니다.
사용자 로그인
이 예에서는 사용자가 인증이 필요한 프로필 화면으로 이동하려고 합니다. 인증이 필요한 작업이므로 아직 인증되지 않은 사용자는 로그인 화면으로 리디렉션해야 합니다.
이 예의 탐색 그래프는 다음과 같을 수 있습니다.

인증하려면 앱에서 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() ... }
자바
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() { ... } }
자바
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) } }
자바
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 } }
자바
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) } }) } ... }
자바
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
의 결과로 로그인하지 않기로 선택했으므로 로그인되어 있지 않습니다.
이러한 사용 사례를 구별하면 사용자에게 반복적인 로그인 요청을 하지 않을 수 있습니다. 실패 사례를 처리하는 비즈니스 로직은 개발자가 결정해야 하며 이전 코드 예의 사례와 같이 여기에는 사용자가 로그인해야 하는 이유를 설명하는 오버레이 표시, 전체 활동 완료 또는 사용자를 로그인하지 않아도 되는 대상으로 리디렉션하는 것이 포함될 수 있습니다.