Single Activity Architecture 이른바 SAA는 단일 혹은 아주 적은 개수의 Activity
만 사용하고 모두 Fragment
로 화면을 구성하는 App 구조를 말한다.
그리고 주로 Jetpack Navigation과 함께 사용되는 구조이다.
위 내용은 구글 I/O 2018에서 발표된 내용으로 실제로 Jetpack Navigation 관련 문서를 찾아보면 Fragment 탐색에 초점을 두고 있는 것을 알 수 있다.
만약 본인이 만들고 있는 앱이 여러개의 Activity
간 전환이 일어난다면 Navigation은 좋은 선택지가 아니다.
그렇다면 SAA 구조를 사용하는가? SAA 구조를 사용하면서 얻을 수 있는 장점은 다음과 같다.
Fragment
가 Activity
보다 상대적으로 가볍다.Fragment
는 하나의 Activity
안에 여러개가 표시될 수 있다. 따라서 좀 더 동적인 UI를 구현할 수 있다. Fragment
를 나눠 UI 관심사를 분리할 수 있다.위와 같이 Activity
에 비해 Fragment
가 갖는 다양한 이점들 때문에 이런 장점을 적극 활용하기 위해서 나온 개념이 바로 Single Activity Architecture(SAA)라고 할 수 있다.
사실 Jetpack Navigation에 대해서는 이전에도 한번 포스트를 쓴 적이 있다. 하지만 그때 포스트 내용이 부실하기도 하니 이번에 다시 한번 제대로 정리해보기로 했다.
Navigation은 크게 3가지 요소로 이루어져 있다.
NavHost
: 현재 탐색 대상(navigation destination)이 포함된 UI 요소. 즉, 사용자가 앱을 탐색할 때 앱은 기본적으로 NavHost
안팎으로 대상을 전환한다.
NavGraph
: 앱 내의 탐색 대상들(Fragment)과 어떻게 연결되어있는지 등이 정의되어있는 자료구조. 앱 내의 화면 연결을 보여주는 일종의 지도 역할을 한다.
NavController
: 실질적으로 탐색 동작을 처리하는 역할. 대상을 탐색하고 딥 링크를 처리하며 대상 백 스택을 관리하는 등의 여러 메소드를 제공한다.
이제 위에서 설명한 Navigation과 Compose를 활용해 BottomNavigationBar를 구현해보자.
implementation("androidx.navigation:navigation-compose:2.7.7")
Compose-Navigation 라이브러리를 gradle에 추가한다.
필자는 여기서 Home
, Rating
, Profile
3개의 화면을 만들어주었다.
@Composable
fun HomeScreen() {
Text(text = "Home")
}
@Composable
fun RatingScreen() {
Text(text = "Rating")
}
@Composable
fun ProfileScreen() {
Text(text = "Profile")
}
Bottom Navigation으로 이동할 item 객체 클래스 BottomNavItem
클래스를 생성하고 그 자식으로 Home
, Rating
, Profile
을 추가한다.
sealed class BottomNavItem(
@StringRes val title: Int,
@DrawableRes val icon: Int,
val screenRoute: String
) {
object Home : BottomNavItem(R.string.home, R.drawable.ic_home, LiamScreens.Home.name)
object Rating : BottomNavItem(R.string.rating, R.drawable.ic_rating, LiamScreens.Rating.name)
object Profile : BottomNavItem(R.string.profile, R.drawable.ic_profile, LiamScreens.Profile.name)
}
NavHost
함수의 NavGraphBuilder
를 통해서 NavGraph
를 생성할 수 있다.
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(route = MyScreens.Sample.name) {
SampleScreen()
}
composable(route = MyScreens.Home.name) {
HomeScreen()
}
composable(route = MyScreens.Rating.name) {
RatingScreen()
}
composable(route = MyScreens.Profile.name) {
ProfileScreen()
}
composable(route = MyScreens.Search.name) {
SearchScreen()
}
}
@Composable
fun MyApp() {
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
MyBottomNavigation(
containerColor = Color.Green,
contentColor = Color.White,
indicatorColor = Color.Green,
navController = navController
)
}
) {
Box(modifier = Modifier.padding(it)) {
MyNavHost(
navController = navController,
startDestination = LiamScreens.Home.name
)
}
}
}
@Composable
private fun MyNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
startDestination: String
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(route = LiamScreens.Sample.name) {
SampleScreen()
}
composable(route = LiamScreens.Home.name) {
HomeScreen(
onSearchClicked = { navController.navigate(LiamScreens.Search.name) }
)
}
composable(route = LiamScreens.Rating.name) {
RatingScreen()
}
composable(route = LiamScreens.Profile.name) {
ProfileScreen()
}
composable(route = LiamScreens.Search.name) {
SearchScreen()
}
}
}
@Composable
private fun MyBottomNavigation(
modifier: Modifier = Modifier,
containerColor: Color,
contentColor: Color,
indicatorColor: Color,
navController: NavHostController
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val items = listOf(
BottomNavItem.Home,
BottomNavItem.Rating,
BottomNavItem.Profile
)
AnimatedVisibility(
visible = items.map { it.screenRoute }.contains(currentRoute)
) {
NavigationBar(
modifier = modifier,
containerColor = containerColor,
contentColor = contentColor,
) {
items.forEach { item ->
NavigationBarItem(
selected = currentRoute == item.screenRoute,
label = {
Text(
text = stringResource(id = item.title),
style = TextStyle(
fontSize = 12.sp
)
)
},
icon = {
Icon(
painter = painterResource(id = item.icon),
contentDescription = stringResource(id = item.title)
)
},
onClick = {
navController.navigate(item.screenRoute) {
navController.graph.startDestinationRoute?.let {
popUpTo(it) { saveState = true }
}
launchSingleTop = true
restoreState = true
}
},
)
}
}
}
}
레퍼런스
1. 안드로이드 공식 홈페이지 [Navigation]
2. [Android] Jetpack Compose - Bottom Navigation 만들기
3. [Android] Single Activity Architecture (SAA) + Navigation
4. Navigation 훑어보기