์ปค์คํ ๋ ์ด์์๊ณผ ์ ์ํ UI
์ง๊ธ๊น์ง Row, Column, Box๋ก ๊ฑฐ์ ๋ชจ๋ ํ๋ฉด์ ๋ง๋ค์๋ค. ์ด๊ฒ ์ ๋๋ ๊ฒฝ์ฐ์, ํฐ์ด ์๋๋ผ ํ๋ธ๋ฆฟ์์ ์ผฐ์ ๋๋ฅผ ๋ค๋ค๋ณด์.
Row, Column์ผ๋ก ์ ๋๋ ๋ฐฐ์น๊ฐ ์๋ค
์๋ฅผ ๋ค์ด "๋ ๋ฒ์งธ ์์ดํ
์ ์ฒซ ๋ฒ์งธ ์์ดํ
์ค๋ฅธ์ชฝ ์๋๋ก ์ ๋ฐ ๊ฒน์น๊ฒ" ๊ฐ์ ๋ฐฐ์น๋ Row/Column/Box์ ์ ํด์ง ๊ท์น(๊ฐ๋ก๋ก, ์ธ๋ก๋ก, ๊ฒน์ณ์)์ผ๋ก๋ ํํ์ด ์ ๋๋ค. ์ด๋ด ๋ Layout์ ์ง์ ์ด๋ค.
@Composable
fun OverlapLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables[0].width + placeables[1].width / 2
val height = placeables[0].height + placeables[1].height / 2
layout(width, height) {
placeables[0].placeRelative(x = 0, y = 0)
placeables[1].placeRelative(
x = placeables[0].width / 2,
y = placeables[0].height / 2
)
}
}
}measurables๋ ์์ ๋ ์์๋ค(์ฌ๊ธฐ์๋ ๋ ๊ฐ)์ ์์ง ํฌ๊ธฐ๊ฐ ์ ํด์ง์ง ์์ ์ํ๋ก ๋ฐ์ ๊ฒ์ด๋ค. measure(constraints)๋ฅผ ํธ์ถํด์ผ ์ค์ ํฌ๊ธฐ๊ฐ ์๋ Placeable์ด ๋๋ค. layout(width, height) { } ๋ธ๋ก ์์์ ๊ฐ ์์์ placeRelative(x, y)๋ก ์ํ๋ ์์น์ ๋๋๋ค. Row/Column์ ๊ฒฐ๊ตญ ์ด Layout์ ๋ด๋ถ์ ์ผ๋ก ์ฐ๋ฉด์ "๊ฐ๋ก๋ก ๋์ดํ๋ค", "์ธ๋ก๋ก ๋์ดํ๋ค"๋ ๋ฐฐ์น ๊ท์น๋ง ๋ฏธ๋ฆฌ ์ ํด๋ ๊ฒ์ด๋ค. ์ง์ Layout์ ์ฐ๋ฉด ๊ทธ ๊ท์น ์์ฒด๋ฅผ ๋ด๊ฐ ์ ํ ์ ์๋ค.
์ด๋ฐ ์ ์์ค API๋ ๊ฑฐ์ ์ ์ด๋ค. ๋๋ถ๋ถ์ Row, Column, Box, ๊ทธ๋ฆฌ๊ณ ์ด๋ค์ ์กฐํฉํ ์ปค์คํ
์ปดํฌ์ ๋ธ๋ก ์ถฉ๋ถํ๊ณ , Layout์ ์ ๋ง ํน์ํ ๋ฐฐ์น๊ฐ ํ์ํ ๋๋ง ๊บผ๋ธ๋ค.
์ด์ ํฐ์ด ์๋๋ผ ํ๋ธ๋ฆฟ์์ ์ด์ด๋ณด๋ฉด
์ง๊ธ todo_project๋ ํฐ ํ๋ฉด๋ง ์๊ฐํ๊ณ ๋ง๋ค์๋ค. ๋ชฉ๋ก ํ๋ฉด์์ ํญ๋ชฉ์ ๋๋ฅด๋ฉด ์์ธ ํ๋ฉด์ผ๋ก ์ ์ฒด ํ๋ฉด ์ ํ์ด ์ผ์ด๋๋๋ฐ, ํ๋ธ๋ฆฟ ๊ฐ์ ํฐ ํ๋ฉด์์๋ ์ด๊ฒ ๊ณต๊ฐ ๋ญ๋น๋ค. ๋ชฉ๋ก๊ณผ ์์ธ๋ฅผ ๋๋ํ ๋ณด์ฌ์ค ์ ์๋๋ฐ ๊ตณ์ด ํ๋ฉด์ ๋ค ๋ฐ๊ฟ ๋ผ์ฐ๋ ๊ฑฐ๋ค.
ํ๋ฉด์ด ์ผ๋ง๋ ๋์์ง๋ถํฐ ์์์ผ ํ๋ค
// build.gradle.kts
dependencies {
implementation("androidx.window:window:1.5.0")
}import androidx.window.layout.WindowMetricsCalculator
import androidx.window.core.layout.WindowSizeClass
@Composable
fun TodoApp() {
val context = LocalContext.current
val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context)
val sizeClass = WindowSizeClass.BREAKPOINTS_V1.computeWindowSizeClass(windowMetrics)
val isWideScreen = sizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
if (isWideScreen) {
TabletLayout()
} else {
PhoneLayout()
}
}WindowSizeClass๋ ํ๋ฉด์ ์ ํํ dp ์์น ๋์ "ํญ์ด ๋๋ต ์ด ์ ๋ ๊ตฌ๊ฐ์ด๋ค"๋ก ๋ญ๋ฑ๊ทธ๋ ค์ ์๋ ค์ค๋ค. isWidthAtLeastBreakpoint(...)๋ก "์ ์ด๋ ์ด ๋๋น๋ ๋๋๊ฐ"๋ฅผ ๋ฌผ์ด๋ณด๋ ์์ผ๋ก ์ด๋ค. ๊ตฌ์ฒด์ ์ธ dp ๊ฐ์ผ๋ก if๋ฌธ์ ์์ญ ๊ฐ ์ฐ๋ ๋์ , "์ข๋ค/์ค๊ฐ/๋๋ค" ๋ช ๋จ๊ณ๋ก๋ง ๋๋ ์ ํ๋จํ๋ ๊ฒ ํต์ฌ์ด๋ค.
์ข์ ํ๋ฉด๊ณผ ๋์ ํ๋ฉด, ๋ ์ด์์์ ๋ค๋ฅด๊ฒ
@Composable
fun PhoneLayout() {
// ์ง๊ธ๊น์ง ๋ง๋ NavHost ๋ฐฉ์: ๋ชฉ๋ก ํ๋ฉด โ ์์ธ ํ๋ฉด
val navController = rememberNavController()
NavHost(navController, startDestination = "list") {
composable("list") { TodoListScreen(/* ... */) }
composable("detail/{todoId}") { /* ... */ }
}
}
@Composable
fun TabletLayout() {
var selectedTodoId by remember { mutableStateOf<Int?>(null) }
Row(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.weight(1f)) {
TodoListScreen(
/* ... */
onTodoClick = { todo -> selectedTodoId = todo.id }
)
}
Box(modifier = Modifier.weight(1f)) {
TodoDetailScreen(
todo = /* selectedTodoId๋ก ์ฐพ์ Todo */,
/* ... */
)
}
}
}[์ด๋ฏธ์ง: ํ๋ธ๋ฆฟ ๋๋น์์ ๋ชฉ๋ก๊ณผ ์์ธ๊ฐ ์ข์ฐ๋ก ๋๋ํ ๋ ์๋ ํ๋ฉด]
ํฐ์์๋ ์ง๊ธ๊น์ง ์ฐ๋ ๋ค๋น๊ฒ์ด์
๋ฐฉ์ ๊ทธ๋๋ก ์ฐ๊ณ , ํ๋ธ๋ฆฟ์์๋ ํ๋ฉด ์ ํ ์์ด Row๋ก ๋ชฉ๋ก๊ณผ ์์ธ๋ฅผ ๋๋ํ ๋์ด๋ค. TodoListScreen, TodoDetailScreen ์์ฒด๋ ๊ทธ๋๋ก ์ฌ์ฌ์ฉํ๋ค. ์ํ ํธ์ด์คํ
์ ํด๋ ๋๋ถ์, ์ด ๋์ ์ด๋ค ๋ ์ด์์์ ์ด๋ป๊ฒ ๋ฐฐ์นํ ์ง๋ ๋ฐ๊นฅ(PhoneLayout/TabletLayout)์์ ๊ฒฐ์ ํ ์ ์๋ค.
์ ๋ฆฌ
Row/Column/Box๋ก ํํ ์ ๋๋ ๋ฐฐ์น๊ฐ ํ์ํ ๋๋งLayout์ ์ง์ ์ด๋ค. ๋๋ถ๋ถ์ ํ์ ์๋คWindowSizeClass๋ก ํ๋ฉด ํญ์ "์ข๋ค/๋๋ค" ๋ช ๋จ๊ณ๋ก ๊ตฌ๋ถํด์, ๊ทธ์ ๋ง๋ ๋ ์ด์์์ ๊ณ ๋ฅธ๋ค- ํ๋ฉด์ ๊ทธ๋ฆฌ๋ ์ปดํฌ์ ๋ธ(
TodoListScreen,TodoDetailScreen)์ ์ฒ์๋ถํฐ "๊ฐ + ์ฝ๋ฐฑ๋ง ๋ฐ๋" ๊ตฌ์กฐ๋ก ๋ง๋ค์ด๋ ๋๋ถ์, ํฐ์ฉ/ํ๋ธ๋ฆฟ์ฉ ๋ ์ด์์์ ๊ฐ์๋ผ์ฐ๊ธฐ๊ฐ ์ฌ์ ๋ค
๋ค์ ๊ธ์์๋ ์ง๊ธ๊น์ง ๋ง๋ ViewModel๊ณผ ํ๋ฉด๋ค์ ํ
์คํธํ๋ ๋ฒ์ ๋ณด๊ฒ ๋ค.
