Android Studio에서는 선언형 프로그래밍 방법으로 개발할 수 있는 툴을 제공한다.
Jetpack Compose라는 새로운 방법을 소개하려고 한다.
선언형 프로그래밍이란?
누가 만든 것을 갖다 쓴다는 것이다.
기존 코드들은 내가 필요한 함수가 있으면 하나하나 다시 정의하고 구현하는 수고가 필요하다.
그래서 코드 가독성은 떨어지게 되고 길이도 길어지게 된다.
하지만 최근들어 선언형 프로그래밍에 대한 관심이 높아지며 나타난 것이 있다.
바로 android
, iOS
, window
, linux
, web
등 크로스플랫폼을 지원하는 Flutter
이다. Flutter는 선언형 프로그래밍의 총집합이다.
dart를 거의 배우지않고 Flutter로 한 depth 들어가서 공부해도 금방 배워지는 것을 느낄 것이다.
Column(
children: [
Padding(
padding: EdgeInsets.all(20),
child: Container(width: 50, height: 50, color: Colors.blue),
),
Expanded(
child: Text(
'Hello world',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.red,
),
)
)
]
)
위 코드가 이해가 되는가?
Column
이라는 함수는 뭔가 세로로 컴포넌트를 표현할 것 같고
그 안의 children 이라는 attribute는 Column
내부로 들어갈 자식 컴포넌트를 넣어야할 것만 같지 않나?
또 다른 언어인 iOS
의 swiftUI
이다.
VStack {
Padding {
padding: 20
Color.blue
.frame(width: 50, height: 50)
}
Spacer()
Text("Hello world")
.font(.system(size: 20, weight: .bold))
.foregroundColor(Color.red)
}
필자는 swift나 objective c 같은 언어를 전혀 모르지만, 위 코드를 보면 어떤 뉘앙스로 코드를 구현했는지 이해가 된다.
Column {
Padding(
padding = PaddingValues(all = 20.dp)
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Blue)
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = "Hello world",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.Red,
modifier = Modifier.padding(horizontal = 20.dp)
)
}
위의 세가지 개발환경 모두 방식이 유사하다고 느껴졌을 것이다.
2017년 Flutter가 탄생하고, WWDC 2019에서 swiftUI가 공개되었으며, 2020년에 Android Compose가 탄생하였다.
기존의 xml과 Activity.java kt 형식의 네이티브 코드에서 선언형 프로그래밍으로 옮겼으니 더 성능이 느리지 않을까?
라고 생각했었다. 원래 가독성을 위해 캡슐 모듈화를 하면 그만큼 성능은 안 좋아지니까...
안드로이드 오픈소스 프로젝트 tivi와 sunflower로 성능 비교를 한 결과가 있다.
Tivi는 Compose only 만 하였기에 이 부분에서는 더 빠른 빌드속도와 작은 앱 사이즈로 기록되었다.
하지만 sunflower는 view 시스템과 혼용하여서 그런지 양측면에서는 크게 개선되지 않은 것처럼 보인다.
최적화를 잘했는지 기존 View 형식의 코드와 성능이 차이가 나지 않고
오히려 빠르게 측정되는 부분이 있기도 하다는 것을 알았으면 좋겠다.
Compose는 Kotlin으로 구현되었기에, Java가 아닌 Kotlin으로만 사용할 수 있다는 걸 참고하자.
뷰가 사용자에게 보이는 화면이라면 그 화면안에는 글자, 이미지 같은 많은 컴포넌트들이 존재한다.
Jetpack Compose에서는 이런 컴포넌트를 Composable하다고 표현한다.
그래서 컴포넌트들은 @Composable
이라는 어노테이션을 사용한다.
또한 Composable은 모두 대문자로 시작하는 UpperCamelCase
를 사용한다.
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greeting(name = "Android")
}
}
특정 Composable의 값을 변경해서 미리 그 결과를 알고 싶을 때가 있다.
아니면 빌드를 하지않고 테스트를 빠르게 진행하고 싶을 때도 있을 것이다.
이때 @Preview
라는 어노테이션을 사용한다.
@Preview
@Composable
fun MyComposablePreview() {
MyComposable()
}
기존의 View Style 에서는 res/drawable/~~~~.xml
에서 디자인을 확인할 수 있었지만,
Preivew 태그를 한 하위 Composable의 결과를 바로바로 볼 수 있다. (에뮬레이터 없이도)
다른 언어들은 Padding
이나 Margin
같은 새로운 컴포넌트 형식으로 제공하지만, 특이하게도 modifier
을 사용한다.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text(
text = "Hello $name!",
modifier = modifier.padding(24.dp)
)
}
}
modifier.padding(24.dp)는 모든 방향에 24dp의 공간만큼 패딩을 준다는 말이다.
수평, 수직 방향을 따로 주고 싶을 때는 Modifier.padding(horizontal = 24dp, vertical = 10dp)
이렇게 줄 수 있다.
좌상우하를 뜻하는 Modifier.padding(start =, top =, end =, bottom = )
도 가능하다.
이외에도 주어진 영역을 최대한 넓게 만드는 fillMaxSize()
, fillMaxWidth()
, fillMaxHeight()
등이 있다.
공식문서에서는 다음 처럼사용한다고 한다.
이전의 코드에서 Column이 등장했는데, 다른언어들과 마찬가지로 나열형 컴포넌트에선 리스트형식이나 for 문으로 반복이 가능하다.
@Composable
fun MyApp(
modifier: Modifier = Modifier,
names: List<String> = listOf("World", "Compose")
) {
Column(modifier) {
for (name in names) {
Greeting(name = name)
}
}
}
디자인과 관련된 설명은 자세히 알아보고 싶다면 Jetpack Compose 공식문서를 참고하자.
View style 에서는 페이지 하나에 대한 상태관리는 쉬웠다.
View style에서 MVC 패턴은 xml
이 뷰이고 이를 Activity.js kt
가 관리하기 때문에,
Activity의 클래스 내부에 멤버변수를 만들어 페이지마다 상태를 관리한다.
그리고 이 상태값을 Intent를 통해 다른 페이지로 넘겨 전역느낌으로 사용한다.
Flutter에서도 statefulWidget안에 state로 상태를 만들고, context로 다른 페이지로 전달한다.
이렇게 되면 하위 페이지로 계속 값을 전달하는 코드의 중복성이 생기기에 복잡해진다.
그래서 탄생한것이 ViewModel 패턴이고 View Style에서는 거의 MVVM 패턴을 채택한다.
Jetpack Compose에서도 이러한 State 개념이 존재한다.
@Composable
fun Greeting(name: String) {
var expanded = false // Don't do this!
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
만약 위 코드를 만들고 버튼을 누르면 아무변화가 없을 것이다.
그럴땐 아래의 State 개념을 적용해야한다.
interface MutableState<T> : State<T> {
override var value: T
}
자료형에 관계없이 데이터 그자체를 임시적으로 저장하는 것이다.
해당 상태를 Composable 안에 넣어 갱신하면된다.
해당 상태는 아래의 3가지 방법 중 한개를 사용해 선언할 수 있다.
3번째는 좀 많이 리액트 같네;; ㅎㅎ
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val expanded = remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
이제는 잘바뀐다!
Composable이 모여 하나의 페이지를 이룰 것인데, Composable 하나하나 상태가 따로 존재하면 관리하기 어렵다.
그래서 자식 Composable들의 상태를 하나의 큰 부모 Composable에 묶어 관리하는 것이 필요하다.
이렇게 상태를 한 계층 위로 올리는 것을 상태 호이스팅
이라고 한다.
실제로 React.js든 Flutter든 이 방법을 많이 사용한다. Client 개발은 다 비슷비슷한가보다..
@Composable
fun ParentComponent() {
var counter = remember { mutableStateOf(0) }
ChildComponent(
counter = counter,
onCounterIncrease = { newCounterValue ->
counter = newCounterValue
}
)
}
@Composable
fun ChildComponent(counter: Int, onCounterIncrease: (Int) -> Unit) {
Button(onClick = {
// 버튼 클릭 시 counter를 증가시킴
onCounterIncrease(counter + 1)
}) {
Text(text = "Counter: $counter")
}
}
함수 그 자체를 함수에 또 넣는 고차원 함수를 지원하는 Kotlin의 장점을 활용한 것이다.
onCounterIncrease
라는 함수라는 이벤트를 자식 Composable에게 넘기고, 자식이 이를 통해 부모의 State를 변경할 수 있다.
또한, 가로 세로 화면이 전환되면 State가 초기값이 돌아가는 현상이 있을 수 있는데
이때는 rememberSavable
로 MutualState를 관리하면 해결된다.
var expanded = rememberSaveable { mutableStateOf(false) }