23 min to read
Introduce Android Jetpack Compose ๐
Jetpack Compose๋ ๋ฌด์์ธ๊ฐ?
Compose๋ Native UI๋ฅผ ์ฝ๋๋ ๋ฒจ๋ก ๊ตฌํํ ์ ์๋ ์ต์ ํดํท์ด๋ค. Compose๋ฅผ ์ด์ฉํ๋ฉด ์๋์ ๊ฐ์ ์ฅ์ ์ด ์๋ค.
-
์ฝ๋ ๊ฐ์
Less Code
- ์ ์ ์์ ์ฝ๋๋ก ๋ ๋ง์ ์์ ์ ํ๊ณ ์ ์ฒด ๋ฒ๊ทธ ํด๋์ค๋ฅผ ๋ฐฉ์งํ ์ ์์ผ๋ฏ๋ก ์ฝ๋๊ฐ ๊ฐ๋จํ๋ฉฐ ์ ์ง ๊ด๋ฆฌํ๊ธฐ ์ฝ์ต๋๋ค.
-
์ง๊ด์
Intuitive
- UI๋ง ์ค๋ช ํ๋ฉด ๋๋จธ์ง๋ Compose์์ ์ฒ๋ฆฌํฉ๋๋ค. ์ฑ ์ํ๊ฐ ๋ณ๊ฒฝ๋๋ฉด UI๊ฐ ์๋์ผ๋ก ์ ๋ฐ์ดํธ๋ฉ๋๋ค.
-
๋น ๋ฅธ ๊ฐ๋ฐ ๊ณผ์
Accelerate Development
- ๊ธฐ์กด์ ๋ชจ๋ ์ฝ๋์ ํธํ๋๋ฏ๋ก ์ธ์ ์ด๋์๋ ์ํ๋ ๋๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ค์๊ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ฐ ์์ ํ Android ์คํ๋์ค ์ง์์ผ๋ก ๋น ๋ฅด๊ฒ ๋ฐ๋ณตํ ์ ์์ต๋๋ค.
-
๊ฐ๋ ฅํ ์ฑ๋ฅ
Powerful
- Android ํ๋ซํผ API์ ์ง์ ์ก์ธ์คํ๊ณ ๋จธํฐ๋ฆฌ์ผ ๋์์ธ, ์ด๋์ด ํ ๋ง, ์ ๋๋ฉ์ด์ ๋ฑ์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ง์ํ๋ ๋ฉ์ง ์ฑ์ ๋ง๋ค ์ ์์ต๋๋ค.
Jetpack Compose์ ์ฌ์ฉํด์ผํ๋ ๊ฐ์ธ์ ์ธ ์ด์
์ง๊ธ๊น์ง ๋์จ ์๋๋ก์ด๋์ ๋ค์ดํฐ๋ธ ๋ทฐ ์ปดํฌ๋ํธ ์์๋ ์ปดํฌ๋ํธ๋ฅผ ํ๋ฒ Window์ ์ถ๊ฐํ ์ดํ ์ธ์คํด์ค์์ ์ ๊ณตํ๋ ์์ฑ๊ฐ์ ์ ์ดํ๋ ํํ๋ก ๊ตฌ์ฑ๋์ด ์๋ค. ๊ทธ๋ฌ๋ค๋ณด๋, ๊ธฐ์กด์ ์๋ ๋ทฐ ์ํ๋ฅผ ์ฒดํฌํ์ฌ ๊ธฐ์กด ๋ทฐ๋ฅผ ๋ณ๊ฒฝํ๋ UI ๋ก์ง์ด ํ์ฐ์ ์ด์๊ณ , ์ด๋ฐ ๋ถ๋ถ์์ ๊ฐ๋ฐ์๊ฐ ์ง์คํด์ผํ ๋ณธ์ง์ ์ธ ๋ถ๋ถ์ธ ๋น์ฆ๋์ค๋ก์ง์ ๋ํด ์ ๊ฒฝ์ ๋ถ์ฐ์ํค๋ ์์ธ์ด ๋๊ธฐ๋ํ๋ค.
์ด๋ฐ๋ฌธ์ ๋ฅผ ๊น๋ํ๊ฒ ํด๊ฒฐํด์ค ์ ์๋ Toolkit์ธ Compose๊ฐ ๋ฆฌ์กํฐ๋ธ ํ๋ก๊ทธ๋๋ฐ ๋ชจ๋ธ์ ๊ธฐ๋ฐ์ผ๋ก ์ ์ธํ UI Builder ํจํด๊ณผ ํจ๊ป ์ฐ๋ฆฌ์์ ์ ๋ณด์ด๊ฒ ๋์๋๋ฐ, ์ฝ๋๋ง ๋ณด๋ฉด ํ๋ฉด์ ์ํModel
์ ๋ฐ๋ผ UI์ ์ํ๋ฅผ ๊ฒฐ์ ์ง๊ธฐ ๋๋ฌธ์, ๋ ์ด์ UI ์ํ๋ฅผ ๋ณด๊ณ UI๋ก์ง์ ๊ตฌ์ฑํ ํ์๊ฐ ์๋ค๋ผ๋ ์ฅ์ ์ด ์๋ค.
๋ํ, Gof์ State ํจํด๊ณผ ๊ฐ์ด ํ๋ฉด์ ์ํ๋ฅผ ๋จ์ผ๋ก ํํํ ์ ์๋ ์๋จ์ด ์๋ค๋ฉด, ํ๋ฉด์ ์ ์ดํ ๋ ๋ํ ๋์์์ด ์ฌ์ด๋ฐฉ๋ฒ์ผ๋ก ๊ฐ๋ฐ์ ๊ฒฝํํ ์ ์๋ค๋ ๊ฒ ๋ํ ์ฅ์ ์ด๋ค.
์ผ๋จ ์ฝ๋๋ฉ์ ์ฝ๋๋ฅผ ๋ณด๊ณ ํ๋ฒ ์ด๋ค ๋๋์ธ์ง ๋ณด๋๋กํ์. Compose CodeLab
Codelab ํ๋์ฉ ์ ๋ฆฌํด๋ณด๊ธฐ - Jetpack Compose basics
์ ์ฝ๋์์ ๊ฐ๋จํ๊ฒ OverView๋ฅผ ๋ณธ ๊ฒ์ ๋ํด, ์ฌ์ฉ๋ฒ์ ์ตํ๋ณด์. ์ฐ๋ฆฌ๋ ๋๋ฌด ๋ง์๊ฒ์ ์ตํ๋ณด๊ธฐ์ ์๊ฐ์ด ๋ถ์กฑํ๋ฏ๋ก, ๊ฐ๋จํ๊ฒ 1 ~ 5๊น์ง ํจ๊ป ๋ณด๋๋กํด๋ณด์.
์ ํ๋ก์ ํธ ์์ฑ - Empty Compose Activity ์ ํ
์ ํ ์ดํ Next๋ฅผ ํด๋ฆญํ๊ณ , Compose๋ฅผ ๊ตฌํํ ์ ์๋ ์ต์ API ๋ ๋ฒจ์ธ 21์ ์ ํํด์ผํ๋ค.
ํ๋ก์ ํธ๋ฅผ ์์ฑํ๋ฉด ์๋์ ๊ฐ์ด app/build.gradle์ ์์กด์ฑ ์ค์ ๋ฐ ์ถ๊ฐ๊ฐ ๋์ด ์๋๊ฒ์ ์ ์ ์๋ค.
android {
...
kotlinOptions {
jvmTarget = '1.8'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
}
dependencies {
...
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.activity:activity-compose:1.3.0-alpha03"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
...
}
์ด์ MainActivity.kt๋ฅผ ๋ณด์.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
BasicsCodelabTheme {
Greeting("Android")
}
}
๊ทธ๋ฌ๋ฉด ์๊น ๋ณด์๋ ์ฝ๋๋ฅผ ๋ณด์์ ๋, ์ด ์ธ๊ฐ์ง์ ์์๋ฅผ ๊ฐ์ง๋ ๊ฒ์ผ๋ก ์ ๋ฆฌํด๋ณผ ์ ์๊ฒ ๋ค.
- ์์ ฏ์ ํฌํจํ๋ Composable ํจ์
- Preview๋ฅผ ํ๊ธฐ ์ํ Preview Composable ํจ์
- setContent ๋๋ค ํํ์์ผ๋ก ์ค์ ํ๋ฉด์ ๋ ธ์ถํ๋ ์ฝ๋
์ผ๋ฐ์ ์ผ๋ก ์ฐ๋ฆฌ๊ฐ ์๋ Activity์ ๋ผ์ดํ์ฌ์ดํด ์ฝ๋ฐฑ onCreate()
์์ setContentView(Int)
ํจ์๋ฅผ ํธ์ถํ๋๊ฒ์ด setContent()
ํจ์๋ก ๋ฐ๋๊ฒ์ด ๊ฐ์ฅ ํฐ ํน์ง์ผ๋ก ๋ณด์ฌ์ง๋ค.
๊ทธ๋ฆฌ๊ณ ํ๋จ์ @Composable
์ด๋ผ๋ ์ด๋
ธํ
์ด์
์ด ๋ณด์ด๊ณ , @Preview
์ด๋
ธํ
์ด์
์ด ๋ณด์ด๋๋ฐ, ์ด ๋๊ฐ์ง๋ฅผ ํตํด ์ฐ๋ฆฌ๋ ์ ์ถํด๋ณผ ์ ์๋ ๋จ์ด์ ์๋ฏธ๊ฐ ์๋ค.
- Composable : ๋ฌด์ธ๊ฐ ์กฐํฉ์ด ๊ฐ๋ฅํ ๋ ์์ด๋ค. ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ์ฌ ์กฐํฉ์ด ๊ฐ๋ฅํ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค.
- Preview : ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ํ ์ ์๋ ๋ ์์ด๋ค. @Composable์ด ์๋์ ๋ถ์๊ฒ์ ๋ณด๋ ์ปดํฌ๋ํธ์ ๋ํ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ ๊ฐ๋ฅํ ํํ๋ก ์ ๊ณตํ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค.
์, ๊ทธ๋ฌ๋ฉด ํ๋ํ๋์ฉ ์ฝ๋๋ฅผ ๋ณด์.
Composable Function
Composable Function์ ์ด๋
ธํ
์ด์
์ ์ด์ฉํ ๊ธฐ์ ์ด๋ค. ํจ์์์ @Composable
์ด๋
ธํ
์ด์
์ ๋ถ์ด๊ฒ ๋๋ฉด ํจ์ ์ ๋ค๋ฅธ ํจ์๋ฅผ ํธ์ถํ ์ ์๊ฒ๋๋ค. ์๋ ์ฝ๋๋ฅผ ๋ณด์.
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
๋จ์ํ๊ฒ ๋ด๋ถ์๋ Text๋ผ๋ ํจ์๊ฐ ์กด์ฌํ๋๋ฐ, ์ด๋ฅผ ํตํด UI๊ณ์ธต ๋ณ ์๊ตฌํ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํด์ค๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ๋ณด์ด๋ text ํ๋ผ๋ฏธํฐ๋ ๋ด๋ถ ์์ฑ์์ ๋ฐ๋ ์ผ๋ถ ์ค ํ๋์ด๋ค.
TextView ๋ง๋ค๊ธฐ
์ ์ฝ๋๋ฅผ ์คํ์์ผ๋ณด๋ฉด ๋น์ฐํ๊ฒ๋ Hello๋ก ์์ํ๋ TextView๊ฐ ํ๋ฉด์ ๊ทธ๋ ค์ง๊ฒ์ ์์ํ๋ค.
setContent {
BasicsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
@Preview
๋ง ๊ทธ๋๋ก ์ด๋ ธํ ์ด์ ์ ์ด์ฉํ์ฌ IDE์์ Preview๋ฅผํ๊ธฐ ์ํ ์ฉ๋์ด๋ค. ์๋ ์ฝ๋์ ๊ฐ์ด @Preview ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ฉด ๋ค์ ๊ฒฐ๊ณผ๋ฅผ ๋ณผ ์ ์๋ค.
@Preview("Greeting Preview")
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
ํ๋ฉด์ ๊ตฌ์ฑํ๋๋ฐ ํ์์ ์ธ ์์๋ค
setContent {
BasicsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
๊ธฐ์กด์ onCreate์์ ์ ํ๋ฉด์ ๊ทธ๋ ค์ฃผ๊ธฐ ์ํ ํ์์ ์ธ ์์๋ฅผ ์ ๋ฆฌํด๋ณด์๋ฉด
-
setContent : Activity์์ setContentViewํจ์๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๊ณผ ๋์ผํ ๋์์ ํ๋ ํ์ฅํจ์์ด๋ค. ๋ค๋ง, setContent์ ๊ฒฝ์ฐ (@Composable) -> Unit ํ์ ์ ์ปดํฌ์ฆ UI๋ฅผ ๊ตฌํํด์ฃผ์ด์ผํ๋ค.
-
XXXTheme : Theme์ ๋ณด๋ฅผ ์๋ฏธํ๋ค. ํด๋น ํ๋ก์ ํธ์์๋ Theme.kt์ ์ฌ๋ฌ ํ ๋ง์ ํ์ํ ์ ๋ณด๋ฅผ ์ ๋ฆฌํ๊ณ , ์ปดํฌ์ฆ UI ๊ตฌํ์ ์ํ ์ฝ๋๋ฅผ ์์ฑํด๋์๋ค.
private val DarkColorPalette = darkColors(
primary = purple200,
primaryVariant = purple700,
secondary = teal200
)
private val LightColorPalette = lightColors(
primary = purple500,
primaryVariant = purple700,
secondary = teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun BasicsCodelabTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = typography,
shapes = shapes,
content = content
)
}
- Surface : Greeting์ ๊ฐ์ธ๋ ๋ทฐ์ ํด๋นํ๋ค. ์ฌ๊ธฐ์๋ ํฌ๊ธฐ๋ฅผ ์ ํ์ง ์๊ณ , background ์์์ ์ ์ํ๊ณ ์๋ค. ์ญ์ ๋๋ค ํํ์์ด๋ค. ์์์ ๋ํ Paramter๋ก
color
๋ผ๋ ๊ฐ์ ์ฌ์ฉํ์ฌ ๋ถ์ฌ๊ฐ ๊ฐ๋ฅํ๋ค. ๋ด๋ถ์ฝ๋๋ฅผ ๋ณด๋ฉด
@Composable
fun Surface(
modifier: Modifier = Modifier,
shape: Shape = RectangleShape,
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),
border: BorderStroke? = null,
elevation: Dp = 0.dp,
content: @Composable () -> Unit
) {
val elevationPx = with(LocalDensity.current) { elevation.toPx() }
val elevationOverlay = LocalElevationOverlay.current
val absoluteElevation = LocalAbsoluteElevation.current + elevation
val backgroundColor = if (color == MaterialTheme.colors.surface && elevationOverlay != null) {
elevationOverlay.apply(color, absoluteElevation)
} else {
color
}
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalAbsoluteElevation provides absoluteElevation
) {
Box(
modifier.graphicsLayer(shadowElevation = elevationPx, shape = shape)
.then(if (border != null) Modifier.border(border, shape) else Modifier)
.background(
color = backgroundColor,
shape = shape
)
.clip(shape),
propagateMinConstraints = true
) {
content()
}
}
}
์ ์ธํ UI
๋ ธ๋์ ๋ฐฐ๊ฒฝ์ ์ ํ ๊ธฐ์กด TextView์ ์ถ๊ฐํด๋ณด์๋ค. ๋ํ, Greeting์๋ Modifier๋ผ๋ ๊ฒ์ ์ด์ฉํ์ฌ Padding์ ์ถ๊ฐํ๋ค. ์๋์ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋์ค๊ฒ ๋์๋ค.
BasicsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = Color.Yellow) {
Greeting("Android")
}
}
@Composable
fun Greeting(name: String) {
var isSelected by remember { mutableStateOf(false) }
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
text = "Hello $name!",
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { isSelected = !isSelected })
)
}
์ ์ธํ UI์ ์ฅ์ ์ ๋ง ๊ทธ๋๋ก ๋ด๊ฐ UI๋ฅผ ์ ์ํ๋๋ก ์๊ฐ์ ์ผ๋ก ํํ์ด ๊ฐ๋ฅํ๋ค๋ ์ฅ์ ์ด ์๋ค. ๊ธฐ์กด์๋ ์์ฑ์ ๋งค๋ฒ On/Off์ ๊ฐ์ ์ต์ ์ ํตํด ๋ณ๊ฒฝํ๋ ๊ฒ์ด ๋ค๋ฐ์ฌ์์ง๋ง, ์ด์ ๋ ๋งค๋ฒ ์์ฑ์ ๋ณ๊ฒฝ์ด ์๊ธธ๋๋ง๋ค ์๋ก ๊ทธ๋ ค์ฃผ๊ฒ ๋๋๊ฒ์ด๋ค.
Compose reusability
์ปดํฌ์ฆ์ ์ฅ์ ์ค ํ๋๋ ์ฌ์ฌ์ฉ์ฑ์ด ๋ฐ์ด๋๊ฒ์ธ๋ฐ, XML์์ ์ฐ๋ฆฌ๊ฐ include ํ๊ทธ๋ฅผ ํตํด ์ฌ๋ฌ๊ณณ์์ ๊ฐ๋ค์ธ ์ ์๋๊ฒ์ฒ๋ผ, ํจ์๋ฅผ ํตํด ์ฌ๋ฌ๊ณณ์์ ์ ์ํ์ฌ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค.
์ฐธ๊ณ ํด์ผํ ์ ์ ์ปดํฌ์ฆ ์ปดํฌ๋ํธ ํ์ฅ ์ @Composable ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ ํ์ฅ์ด ํ์ํ๋ค.
Container ์์ฑ
MyApp์ด๋ผ๋ ์ด๋ฆ์ผ๋ก ์ปดํฌ์ฆ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํฌํ์ฌ ์ฌ๋ฌ๊ณณ์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ ์ ์๋ Composable์ ๊ตฌํํ์๋ค. ๋ด๋ถ์ ์ผ๋ก Container๋ด ๋ด๊ฐ ์ํ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ฃ์ด์ฃผ๋ ค๋ฉด ์๋์ ๊ฐ์ด ์ธ์๋ก @Composable () -> Unit
ํ์
์ ๋๊ฒจ๋ฐ์ ์ฒ๋ฆฌํด์ฃผ๋ฉด ๋๋ค.
@Composable
fun MyApp(content: @Composable () -> Unit) {
BasicsCodelabTheme {
Surface(color = Color.Yellow) {
content()
}
}
}
์ ํจ์๋ฅผ ํตํด ์ด์ ๋ ์ด๋์๋ ๋ฐ๋ณตํด์ ์ฌ์ฉํ ์ ์๋ Container๋ฅผ ๊ตฌํํ๊ฒ ๋์ด ์๋์ ๊ฐ์ด ์ฝ๋๋ฅผ ํ์ฉํ ์ ์๊ฒ๋์๋ค.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
Greeting("Android")
}
}
...
}
Calling Composable functions multiple times using Layouts
์ง๊ธ๊น์ง๋ ํ๋์ ์ปดํฌ๋ํธ๋ง์ ๊ฐ๊ณ ์ฌ์ฉํ์ง๋ง, ์ฌ๋ฌ๊ฐ์ ์ปดํฌ๋ํธ๋ฅผ ๋ฃ๋๊ฒ๋ ๊ฐ๋ฅํ๋ค.
@Composable
fun MyScreenContent() {
Column {
Greeting("Android")
Divider(color = Color.Black)
Greeting("there")
}
}
Column
๊ณผ ์์์๋ถํฐ ์ฌ์ฉํ๋ Greeting
ํจ์๋ฅผ ์ฌ์ฉํ๊ณ , ๋ผ์ธ์ ๊ทธ์ด์ฃผ๊ธฐ ์ํ Divider
๋ฅผ ์ถ๊ฐํ ๊ฒฐ๊ณผ๋ฌผ์ ๋ค์๊ณผ ๊ฐ๋ค.
์ ์ปดํฌ๋ํธ ์ค ๋ชป๋ณด๋ ์ปดํฌ์ ๋ธ์ด ์๋๋ฐ, ์๋์ ๊ฐ์ด ์ค๋ช ์ด ๊ฐ๋ฅํ๋ค.
- Column : ํญ๋ชฉ์ ์์๋๋ก ๋ฐฐ์นํ๊ธฐ ์ํด ์ฌ์ฉํ๋ค.
- Divider : ์ ๊ธ๊ธฐ ๊ฐ๋ฅํ Compose ํจ์์ด๋ค.
์ด๋ฅผ ๋ฆฌ์คํธ ํํ๋ก๋ ๊ตฌํ์ด ๊ฐ๋ฅํ๋ค.
@Composable
fun MyColumnScreen(names: List<String> = listOf("Line One", "Line Two")) {
Column {
names.forEach {
Greeting(name = it)
Divider(color = Color.Black)
}
}
}
์ํ๊ฐ ๊ด๋ฆฌ
์ ์ปดํฌ๋ํธ์ ๋ฒํผ์ ํด๋ฆญํ์ ๋ ํด๋ฆญํ ์นด์ดํธ๋ฅผ ์ง๊ณํ๋ ๊ฐ๋จํ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค์ด๋ณด์๋ค.
@Composable
fun MyColumnScreen(names: List<String> = listOf("Line One", "Line Two")) {
val counterState = remember { mutableStateOf(0) } //
Column {
names.forEach {
Greeting(name = it)
Divider(color = Color.Black)
}
Counter(
count = counterState.value,
updateCount = { newCount ->
counterState.value = newCount
}
)
}
}
rememer๋ผ๋ ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๊ธฐ์กด์ ์กด์ฌํ๋ ์ปดํฌ๋ํธ์ ์ํ๊ฐ์ ๊ธฐ์ตํ๊ฒ ํ๋ ํจ์๊ฐ ์๋ค. ๋ด๋ถ๋ฅผ ํ๋ฒ๋ณด๋ฉด
/**
* Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition.
* Recomposition will always return the value produced by composition.
*/
@OptIn(ComposeCompilerApi::class)
@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
currentComposer.cache(false, calculation)
๋งค๋ฒ๋ง๋ค Recomposition(์ฌ์กฐํฉ)ํ๊ฒ๋๋ ๊ฒฝ์ฐ ์ปดํฌ๋ํธ์ ๊ฐ์ ๋ค์ ์ ๊ณตํ๋ ๊ฒ์ ์ ์ ์๋ค. @Composable ์ด๋ ธํ ์ด์ ์ ๋ค์ด๊ฐ ํจ์๋ ๋งค๋ฒ ํด๋น ์ํ๋ฅผ ๊ตฌ๋ ํ๊ณ , ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์๋ฆผ์ ๋ฐ์ ๊ธฐ์กด ํ๋ฉด์ ๊ฐฑ์ ํด์ค๋ค.
๊ทธ๋ฆฌ๊ณ , ์๋ Counter๋ฅผ ๋ณด๋ฉด Button์ ์ด์ฉํ์ฌ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ ์ฒ๋ฆฌํ๋๋ก ํ๋ค.
@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
Button(
onClick = { updateCount(count + 1) },
colors = ButtonDefaults.buttonColors(
backgroundColor = if (count > 5) Color.Green else Color.White
)
) {
Text("I've been clicked $count times")
}
}
updateCount(Int)
ํจ์๋ฆ ํตํด ๋งค๋ฒ ๊ฐ์ ์
๋ฐ์ดํธ ํด์ฃผ๋๋ฐ, ์ด๋ฅผ ํตํด counterState์ ๊ฐ์ ๋ฃ์ด์ฃผ๋ฉด์ ํด๋น ์ปดํฌ๋ํธ๊ฐ ๋งค๋ฒ ๋ณ๊ฒฝ์ด ๋๋๊ฒ์ด๋ค.
๋ฐ๋ผ์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด, ๋ค์๊ณผ ๊ฐ๋ค. Count๊ฐ 5๊ฐ ๋์ด๊ฐ๋ฉด ์ด๋ก์์ผ๋ก ๋ฐ๋๋ค.
๊ทธ ์ธ์๋ ์ฌ๋ฌํํ์ ๋ชจ์์ ๊ตฌ์ฑํ ์ ์๋๋ก ์ต์ ์ด ์ ๊ณต๋์ด ์๋ค. ์์ธํ ์ ๋ณด๋ ๋์ค์ Codelabs์ ๋ ๋์ ์์ผ๋ ๋ณด๋๋กํ๊ณ , ์ด๋ฒ์ setContent์ ๋ํ ๋์์๋ฆฌ๋ฅผ ํจ๊ป ๊ณ ๋ฏผํด๋ณด์.
Activity์์ ์์ฑ
Compose๋ฅผ ์๋๋ก์ด๋ ์ฑ์์ ์ฌ์ฉํ๋ ค๋ฉด Activity, Fragment์ ๊ฐ์๊ณณ์์ contentView๋ก ๋ฟ๋ ค์ค์ผํ๋ค. ๊ธฐ์กด์ ์ฐ๋ฆฌ๊ฐ ์ฌ์ฉํ๋ ํจ์๋ฅผ ๋ณด์.
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
UI ์ปดํฌ๋ํธ์์ ํ๋ฉด์ ๋ถ์ผ ์ ์๋ Window๋ผ๋ ๋ ์์์ Layout Resource Id๋ฅผ ํตํด ๊ธฐ์กด์ ๋ฑ๋ก๋์ด์๋ Layout XML ํ์ผ์ ๋ก๋ํ์ฌ ์ธํ๋ ์ดํฐ์์ ํ์ฑํ๊ณ , ์ด๋ฅผํตํด ๋ ์ด์์ ๊ณ์ธต์ ์๋ ๋ทฐ๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ ์์ฐจ์ ์ผ๋ก ViewGroup, View๋ฅผ ๋ง๋ค์ด ๋ฃ์ด์ฃผ๊ฒ ๋๋ค.
PhoneWindow
๋ฅผ ๋ณด๋ฉด ์์ธํ๊ฒ ์ ์ ์๋๋ฐ, Window๋ฅผ ๊ตฌํํ setContentView์์ ์ฒ์์ ์์ฑ๋๋ ์ต์์ ๋ ์ด์์ ๊ทธ ์์ ๋ฐ๋ก ์๋ค๋ฉด installDecor()
ํจ์๋ฅผ ํตํด mContentParent(๋ ์ด์์ ๋ฆฌ์์ค๊ฐ ๋ถ๊ฒ๋ ViewGroup)๋ฅผ ์์ฑํ๊ณ , ํ์์ ๋ฃ์ด์ฃผ๊ฒ ๋๋ค.
๊ทธ๋ฌ๋ฉด ๊ธฐ์กด ๋ฐฉ์์ ์ด์ ๋๋ก ์ค๋ช
์ํ๊ณ , ์ด๋ฒ์ Compose์์ setContent()
๋ผ๋ ํจ์๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ๋ณด์.
/**
* Composes the given composable into the given activity. The [content] will become the root view
* of the given activity.
*
* This is roughly equivalent to calling [ComponentActivity.setContentView] with a [ComposeView]
* i.e.:
*
* ```
* setContentView(
* ComposeView(this).apply {
* setContent {
* MyComposableContent()
* }
* }
* )
* ```
*
* @param parent The parent composition reference to coordinate scheduling of composition updates
* @param content A `@Composable` function declaring the UI contents
*/
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
์ด๋
์๋ ๋ง์ฐฌ๊ฐ์ง๋ก window.decorView.findViewById<ViewGroup>(android.R.id.content)
ํจ์๋ฅผ ํธ์ถํ์ฌ decorView๋ฅผ ๊ฐ์ ธ์จ๋ค. ๋ง์ฝ compose๋ฅผ ํตํด ๋ง๋ค์ด์ง ์ต์์ ๋ ์ด์์์ด ์กด์ฌํ๋ฉด, ๊ธฐ์กด์ inflator์์ ViewGroup, View๋ฅผ ์์ฑํด์ ๋ฃ์ด์ฃผ๋๊ฒ ์ฒ๋ผ setContent()
=> window๊ฐ Activity/Fragment์ ๋ถ์ผ๋ฉด createComposition()
๋ฅผ ํธ์ถํ์ฌ ๊ฒ์ฆ ํ ensureCompsositionCreated()
ํจ์๋ฅผ ํธ์ถํ๋ค. ํ์ฌ๋ ๋ด๋ถ์ ์ผ๋ก ViewGroup.setContent()
๋ฅผ ์ฌ์ฉํ๊ณ ์๋๋ฐ, ๊ณง ๊ต์ฒด ๋ ์์ ์ด๋ผ๊ณ ํ๋ค. ์ด์ฝ๋๋ ๋ณด๋ฉด ๊ธฐ์กด์ ์๋ ViewGroup์ ํ์ฅํจ์๋ก ๊ตฌํํ ๋
์์ธ๋ฐ, ์ฝ๊ฒ ๋งํด ViewGroup์ ํ์ View, ViewGroup์ Composable๋ก ๊ตฌํ๋ ํจ์๋ก ์ปดํฌ๋ํธ๋ฅผ ๋ฃ์ด์ค ๋ AndroidComposeView๋ผ๋ ๊ฐ์ฒด๋ฅผ ๊บผ๋ด์ค๊ฑฐ๋ ์๋ค๋ฉด ์๋ก ์์ฑํ์ฌ ๋ฃ์ด์ค๋ค.
/**
* Composes the given composable into the given view.
*
* The new composition can be logically "linked" to an existing one, by providing a
* [parent]. This will ensure that invalidations and CompositionLocals will flow through
* the two compositions as if they were not separate.
*
* Note that this [ViewGroup] should have an unique id for the saved instance state mechanism to
* be able to save and restore the values used within the composition. See [View.setId].
*
* @param parent The [Recomposer] or parent composition reference.
* @param content Composable that will be the content of the view.
*/
internal fun ViewGroup.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
GlobalSnapshotManager.ensureStarted()
val composeView =
if (childCount > 0) {
getChildAt(0) as? AndroidComposeView
} else {
removeAllViews(); null
} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
return doSetContent(composeView, parent, content)
}
๋ค์ ๋์์์, ComposeView์ setContent()
์ด๋ผ๋ ๋
์์ ๋ณด์.
/**
* A [android.view.View] that can host Jetpack Compose UI content.
* Use [setContent] to supply the content composable function for the view.
*
* This [android.view.View] requires that the window it is attached to contains a
* [ViewTreeLifecycleOwner]. This [androidx.lifecycle.LifecycleOwner] is used to
* [dispose][androidx.compose.runtime.Composition.dispose] of the underlying composition
* when the host [Lifecycle] is destroyed, permitting the view to be attached and
* detached repeatedly while preserving the composition. Call [disposeComposition]
* to dispose of the underlying composition earlier, or if the view is never initially
* attached to a window. (The requirement to dispose of the composition explicitly
* in the event that the view is never (re)attached is temporary.)
*/
class ComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
@Suppress("RedundantVisibilityModifier")
protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@Composable
override fun Content() {
content.value?.invoke()
}
/**
* Set the Jetpack Compose UI content for this view.
* Initial composition will occur when the view becomes attached to a window or when
* [createComposition] is called, whichever comes first.
*/
fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}
}
์์ ๋ญ๋ผ๋ญ๋ผ ์จ์ ธ ์๋๋ฐ, ๊ฒฐ๋ก ์ ์ผ๋ก AbstractComposeView
๋ผ๋ ๋
์์ ViewGroup์ ์์๋ฐ์ ๋
์์ด๋ฉฐ, ๋ชจ๋ composable์ ์ํ๊ฐ ๋ณํ ๋์์ ๋ ์ด๋ฅผ ๊ฐ์งํ๋ ์ค์ํ ๋
์์ด๋ค.
setContent()
๋ผ๋ ํจ์๋ ์์์ ์ค๋ช
ํ์ผ๋ ๋์ด๊ฐ๊ณ , ์ด๋ฒ์๋ Content
๋ผ๋ ๋
์์ ๋ณด์. ์ด๋
์์ ์ถ์ ๋ฉ์๋๋ก, createComposition()
์ด๋ผ๋ ํจ์๊ฐ ํธ์ถ ๋์์ ๋, ๊ฐ์ฅ ๋จผ์ ๋ถ๋ฆฌ๋ ํจ์์ด๋ค. ์๊น ์ธ๊ธ๋์๋ ensureCompsositionCreated()
ํจ์์์ tree๊ณ์ธต์ ComposeView๊ฐ ๋ค ๋ถ์๋ค๋ฉด, ์ดํ์ ์ฆ์ Contentํจ์๊ฐ ํธ์ถ์ด๋๋ค.
@Suppress("DEPRECATION") // Still using ViewGroup.setContent for now
private fun ensureCompositionCreated() {
if (composition == null) {
try {
creatingComposition = true
composition = setContent(
parentContext ?: findViewTreeCompositionContext() ?: windowRecomposer
) {
Content() // ์ด๊ณณ์์ ๋ทฐ๊ฐ ๋ค window์ ๋ถ๊ฒ๋๋ฉด ์ฝ๋ฐฑ์ ํธ์ถํ๋ค.
}
} finally {
creatingComposition = false
}
}
}
๊ทธ๋ฌ๋ฉด ์๋ ComposeView
์ ์ค๋ฒ๋ผ์ด๋ฉ ๋ Content๊ฐ ํธ์ถ๋๋ฉด์, ๊ธฐ์กด์ ์์ฑ๋ View์ UI์์ฑ๊ณผ ๊ฐ์ Content๊ฐ ๋ถ๊ฒ๋๋ค.
/**
* The Jetpack Compose UI content for this view.
* Subclasses must implement this method to provide content. Initial composition will
* occur when the view becomes attached to a window or when [createComposition] is called,
* whichever comes first.
*/
@Composable
abstract fun Content()
Content๋ ์ค๋ช
์์ ๋ณด๋๊ฒ๊ณผ ๊ฐ์ด createComposition()
ํจ์ ํธ์ถ ํ View๊ฐ Window์ ๋ถ์ ์ดํ ์ฆ์ ํธ์ถ๋๋ค.
์ต์ข
์ ์ผ๋ก ComponentActivity.setContent(CompositionContext?, @Composable () -> Unit)
ํจ์์์ ๊ตฌํ๋ ComposeView ์ธ์คํด์ค๋ฅผ ContentLayout์ widht/height๋ฅผ wrapContentํฌ๊ธฐ๋ก ์ ํ์ฌ ContentView๋ฅผ Setํด์ฃผ๊ฒ ๋๋ค.
/**
* Composes the given composable into the given activity. The [content] will become the root view
* of the given activity.
*
* This is roughly equivalent to calling [ComponentActivity.setContentView] with a [ComposeView]
* i.e.:
*
* ```
* setContentView(
* ComposeView(this).apply {
* setContent {
* MyComposableContent()
* }
* }
* )
* ```
*
* @param parent The parent composition reference to coordinate scheduling of composition updates
* @param content A `@Composable` function declaring the UI contents
*/
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
...
else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
๊ทธ๋์, Jetpack Compose๋ ์ธ๋งํ๊ฐ?
์ธ๋งํ๋ค๊ณ ์๊ฐํ๋ค. ์๋๋ก์ด๋์์ ์ ํ ์๊ฐ์ง๋ ๋ชปํ ์ ์ธํ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ ๋์ ํ ์ ์๋ ๊ฒ์ด ์ ์ ํ๋ค๊ณ ์๊ฐํ๊ณ , ์ ์ธํ ํ๋ก๊ทธ๋๋ฐ๋ ๊ฒฐ๊ตญ ์ ํ์ด๋ ๊ฒ์ด๊ธฐ๋๋ฌธ์, ๊ธฐ์กด์ ์ ์ธํ ํ๋ก๊ทธ๋๋ฐ์ ์ฝ๊ฒ ์ ํ๋ ๊ฐ๋ฐ์๋ค ex) React, Flutter ๋ฑ๋ฑ์ ์ ํ ๊ฐ๋ฐ์๋ ๋ ์ฝ๊ฒ ์๋๋ก์ด๋ ๊ฐ๋ฐ์ ๋์ ํด๋ณด์ง ์์๊น ์ถ๋ค. ์คํ๋ ค ๋ฐ๋๋ก ๊ธฐ์กด์ XML๋ก ๋ ์ด์์ ์ฝ๋๋ฅผ ์ง๋ ๊ฐ๋ฐ์๋ค์ ์์ํ ๋ฐฉ์์ด๋ผ ์ ์ํ๋๋ฐ ์๊ฐ์ด ๊ฝค๋ ๊ฑธ๋ฆด ๊ฒ์ด๋ค.
๋ค๋ง, ์์ง๋ alpha๋ฒ์ ์ด๋ผ ๋ฐ๋ ์ ์๋ ๊ฒ์ด ๋๋ฌด๋๋ ๋ง์์ํฉ์ด๋ค. ์์ผ๋ก ์ข์ ์์๋ค์ด ๋์ค๊ณ , ์ข ๋ ๊ธฐ์กด ์๋๋ก์ด๋ ๊ฐ๋ฐ์๋ค์ด ์ต์ํด ํ ๋งํ ์ปดํฌ๋ํธ๋ก ์ ๊ณต์ด ๋๋ฉด, ์ข ๋ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ ์ธํ ํ๋ก๊ทธ๋๋ฐ์ผ๋ก ๋์ด๊ฐ ์ ์์ง ์์๊น ์ถ๋ค.
์ง๊ธ ๋น์ฅ์.. ๋ณธ์ธ์ ๊ฒฝ์ฐ ์น ํ๋ก๊ทธ๋๋ฐ๋ ๊ฒฝํ์ ํด๋ดค๋ํฐ๋ผ ์ต์ํ ์ฝ๋ํจํด์ด๊ธดํ์ง๋ง, ๋น์ฅ ๊ธฐ์กด์ ์๋ ์ฝ๋์ ๊ณต์กดํ๋ฉด์ ์ฐ๊ธฐ์๋ ์ฌ๊ฐ ๋ถํธํ ๊ฒ ๊ฐ๊ธฐ๋ํ๋ค. ์ ์ด์ Flutter์ ๊ฒฝ์ฐ ์ ์ธํ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ๋ ๋๋ง์ ์ง์ํ๊ธฐ ๋๋ฌธ์ ์ฒ์๋ถํฐ ๊ฐ๋ฐ์ ์ ์ธํ์ผ๋ก ํ๊ฒ๋๋, ๊ตณ์ด ์ด๋ด๋ฐ์๋ Flutter๋ก ์๋น์ค๋ฅผ ๋ฐ๋ก ๊ตฌํํ๋ ๊ฒ ๋ซ์ง ์์๊น๋ผ๋ ์๊ฐ์ด ๋ค์๋ค.
๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ , ์ด๋ฌํ ์๋ก์ด ์๋๋ ์๋๋ก์ด๋ ์ง์์์ ์นจ์ฒด๋์๋ ๋ถ์๊ธฐ์ ํ๊ธฐ๋ฅผ ๋์์ฃผ์๊ณ , ๊ฐ๋ฐ์์ ์ทจํฅ์ ๋ฐ๋ผ ์๋๋ก์ด๋ ํ๋ก๊ทธ๋๋ฐ์ ํ ์ ์๋๋ก ํญ์ด ๋์ด์ง ๋ถ๋ถ์์๋ ๋งค์ฐ ์ข์๊ฒ ๊ฐ๋ค.(๊ณต๋ถํ ๊ฒ ๋ ๋์๊ตฌ๋ง..)
Comments