compose에서 제공하는 navigation 방식으로 composable들 사이를 이동할 때 사용할 수 있다. 공식문서는 이 링크에서 확인할 수 있다.
두 번째 작업으로 navigation을 택한 이유는 이 navigation까지 로직을 짜 두면 이제는 정말 "만들고 싶은 것을 만들 수 있는 상태"가 된다고 생각했기 때문이다.
실제로 navigation-compose 자체를 적용하는 것은 매우 간단하다. 앞서 언급한 공식문서만 참고해도 바로 적용이 가능하다. 작업에서도 적용 자체는 간단했다. 관련 commit을 남긴다.
위 commit과 같은 상태까지만 작업을 한다해도 동작상의 문제는 없다. 하지만 Screen 구조가 복잡해지고, project가 커질 경우(이 프로젝트엔 해당하지 않을 수 있겠지만) 큰 문제점이 하나 생긴다.
이를 해결할 방법을 찾다 Navigator Class를 Custom하게 만드는 방식을 찾았다. 이 방식의 경우 Stream-slack-clone을 참고했으며, 최소한으로 필요한 부분만 추려서 작성했다.
간단히 말해 Navigation에 대한 정보를 담는 class를 만들었다~ 정도이다. 여기서는 Navigation에는 NavigateUp과 NavigateToRoute 두 종류가 있겠구나~ 하면 된다.
sealed class NavigationCommand {
object NavigateUp : NavigationCommand()
}
sealed class ComposeNavigationCommand : NavigationCommand() {
data class NavigateToRoute(val route: String, val options: NavOptions? = null) :
ComposeNavigationCommand()
}
이 부분은 나도 아직은 Flow에 대한 추가적인 이해가 있어야 100% 이해할 수 있을 것 같은 부분으로, 지금은 절반정도 이해했다고 생각한다. 각 요소들을 간략하게 나마 순서대로 설명해보자면
abstract class Navigator {
val navigationCommands = MutableSharedFlow<NavigationCommand>(extraBufferCapacity = Int.MAX_VALUE)
val navControllerFlow = MutableStateFlow<NavController?>(null)
fun navigateUp() {
navigationCommands.tryEmit(NavigationCommand.NavigateUp)
}
}
abstract class ComposeNavigator : Navigator() {
abstract fun navigate(route: String, optionsBuilder: (NavOptionsBuilder.() -> Unit)? = null)
suspend fun handleNavigationCommands(navController: NavController) {
navigationCommands
.onSubscription { this@ComposeNavigator.navControllerFlow.value = navController }
.onCompletion { this@ComposeNavigator.navControllerFlow.value = null }
.collect { navController.handleComposeNavigationCommand(it)}
}
private fun NavController.handleComposeNavigationCommand(navigationCommand: NavigationCommand) {
when (navigationCommand) {
is ComposeNavigationCommand.NavigateToRoute -> {
navigate(navigationCommand.route, navigationCommand.options)
}
is NavigationCommand.NavigateUp -> navigateUp()
}
}
}
ComposeNavigator의 구현부가 되는 부분으로 위에서 미처 구현되지 않았던 fun navigate의 구현을 볼 수 있다.
class MovieAppNavigator @Inject constructor() : ComposeNavigator() {
override fun navigate(route: String, optionsBuilder: (NavOptionsBuilder.() -> Unit)?) {
val options = optionsBuilder?.let { navOptions(it) }
navigationCommands.tryEmit(ComposeNavigationCommand.NavigateToRoute(route, options))
}
}
지난 글에도 나왔던 hilt를 통한 의존성 주입 과정이다. "provideComposeNavigator"는 어디선가 ComposeNavigator를 필요로하면 이 코드를 통해서 만들어가십쇼~ 하는 의미이며, 구현부를 파라미터로 넘겨주고 있는 것을 볼 수 있다.
따라서 해당 코드는 "ComposeNavigator의 구현은 MovieAppNavigator입니다."라고 말해주는 코드다.
@Module
@InstallIn(SingletonComponent::class)
abstract class NavigationModule {
@Binds
@Singleton
abstract fun provideComposeNavigator(movieAppNavigator: MovieAppNavigator): ComposeNavigator
}
navigation 관련 파트를 AppContent로 분리했고, MainActivity에서는 ComposeNavigator를 주입받아 AppContent에 넘겨준다.
## MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var composeNavigator: ComposeNavigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MovieAppTheme {
AppContent(composeNavigator)
}
}
}
## AppContent.kt
@Composable
fun AppContent(
composeNavigator: ComposeNavigator
) {
val navController = rememberNavController()
LaunchedEffect(Unit) {
composeNavigator.handleNavigationCommands(navController) // navController 등록
}
NavHost(
navController = navController,
startDestination = "home"
) {
composable(
route = "home"
) {
CompositionLocalProvider(
provideHomeViewModelFactory { hiltViewModel<RealHomeViewModel>() }
) {
HomeScreen()
}
}
}
}
이로써 navigation 구조도 작업이 마무리되었고. 이번 글과 관련된 코드는 이 branch에서 확인 가능하다.
이제는 원하는 것을 그려나가기 위한 기초 정도는 갖춰졌다고 생각한다. 따라서 사용하려 했던 API를 좀 탐구해보고, 어떤 걸 만들 수 있을지 고민해보려고 한다.
추가적으로 이번 작업에 많이 참고했던 stream-slack-clone-android github을 다시 한번 적어둔다.
참고가 되었습니다! 감사합니다!