Material You (Material 3) 사용하면서 변경사항이 좀 많은 것 같아 레퍼런스 참고
예제코드 중 Material3이 반영되지 않은 코드는 변환해가며 작성해봤다.
부모 테마를 재정의해 테마를 중첩할 수 있다.
@Composable
@Preview
fun MaterialThemeDemo() {
MaterialTheme(
typography = Typography.copy( // Typography 재사용
displayLarge = TextStyle(color = Color.Red)
)
) {
Row(
Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text = "Hello",
style = MaterialTheme.typography.displayLarge // 수정된 MaterialTheme
)
Spacer(modifier = Modifier.width(2.dp))
MaterialTheme(
typography = Typography.copy( // Typography 재사용
displayLarge = TextStyle(color = Color.Blue)
)
) {
Text(
text = "Compose",
style = MaterialTheme.typography.displayLarge // 수정된 MaterialTheme
)
}
}
}
}
테마 xml에도 컬러 값을 사용하고 컴포즈에서도 컬러 값을 또 선언하지 않으려면 colorResource()
을 사용해 리소스 쪽에서만 컬러 값을 정리해두자.
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
// Android 31 미만에서는 다크모드 X
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme.copy(secondary = colorResource(id = R.color.orange_dark))
}
SplashScreen을 안드로이드 12 이전에서도 사용할 수 있는 방법도 있다니 찾아보자.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.AnimatedSplashScreen" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/black</item>
<item name="android:windowBackground">@color/black</item>
</style>
</resources>
<style name="Theme.AnimatedSplashScreen" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/black</item>
<item name="android:windowBackground">@color/black</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/transparent_image</item>
</style>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ComposeUnitConverter(factory: ViewModelFactory) {
val navController = rememberNavController() // 화면을 이동하기 위한 NavHostController 생성
val menuItems = listOf("Item #1", "Item #2")
// Material3: ScaffoldState 사용 안하고 바로 snackbarHostState 선언
val snackbarHostState = remember { SnackbarHostState() }
val snackbarCoroutineScope = rememberCoroutineScope()
Chapter06Theme(dynamicColor = false) {
Scaffold(topBar = {
ComposeUnitConverterTopBar(menuItems) { s ->
snackbarCoroutineScope.launch {
snackbarHostState.showSnackbar(s)
}
}
}, bottomBar = {
ComposeUnitConverterBottomBar(navController)
}) {
ComposeUnitConverterNavHost(
navController = navController, factory = factory, modifier = Modifier.padding(it)
)
}
}
}
@ExperimentalMaterial3Api
@Composable
fun TopAppBar(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: @Composable () -> Unit = {}, // 앱 바 좌측 아이콘
actions: @Composable RowScope.() -> Unit = {}, // 앱 바 우측에 들어가는 Row()
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null // 스크롤 상태에 따라 투명도를 조절하는 행동을 넣을 수 있겠다...
) {
SingleRowTopAppBar(
modifier = modifier,
title = title,
titleTextStyle = MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont),
centeredTitle = false,
navigationIcon = navigationIcon,
actions = actions,
windowInsets = windowInsets,
colors = colors,
scrollBehavior = scrollBehavior
)
}
예제 코드에는 DropdownMenu까지 action에 추가해서 보여주고 있지만 여기서는 중요하진 않아서 기록하지는 않겠음.
Scaffold()에서 bottomBar로 추가한 BottomAppBar() 알아보기
화면별로 라우팅을 하기 위한 객체가 하나 필요하다.
sealed class ComposeUnitConverterScreen(
val route: String,
@StringRes val label: Int,
@DrawableRes val icon: Int
) {
companion object {
val screens = listOf(
Temperature,
Distances
)
const val route_temperature = "temperature"
const val route_distances = "distances"
}
private object Temperature : ComposeUnitConverterScreen(
route_temperature,
R.string.temperature,
R.drawable.baseline_thermostat_24
)
private object Distances : ComposeUnitConverterScreen(
route_distances,
R.string.distances,
R.drawable.baseline_square_foot_24
)
}
ComposeUnitConverterScreen
를 어떻게 사용하는지 나온다.@Composable
fun ComposeUnitConverterBottomBar(navController: NavHostController) {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
// 모든 스크린 forEach
ComposeUnitConverterScreen.screens.forEach { screen ->
NavigationBarItem(selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
// 일단 클릭하면 그 화면으로 이동하기
navController.navigate(screen.route) {
launchSingleTop = true
}
},
label = {
// screen 객체에 이미 정의된 label
Text(text = stringResource(id = screen.label))
},
icon = {
// screen 객체에 이미 정의된 icon, label
Icon(
painter = painterResource(id = screen.icon),
contentDescription = stringResource(id = screen.label)
)
},
alwaysShowLabel = false // 선택되었을 때만 label 보여줌
)
}
}
}
교재는 섭씨/화씨 변환 기능이 있는 화면과 미터/마일 변환 기능이 있는 화면을 NavHostController
와 NavHost
를 이용해 전환하는 예제를 보여주고 있다.
@Composable
fun ComposeUnitConverterNavHost(
navController: NavHostController, factory: ViewModelProvider.Factory?, modifier: Modifier
) {
NavHost(
navController = navController,
startDestination = ComposeUnitConverterScreen.route_temperature,
modifier = modifier
) {
composable(ComposeUnitConverterScreen.route_temperature) {
TemperatureConverter(
viewModel = viewModel(factory = factory)
)
}
composable(ComposeUnitConverterScreen.route_distances) {
DistancesConverter(
viewModel = viewModel(factory = factory)
)
}
}
}
근데 아까 위에서 Splash 화면에서 홈화면으로 이동하기 위해서도 이러한 동작이 필요했었다.
MainActivity부터 살펴보자
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Chapter06Theme {
val navController = rememberNavController()
SetupNavGraph(navController = navController)
}
}
}
}
이 Activity에서 이동할 화면들은 모두 NavHostController로 제어한다.
@Composable
fun SetupNavGraph(navController: NavHostController) {
NavHost(navController = navController, startDestination = Screen.Splash.route) {
composable(route = Screen.Splash.route) {
SplashScreen(navController = navController)
}
composable(route = Screen.Home.route) {
HomeScreen()
}
}
}
여기서는 화면이동 단위를 SplashScreen과 HomeScreen으로 나눴다.
이 HomeScreen 안에서는 아까 위에서 섭씨/화씨, 미터/마일 변환하는 화면이 나올 것이다.
그 화면들을 여기서 전부 통합해 정의하면 어떻게 될까...?
@Composable
fun SetupNavGraph(navController: NavHostController) {
NavHost(navController = navController, startDestination = ComposeUnitConverterScreen.splash) {
composable(ComposeUnitConverterScreen.splash) {
SplashScreen(navController = navController)
}
composable(ComposeUnitConverterScreen.temperature) {
HomeScreen(navController = navController)
}
composable(ComposeUnitConverterScreen.distances) {
HomeScreen(navController = navController)
}
}
}
이렇게 구성했더니 HomeScreen에서 화면을 이동할 때마다 깜빡거린다. 확실히 이건 잘못된 방법이었다. ㅠㅠ
이 예제는 Navigation 기능을 사용해 화면을 이동하는 예제다 보니 화면 전환했던 기록이 전부 쌓여있어서 Back 버튼을 누르면 이전 화면으로 되돌아가는게 보인다. (데이터도 이전 화면의 데이터가 남아있음)
그리고 ViewModelFactory를 사용하다보니 화면을 새로 만들면서도 이전 값이 유지 되면서 생성된다는 점도 같이 확인하면 되겠다.