Android Compose Navigation

Park Jae Hong·2023년 1월 28일
0

Android Compose Navigation 다루기

Android Compose Navigation 을 다루기 위해 좋은 라이브러리 도 있다. 하지만 내부 동작을 이해하기 위해 직접 구현해서 사용 !

Destination

sealed class Destination(
	protected val route: String,
	vararg params: String
)

먼저, 특정 UI을 가기 위한 Destination sealed class 를 만든다.


private fun String.appendParams(vararg params: Pair<String, Any?>): String {
    val builder = StringBuilder(this)

    params.forEach {
        it.second?.toString()?.let { arg ->
            builder.append("/$arg")
        }
    }

    return builder.toString()
}

Android compose navigation 에서는 다음 UI로 이동할 때 "/"를 사용하여 값을 보내기 때문에 위와 같은 함수를 구현해 준다.



sealed class Destination(
	protected val route: String,
	vararg params: String
) {
  val fullRoute: String = if (params.isEmpty()) route else {
        val builder = StringBuilder(route)
        params.forEach { builder.append("/{${it}}") }
        builder.toString()
    }
}

params로 넘어온 값을 "/" 로 구분하여 fullRoute에 추가하는 함수를 구현해준다.


sealed class NoArgumentsDestination(route: String) : Destination(route) {
        operator fun invoke(): String = route
    }

만약 params가 없을 경우에 사용할 sealed class도 구현해 준다.


	object Dynasty : NoArgumentsDestination(DestinationType.DYNASTY.value)
    object MyStudy : NoArgumentsDestination(DestinationType.MY_STUDY.value)
    object StudyType : Destination(DestinationType.STUDY_TYPE.value, "dynastyType") {
        const val DYNASTY_TYPE_KEY = "dynastyType"

        operator fun invoke(dynastyType: String): String = route.appendParams(
            DYNASTY_TYPE_KEY to dynastyType
        )
    }
    object TypeCheck : Destination(DestinationType.TYPE_CHECK.value, "dynastyType") {
        const val DYNASTY_TYPE_KEY = "dynastyType"

        operator fun invoke(dynastyType: String): String = route.appendParams(
            DYNASTY_TYPE_KEY to dynastyType
        )
    }

    object StudyPage : Destination(DestinationType.STUDY_PAGE.value, "dynastyType", "studyType") {
        const val DYNASTY_TYPE_KEY = "dynastyType"
        const val STUDY_TYPE_KEY = "studyType"

        operator fun invoke(dynastyType: String, studyType: String): String = route.appendParams(
            DYNASTY_TYPE_KEY to dynastyType,
            STUDY_TYPE_KEY to studyType,
        )
    }

위와 같이 params 가 필요 없다면 NoArgumentsDestination, 필요있다면 Destination로 해당 UI를 정의해 준다.


  • Interface 구현
interface Navigator {
    val navigationChannel : Channel<NavigationIntent>

    suspend fun navigateBack(
        route: String? = null,
        inclusive: Boolean = false,
    )

    suspend fun navigateTo(
        route: String,
        popUpToRoute: String? = null,
        inclusive: Boolean = false,
        isSingleTop: Boolean = false,
    )

}
sealed class NavigationIntent {
    data class NavigateBack(
        val route: String? = null,
        val inclusive: Boolean = false,
    ) : NavigationIntent()

    data class NavigateTo(
        val route: String,
        val popUpToRoute: String? = null,
        val inclusive: Boolean = false,
        val isSingleTop: Boolean = false,
    ) : NavigationIntent()
}

Channel을 통해서 Intent를 관리하고 특정 UI로 가기위한 navigationTo, 이전 UI로 돌아가기 위한 navigationBack 함수를 정의해 준다.


  • class 구현
class NavigatorImpl : KoreanHistoryNavigator {

    override val navigationChannel = Channel<NavigationIntent>(
        capacity = Int.MAX_VALUE,
        onBufferOverflow = BufferOverflow.DROP_LATEST,
    )

    override suspend fun navigateBack(
        route: String?,
        inclusive: Boolean,
    ) {
        navigationChannel.send(
            NavigationIntent.NavigateBack(
                route = route,
                inclusive = inclusive
            )
        )
    }

    override suspend fun navigateTo(
        route: String,
        popUpToRoute: String?,
        inclusive: Boolean,
        isSingleTop: Boolean,
    ) {
        navigationChannel.send(
            NavigationIntent.NavigateTo(
                route = route,
                popUpToRoute = popUpToRoute,
                inclusive = inclusive,
                isSingleTop = isSingleTop,
            )
        )
    }
}

받아온 params 값으로 NavigationIntent로 만들어서 Channel 로 보낸다.


@Composable
fun NavHost(
    navController: NavHostController,
    startDestination: Destination,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) {
    androidx.navigation.compose.NavHost(
        navController = navController,
        startDestination = startDestination.fullRoute,
        modifier = modifier,
        route = route,
        builder = builder
    )
}

fun NavGraphBuilder.composable(
    destination: Destination,
    arguments: List<NamedNavArgument> = emptyList(),
    deepLinks: List<NavDeepLink> = emptyList(),
    content: @Composable (NavBackStackEntry) -> Unit
) {
    composable(
        route = destination.fullRoute,
        arguments = arguments,
        deepLinks = deepLinks,
        content = content
    )
}

직접 만든 Destination 을 사용하기위해 NavHost의 재정의가 필요하다.


Composable
fun NavigationEffects(
    navigationChannel: Channel<NavigationIntent>,
    navHostController: NavHostController
) {
    val activity = (LocalContext.current as? Activity)
    LaunchedEffect(activity, navHostController, navigationChannel) {
        navigationChannel.receiveAsFlow().collect { intent ->
            if (activity?.isFinishing == true) {
                return@collect
            }
            when (intent) {
                is NavigationIntent.NavigateBack -> {
                    if (intent.route != null) {
                        navHostController.popBackStack(intent.route, intent.inclusive)
                    } else {
                        navHostController.popBackStack()
                    }
                }
                is NavigationIntent.NavigateTo -> {
                    navHostController.navigate(intent.route) {
                        launchSingleTop = intent.isSingleTop
                        intent.popUpToRoute?.let { popUpToRoute ->
                            popUpTo(popUpToRoute) { inclusive = intent.inclusive }
                        }
                    }
                }
            }
        }
    }
}

navigationChannel 에 있는 Intent 값을 확인해서 NavigateBack 이면 popBackStack()을 하고 NavigateTo 이면 navHostController 의 navigate를 통해 특정 UI로 이동해 준다.


사용

val navController = rememberNavController()
    NavigationEffects(
        navigationChannel = homeViewModel.navigationChannel,
        navHostController = navController
    )
    AppTheme {
        NavHost(
            navController = navController,
            startDestination = Destination.Dynasty) {
            composable(Destination.Dynasty) {
                DynastyScreen()
            }
            composable(Destination.MyStudy) {
                MyStudyScreen()
            }
            composable(Destination.StudyType) {
                StudyTypeScreen()
            }
            composable(Destination.StudyPage) {
                StudyPageScreen()
            }
            composable(Destination.TypeCheck) {
                TypeCheckScreen()
            }
        }
    }

MainScreen에서 NavigationEffects 와 NavHost를 정의해준다.
(이때, NavHost는 내가 재정의한 NavHost를 사용한다.

모든 코드 보러가기

profile
The people who are crazy enough to think they can change the world are the ones who do. -Steve Jobs-

0개의 댓글