2022년 11월 27일 일요일

Android Jet Compose 기초

Android Jet Compose 기초

Android Jetpack Compose

Jetpack Compose는 Android 인터페이스 개발을 극적으로 간소화하는 최신 툴킷으로 Twitter, Airbnb 및 Google Play를 비롯한 수천 개의 글로벌 앱에서 사용되고 있습니다.

기존의 앱이 xml의 레이아웃을 읽어들여 그안의 뷰와 위젯들을 해석해서 화면에 출력하는 과정이었는데 이를 코드로 작업할수 있도록 해주는 라이브러리 입니다.
여기서 오해하지 않아야 될점은 Compose는 기존의 xml 로 처리하던 레이아웃을 단순히 프로그램으로 가능하도록 한것이 아니라 완전히 새로운 ui 라이브러리라고 생각해야 합니다.
호환성을 위해서 기존의 xml 에서 선언하던 내용대로 화면구성이 가능하도록 되어있고 추가로 애니메이션 데이터바인딩 등의 강력하고 작업하기 쉬운(?) 기능들이 많이 탑재되어있는 멀티디바이스에서 사용가능한 UI 라이브러리입니다.

Jetpack Compose를 사용하면 직관적이고 강력한 API를 활용하면서 소량의 코드를 사용 및 유지 관리할 수 있으며 Android를 최대한 활용하는 동시에 사용자를 위한 매력적인 경험을 구축하여 애플리케이션 개발을 더욱 가속화할 수 있습니다.

JetPack이 그밖에 제공하는것들은 아래와 같습니다.

  • NavigationFragment스위칭, 시각화, 바인딩 가능한 컨트롤, 애니메이션 지원 등 을 관리하기 위한 도구 클래스 가 장점입니다.
  • Data BindingMVVM에서는 꼭 필요한거입니다.양방향 바인딩 및 고급 바인딩 적응 기능을 제공합니다.ViewBinding의 기능도 포함됩니다.
  • LifecycleFragmentActivity의 생명주기를 활용하는 Lifecycle
  • ViewModelMVVM에서는 꼭 필요한거입니다.
  • LiveDataRxJava동일한 기능, 데이터 모니터링, 수명 주기를 다룰 필요가 없고, 메모리 누수 등이 없다는 장점이 있습니다.
  • Room:강력한 ORM 데이터베이스 프레임워크.
  • PagingRecyclerView지원되는 사용하기 쉬운 데이터 페이징 라이브러리.
  • WorkManager:유연하고 간단하며 지연되고 보장된 실행 백그라운드 작업 처리 라이브러리입니다.
  • DataStore:SharedPreferences를 대체하는 DataStore]는 현재 알파 버전입니다.

1. 선언적 UI의 과거와 현재

사실 선언적 UI는 새로운 기술이 아닙니다. 이미 2006년에 Microsoft는 XAML 마크업 언어를 사용하고 양방향 데이터 바인딩, 재사용 가능한 템플릿 및 기타 기능을 지원하는 차세대 인터페이스 개발 프레임워크인 WPF를 출시했습니다.

2010년에는 노키아가 이끄는 Qt팀도 선언적인 차세대 인터페이스 솔루션인 Qt Quick을 공식 출시했으며, Qt Quick의 원래 이름도 Qt Declarative였습니다. QML 언어는 또한 데이터 바인딩, 모듈화 및 기타 기능을 지원하고 내장 JavaScript도 지원하므로 개발자는 QML만 사용하여 간단한 대화형 프로토타입 응용 프로그램을 개발할 수 있습니다.

선언적 UI 프레임워크는 최근 몇 년 동안 빠르게 발전했으며 웹 개발분야에서 절정에 이르렀습니다. React는 선언적 UI의 견고한 토대를 마련했으며 향후 개발을 주도해 왔습니다. Flutter도 선언적 UI의 아이디어를 모바일 개발 분야에 성공적으로 가져왔습니다.

선언적 UI는 원하는 UI 인터페이스의 종류를 설명하는 것을 의미하며, 상태가 변경되면 명령처럼 단계별로 프로그램을 알려주는 대신 이전 설명에 따라 인터페이스를 다시 렌더링하여 절대적으로 올바른 상태의 인터페이스를 얻을 수 있습니다.

이제 안드로이드 개발에서는 새로운 선언적 UI 프레임워크인 Jetpack Compose는 선언적 Kotlin API를 사용하여 Android의 기존 xml 레이아웃을 대체할 수 있습니다.
다시 말하지만 Compose가 단지 xml 을 코딩으로 옮겨 적은것뿐 아니라 새로운 방식으로UI를 제작하도록 지원하는 라이브러리입니다. 반대 생각해서 안드로이드의 새로운 UI 라이브러리가 생겼고 기존의 xml에서 사용하던 속성들 일부를 위화감없이 사용하도록 도와주는 것이라고 인식하는게 맞습니다.

2. Jet Compose를 사용할때의 장점

구글의 공식사이트에서는 Compose를 이용하여 개랍할때는 아래와 같은 장점이 있을수 있다고 합니다.

  • Less Code : 더 적게 코딩하기
    • xml로 작성하는거보다 필요한 부분만 코드로 작성할수있어서 전체적인 코딩량이 감소합니다. 코드가 줄어든면 로직을 수정하거나 유지관리하기가 훨씬 수월해 집니다.
  • Intuitive : 직관적으로 UI개발하기
    • 명확하게 필요한 부분만 정의하여 코딩할수있으므로 기존의xml속성을 찾아가며 작업하지 않아도 됩니다. 또한 작성된 UI는 상태값을 프로그램에서 언제든지 수정할수 있어서 훨씬 직관적인 방식으로 설계할수있습니다.
  • Accelerate Development : 개발의 가속화
    • 재빌드를 하지않고도 미리보기 화면에서 바로 작업중인UI의 상태를 확인할수 있어서 개발을 빠르게 진행할수있습니다.
  • Powerful : 강력한 성능
    • 뷰 애니메이션, 머터리얼지다인 등 Android API에 거의 모든 기능을 지원하면서 빠르고 강력한 성능을 보장해 줍니다.

3. Compose 멀티플랫폼, 멀티디바이스

기본적으로 Compose 는 멀티디바이스/멀티플랫폼을 지향합니다.
따라서 한번 작성한 UI 코드는 별다른 수정없이도 다른 디바이스에서 거의 동일한 수준으로 보이며 같은 사용자경험을 제공할수 있도록 되어 있습니다.

4. 샘플프로젝트 생성(HelloAndroid)

일단 백분이 불여일견. Hello Android 샘플 프로젝트를 만들어 봅시다.

4.1 새로운 프로젝트 생성

File->New Project 를 선택해서 Compose Activity를 선택합니다.
enter image description here

패키지이름과 프로젝트 저장폴더를 지정하여 생성합니다.
enter image description here
로젝트는 Kotlin 최소 SDK 버전 21, Android 5.0이상 지원합니다.

4.2 MainActivity 구성

보통의 MainActivity 를 성생할때랑 다르게 activity_main.xml이 없습니다. 대신 MainActivity파일안에 Composable, Preview, 등의 어노테이션과 MainApplicationTheme, surface 등이 있습니다.

enter image description here
위의 코드에서 setContent()는 더 이상 View나 레이아웃을 전달하지 않고 구성 함수, 즉 Compose 구성 요소를 전달합니다.@Composable이 포함된 Kotlin 함수는 일반적으로 일반 함수와 구별되는 Compose 구성 요소이므로 위에서 Greeting과 DefaultPreview는 모두 Compose 구성 요소입니다.

4.4 디자인 미리보기

기존의 xml 프로젝트라면 의 text속성과 DataBinding을 이용하여 변경하여 화면에 보이는 문자열을 변경할수 있었습니다.
Compose에서는 이 모든걸 코드로 작업할수 있기때문에 @Composable로 선언된 요소들을 수정하여 바로 디자인요소를 변경할수있습니다. 변경된 사항은 @Preview 가 붙은 항목은 실시간으로 미리보기가 가능합니다.
Preview의 왼쪽에 보면 디자인 미리보기 아이콘이 있습니다.
@Preview는 Android Studio 렌더링 방식을 지원하는 매개변수를 허용합니다. 이러한 매개변수를 코드에 수동으로 추가하거나 @Preview 옆의 가장자리 아이콘enter image description here을 클릭하여 이러한 구성 매개변수를 선택하고 변경할 수 있는 구성 선택기를 표시할 수 있습니다

enter image description here

4.5 인터랙티브 모드에서는 앱의 UX를 미리 경험해 볼 수 있습니다.

미리보기화면에서 버튼을 클릭한다든지, 체크박스를 체크한다든지, 실제앱이 실행되는 환경처럼 인터랙티브환경을 미리보기 할수있습니다.
미리보기화면의 enter image description here(손가락 아이콘)버튼을 누르면 인터랙티브 모드가 된다.
Greeting의 내용을 바꾸면 인터랙티브 모드에서 입력이 가능하다.

enter image description here
인터랙티브모드는 아직 불안정합니다. 또한 아래와 같은 제한이 있습니다.

  • 네트워크에 액세스할 수 없습니다
  • 파일에 액세스할 수 없습니다

4.6 실행버튼으로 앱을 실행합니다.

enter image description here

5. Compose를 사용하기 위한 종속성 설정

compose를 사용하기 위해서는 gradle 에 아래와 같은 종속성을 추가해야 합니다.

- build.gradle(Project)
buildscript {  
  ext {  
  compose_ui_version = '1.3.0'  
  }  
}
plugins {  
  // Kotlin 1.4.30 이상이어야 합니다.
  id 'org.jetbrains.kotlin.android' version '1.6.10' apply false  
}

- build.gradle(Module)
  
buildFeatures {  
  compose true  
}  
composeOptions {  
  kotlinCompilerExtensionVersion '1.1.1'  
}

implementation 'androidx.core:core-ktx:1.9.0'  
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'  
implementation 'androidx.activity:activity-compose:1.6.1'  
implementation "androidx.compose.ui:ui:$compose_ui_version"  
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"  
implementation 'androidx.compose.material:material:1.1.1'  
testImplementation 'junit:junit:4.13.2'  
androidTestImplementation 'androidx.test.ext:junit:1.1.4'  
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'  
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"  
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"  
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"

6. Composable어노테이션

공식문서 (Codelab)에는 아래와 같이 설명되어있습니다.

이 주석을 통해 이 함수가 데이터를 UI로 변환하게 되어 있다는 것을 Compose 컴파일러에 알립니다.

위에서 말하듯이 Jetpack Compose는 composable함수를 중심으로 구축되었습니다.
당연히 Composable이 붙은 함수는 다른 Composable함수를 호출하여 UI 구성을 할수 있습니다. 쉽게말해 xml로 구성할때 LinearLayout안에 TextView, ButtonVIew등을 구성하듯이 중첩구성을 할수 있습니다.

class MainActivity : ComponentActivity() {  
  override fun onCreate(savedInstanceState: Bundle?) {  
    super.onCreate(savedInstanceState)  
    setContent {  
  MyApplicationTheme {  
  // A surface container using the 'background' color from the theme  
  Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {  
  LinearColumn("kim")  
        }  
 } }  }  
}  
  
@Composable  
fun LinearColumn(name: String) {  
  Column(  
    Modifier.padding(16.dp)  
  ) {  
  Greeting("$name 1")  
    Greeting("$name 2")  
  }  
}  
  
@Composable  
fun Greeting(name: String) {  
    Text("Hello : $name")  
}  
  
@Preview(name = "sample")  
@Composable  
fun DefaultPreview() {  
  LinearColumn("Kim")  
}

7.Preview어노테이션

Preview 어노테이션을 붙이면 코딩화면에서 바로 레이아웃을 미리보기 할수 있으며, preview run 을 통해 앱을 실행하면 composable이 지정된 UI요소들의 코딩을 변경함과 동시에 앱에서도 바로 반영되어 보여질수있습니다.
Preview기능을 이용하면 개발자나 디자이너는 이제 실제 실행되는 형태의 앱디자인을 보면서 세세한 조정을 할수 있습니다.

아래와 같이 위의 소스에 이미지와 버튼을 추가합니다.

  
  
@Composable  
fun LinearColumn(name: String) {  
  Column(  
    Modifier.padding(16.dp)  
  ) {  
  
  OutlinedButton(  
      onClick = { },  
      border = BorderStroke(1.dp, Color.Red),  
      shape = RoundedCornerShape(50), // = 50% percent  
 // or shape = CircleShape  colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.Red)  
    ) {  
  Text(text = "Save")  
    }  
  
  Image(  
      painter = painterResource(id = R.drawable.kbook2),  
      contentScale = ContentScale.Crop,  
      modifier = Modifier  
        .wrapContentWidth()  
        .border(  
          width = 1.dp, shape = RoundedCornerShape(40.dp), color = Color.Unspecified  
  ).clip(  
          RoundedCornerShape(40.dp)),  
      contentDescription = null  
  )  
  
    Spacer(  
      Modifier  
        .height(40.dp)  
        .wrapContentWidth()  
    )  
  
    Greeting("$name 1")  
    Greeting("$name 2")  
  }  
}

자 이제 ,enter image description here
처럼 작은핸드폰 아이콘처럼 생긴 버튼을 눌러서 실행합니다.
그리고 RoundCornerShape항목의 값을 모두 10으로 변경해봅니다.
변경전
enter image description here
변경후
enter image description here

Jet Compose를 사용할 이유가 하나 생겼네요.

Preview어노테이션은 동시에 여러개를 생성/지정할수있습니다.
아래와 같이 색상테마별로 미리보기를 추가합니다.

@Composable  
fun ThemeVariationSample(colors: Colors){  
  MaterialTheme(colors) {  
  LinearColumn("Kim")  
  }  
}  
  
@Preview(name = "sample", showBackground = true)  
@Composable  
fun DefaultDarkPreview() {  
  ThemeVariationSample(darkColors())  
}  
@Preview(name = "sample", showBackground = true)  
@Composable  
fun DefaultPreview() {  
  ThemeVariationSample(lightColors())  
}

아래처럼 밝은/어두운 테마에서 동시에 어떻게 보이는지 확인할수 있습니다.
enter image description here

이밖에 uiMode, device,locale등 다양한 환경변수를 적용하여 UI가 어떻게 보이는지 확인할수 있습니다.

8.Modifier속성

Modifier(수정자)는 표준 Kotlin 객체인 Composable 수정자입니다.

과거에는 레이아웃에서 컨트롤의 크기, 간격, 클릭 이벤트, 너비와 높이, 배경 및 기타 속성 값을 xml 을 통해 설정했습니다. Compose에서는 컨트롤의 속성 구성을 위한 도구 클래스에 해당하는 Modifier를 통해 설정합니다.
쉽게 생각해서 기존의 각 뷰나 위젯의 xml속성을 Modifier에서 한다고 생각하면됩니다. 추가로 데이터바인딩, 이벤트속성등을 좀더 쉽게 지정할수도 있습니다.

Modifier는 대략 다음과 같은 기능을 가지고 있습니다.

  • 구성 가능한 항목의 크기, 레이아웃, 동작 및 모양을 업데이트할 수 있습니다.
  • 상호 작용을 추가합니다. 클릭, 스크롤, 드래그 가능, 확대/축소 등
  • 사용자 입력 처리
  • 접근성 레이블과 같은 정보 추가

Modifier에는 수많은 속성이 있습니다. 너무 내용이 많아서 공식페이지를 참고 하시는게 좋습니다.
https://developer.android.google.cn/reference/kotlin/androidx/compose/ui/Modifier

https://developer.android.google.cn/jetpack/compose/modifiers-list

공식페이지를 보면 알겠지만, Modifier의 확장함수 (체인호출로 순서에 영향을 받습니다) 로 많은 기능이 이미 구현되어있습니다.물론 개발자가 원하는 확장함수를 추가하여 팀내에서 공유해서 작업할수도 있습니다.

@Composable  
fun Greeting(name: String) {  
  Text("Hello : $name", Modifier  
    .squareBox()  
  )  
}  
  
@Stable  
fun Modifier.squareBox() = this.then(  
  width(100.dp).height(150.dp)  
)

각 UI 객체들은 modifier를 인수로 받아서 화면에 지정된 속성으로 출력하게 되어있는데, 이는 실행중에 UI 객체의 속성을 변경할수 있다는 것을 말합니다. 물론 기존에도 소스코드에서 UI 객체의 속성을 변경할수있었지만 한번에 하나의 속성만 변경하는 불편한점이 있었습니다. compose에서는 각 상태에 맞는 modifier를 미리 만들어 두어 여러가지 속성을 한번에 변경적용할수 있습니다.

modifier에서 한가지 재밌는것이 있는데, modifier의 소스코드를 보면 modifier의 인터페이스에 아래와 같이 정의되어 있습니다.

interface Modifier {
...
infix  fun then(other: Modifier): Modifier = if (other === Modifier) this  else CombinedModifier(this, other)
}
```kotlin
modifier에 각 속성(확장함수로 구현된 속성들)을 체인으로 연결하여 추가할수있는데, 체인의 순서에 따라서 결과물이 다르게 표현될수도 있으므로 속성정의 할떄 순서에 주의를 할 필요가 있습니다.
예를 들어 padding속성의 경우 아래와 같이 할경우, 해당 UI객체의 외부와 내부에 padding을 줄수 있습니다.
```kotlin
Column(
modifier = Modifier 
.fillMaxWidth()
.padding(4.dp) // 객체 외부에는 4dp 만큼 간격띄우기  
.border(1.dp, Color.LightGray, RoundedCornerShape(4.dp))
.padding(8.dp) // 객체 내부에는 8dp 만큼 간격띄우기
)
  

@StableMaker( @Stable, @Immuable)

@Stable @Immuable 은 Compose가 재구성 성능을 개선하는 데 도움이 될 수 있는 Compose 관련 유형 안정성 관련 주석입니다.
Compose는 각각의 배치된 UI객체들이 매프레임마다 다시 계산하지 않도록 내부적으로 캐시를 이용하고 있습니다. 이때 관찰중인 변수가 변경될때 해당 객체를 재구성(recomposition)단계에 추가하여 화면갱신시에 새로운 형태로 갱신하도록 합니다.
이를 위해서는 객체가 보유한 값(val 또는 mutableState)와 새로운 값을 비교, 즉 equal로 값을 비교해서 변경이 있을때는 재구성을 합니다.
예를들어 리스트뷰에서 데이터를 가지고 있는 아이템들이 화면의 스크롤될때마다 재구성을 하게되면 매우 비효율적이기 때문에 Satable을 지정하여 값이 변경되는 시점에만 해당 아이템을 재구성에 참여하도록 해야합니다.
그리고 값이 절대로 변하지 않는 요소는 Immutable을 지정하여 재구성시에 참여하지 않도록 하여 화면을 구성하고 그리는 시간을 단축할수 있습니다.

컴파일러는 다음 유형을 안정적인 유형으로 자동 인식합니다.

  • Kotlin, Boolean, Int, Long, Float, Char 등의 기본 유형
  • 문자열 유형
  • 다양한 함수 유형, Lambda
  • 모든 공용 속성은 최종(val 선언) 객체 유형이며 속성 유형은 변경 불가능한 유형 또는 관찰 가능한 유형입니다.

위의 사양을 따르지 않는 타입은 불안정한 타입이지만 수동으로 @Stable 또는 @Immutable 어노테이션을 추가하여 컴파일러가 안정한 타입으로 취급하도록 할 수 있습니다. 유형은 가변적이지만 추적 가능한 변경 사항입니다.

Stable

사용자정의 데이터등은 값이 변경되지 않더라도 화면에 재구성 하는 불안정한(unstable)상태가 되어 불필요하게 화면갱신에 필요한 리소스를 잡아먹을수 있습니다. 따라서 Stable 어노테이션을 지정하여 변경이 되는 요소이며 재구성이 필요한 경우 이쪽에서 반대로 알리겠다고 선언합니다.

val str = remember { CustomDataObject(0) }  
var state by remember { mutableStateOf(false) }  
if (state) {  
  str.data++  
  state = false  
}  
Button(onClick = { state = true }) {  
  WrapperText(str)   
}

@Composable  
fun WrapperText(data: CustomDataObject) {  
  Text("Hello ! ${data.data}")  
}

data class CustomDataObject(var data: Int)  
@Stable class CustomDataObjectStable(data: Int){  
  var data by mutableStateOf(data)  
}
// or
// data class CustomDataObjectStable(  
//   val data: MutableState<Int> = mutableStateOf(0)  
// )
// or 
// @Stable data  class CustomDataObjectStable(val  data: String)

CustomDataObject를 str에 대입하면 compose는 재구성시에 WrapperText를 계속 재구성합니다. 반면 CustomDataObjectStable을 대입하면 값이 변경이 있을경우에만 재구성을 합니다.

이처럼 안정적인 유형으로 하기위해서는 @Stable 어노테이션 또는 불변갑(val) ,mutableStateOf 로 변수할당 하는 등의 방법이 있습니다.

Immuable

불변의 요소는 재구성시에 매번 참여하지않도록 @Immuable 어노테이션을 지정합니다.

상태관리 mutableStateOf, remember

구글의 Compose상태관리를 먼저 읽어보세요. Link

Composable 어노테이션이 붙은 함수는 수시로 재시작 되기 때문에 함수안에서의 변수들은 항상 초기화 됩니다. 이를 방지하기 위해서 remember 와 state를 이용하여 객체가 다시 그려질때에도 저장된 상태값으로 다시 그려질수 있도록 합니다.

Compose는 State를 사용하여 상태를 관리합니다(대부분의 경우 remember()와 결합해야 함).
mutableStateOf()는 변경 가능한 State를 반환합니다. State는 이 변수가 Compose가 관리해야 하는 "상태"임을 나타냅니다. 상태가 변경되면 이를 감지하고 재구성을 트리거해야 합니다. 실제 사용에서 State는 일반적으로 Remember 기능과 함께 사용되며, Remember는 재구성이 발생할 때 마지막 State를 기억할 수 있음을 의미합니다.

mutableStateOf

선언적 UI에서는 기존처럼 setText,등과 같이 직접적으로 객체에 값을 지정하지 않도록 합니다. UI 객체는 선언만 되어있을뿐 표시데이터는 외부에서 제어하도록 합니다.
Composable은 mutableStateOf로 등록된 변수를 관찰하며, 값이 변경되면 리컴포지션 객체의 갱신을 요구하게됩니다.
mutableStateOf 선언은 아래와 같이 3가지 정의 방식이 있습니다.

val mutableState = remember { mutableStateOf(default) } 
var value by remember { mutableStateOf(default) } 
val (value, setValue) = remember { mutableStateOf(default) }

변수를 모니터링하고 상태가 변경되면 다시 그리기를 트리거합니다.

아래의 코드는 Text객체(뷰가 아닙니다.객체입니다)에 mutableStateOf로 지정된 값객체의 변화에 따라 자동으로 문자열이 바뀌는 선언적 UI의 예제입니다.

val name = mutableStateOf("default value") 
setContent { Text(name.value) } 
lifecycleScope.launch { 
  delay(3000) 
  name.value = "after 3 second" 
}

mutableStateOf말고도 다른 형태의 관찰가능한 기능(라이브러리)를 사용하여 Compose가 변수를 관찰하도록 하는것도 가능합니다. 기존의 라이브러리나 작업방식이 있숙하다면 굳이 mutableStateOf을 사용하지 않고도 아래의 3개의 외부 기능을 AsState 함수로 Compose가 관찰가능하도록 할수 있습니다.

  • LiveData : observeAsSState 등
  • Flow : collectAsState 등
  • Rx2Java : subscribeAsState등

derivedStateOf

정의된 객체 상태가 다른 객체 상태에 의존할 때 사용합니다. 종속 객체의 상태가 변경되면 자체적으로 변경될 수도 있습니다. derived 즉 다른 state 객체에서 파생해서 해당 객체를 주시하는 새로운 상태의 객체를 만들때 사용합니다.

var age by remember { mutableStateOf(0) }  
val person by remember {  
  derivedStateOf { "내 나이는 $age" }  
}  
Column(modifier = Modifier.padding(16.dp))  {  
  Button(onClick = {  
  age += 1  
  }) { 
	  Text(text = person)  
  }  
  Text(text = person)  
}

위에서 derivedStateOf를 지정하지 않는다면 항상 “내 나이는 0” 으로 출력됩니다.

remember

mutableStateOf는 마지막 상태를 가지고 있습니다. 그런데 Compose의 특성상 UI를 다시그리는,즉 Surface가 갱신된다든지, 일부 객체를 갱신할때 기존의 값을 보존해야 될 필요성이 있는데 이 때 이용하는 것이 remember 키워드입니다.

아래의 예제에서 변수의 변경에 따라서 객체가 갱신되면서 txt 값이 다시 초기의 값으로 설정된채로 되어 예상했던 “바뀐값” 는 보이지 않게됩니다.

Column(modifier = Modifier.padding(16.dp)) {  
  var txt by  mutableStateOf("초기값") 
  
  OutlinedTextField(  
    value = txt,  
    onValueChange = { txt = it },  
    label = { Text("Name") }  
  )  
  
  Button(  
    onClick = {  
  txt = "바뀐값"  
  },  
  ) {  
  Text(text = "값바꿈")  
  }  
}

자,이제 remember 를 사용하여 버튼클릭시 객체가 다시 그려질때 기억하고 있는 값을 이용하도록 해봅시다. 아래처럼 remember 키워드를 넣어주면 됩니다.

  var txt by remember { mutableStateOf("초기값") }

ViewModel에서 사용할떄는 아래와 같은 형식으로도 가능합니다.

val txt by remember { viewModel.textState }

rememberSaveable 로 상태복원

remember 와 mutableStateOf로 앱/액티비티가 활성화 된 상태에서 화면 갱신할때는 아무런 문제 없습니다. 하지만 액이 백그라운드상태, 또는 재시작, 화면회전 되는 상태에서는 이미 remember 된 요소들은 메모리에서 해제되었기때문에 화면이 최종상태로 복원되지 않습니다.
rememberSaveable은 remember 할수 있는 값들을 Bundle형태(경우에따라서 parcelize 추가사용)로 저장하여 Compose가 번들을 통해서 저장되었던 값으 읽어들여 화면을 최종상태로 재구성할수 있습니다.

@Parcelize data class User(val name:String,val age:Int): Parcelable 
var user = rememberSaveable { mutableStateOf(User("Korea", 3)) }

이밖에 키와 값쌍으로 구성된 여러개의 세트값을 저장하는 mapSaver 와 ListSaver를 이용할수 있습니다.

Stateful 과 Stateless

Stateful은 말그대로 상태를 가지고있는 Composable객체를 말합니다. 각각의 객체가 상태를 가지고 있으면 개발시에는 편리하겠지만, 객체가 항상 상태값에 의존하기 때문에 확장유지관리와 테스트하기가 어렵습니다.
따라서 되도록 Stateless 로 만들어 처리와 데이터를 분리시키는것이 좋습니다.
상태값을 이용해야 되는 Composable객체를 Stateless, 즉 상태값을 보유하지 않도록 하는 방법에는 Hoisting(끌어올리다)라는 방법을 사용합니다.
호이스팅이란 , 상태값을 이용하는 객체에서 상태값의 변경이 필요할때는 값의 변경이벤트를 윗단계, 즉 호출자에게 위임시켜 값을 변경시키고록 이벤트를 전달고 이벤트처리가 된 값을 다시 받아서 바뀐값을 이용만하는 과정을 거치는 것을 말합니다. 호이스팅은 일급함수가 지원되는 언어들에게 주로 사용방법으로 코틀린은 일급함수를 인수로 전달가능하기에 호이스팅이 가능한것입니다.
앞서 링크한 페이지에서 구글이 제공하는 예제 코드를 봅시다.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

HelloContent의OutlinedTextField객체를 클릭하는순간, 일급함수로 넘어온 onNameChange가 HelloScreen에서 지정한 (name = it) 대로 name변수에 입력박스의 값을 대입합니다.
동시에 name은mutableStateOf이므로 composable에서 관찰자로 자동등록되어있기때문에, 값이 변경됨과 동시에 composable객체들을 재구성(리콤포지션) 하게되어 변경된 name 값이 바뀐화면에 출력되게 됩니다.

  • mutableStateOf로 name 값의 변경을 compose가 관찰
  • 입력박스에 입력시 onValueChange 가 발생하여 onNameChange 일급함수의 내용을 실행
  • 관찰중인 name 값이 벼경되었으므로 화면 재구성(갱신)

위와같이 HelloContent를 아무런 상태를 보존하지 않는 Stateless 상태로 만들고 단지 이벤트를 넘겨주는 것으로 그것을 호출하는 상위함수 HelloScreen 에서 Stateful 상태로 데이터를 갱신할수 있습니다.
기존의 view-viewmodel에서 livedata, flow 등으로 처리할때 뷰에 데이터의 상태를 저장하지 않는것과 크게 다르지 않습니다.

UI 요소

앞서 Compose 예제에서 보았듯이 @Composable 어노테이션을 붙인 함수에 compose에서 미리 정의된 UI 요소들을 사용할수 있습니다.
기존의 xml에서 가능했던 요소들은 대부분 그대로 사용가능합니다.
레이아웃을 학습할수 있는 공식 codelab 사이트에서 단계별로 따라하며 레이아웃을 익힐수 있습니다.

Surface

머터리얼 디자인스타일 요소로서 높이, 그림자, 외부모양(클리핑포함), 배경등을 사용자가 설정할수 있도록 해줍니다.

MaterialTheme

메터리얼 테마를 구성하도록 합니다.
기존의 xml에서 아래와 같이 구현 것을 이 함수로 정의 합니다. 물론 사용자정으 ㅣ테마도 구성할수있습니다.

MaterialTheme( 
typography = type, 
colors = colors, 
shapes = shapes ) {  
// Surface, Scaffold, etc 
}
<style name="Theme.MyApp" parent="Theme.AppCompat.DayNight"> <item name="colorPrimary">@color/purple_500</item> <item name="colorAccent">@color/green_200</item> </style>

표준 Layout (Column, Row, Box,BoxWithConstraints)

Compose에서 UI요소를 구성하는 기본 레이아웃은 Column, Row, Box ,BoxWithConstraints가 있습니다.
단어를 보면 알수 있듯이 열,행,상자(Frame레이아웃같은)를 의미합니다.
기존의 ConstraintLayout과 비슷한 것은 BoxWithConstraints입니다.

Lazy Layout (LazyColumn, LazyRow, LazyVerticalGrid)

기본 레이아웃은 Column, Row, Box 에 vertical(horizontal)Scholl을 추가한다면 기본 레이아웃으로도 스크롤리스트 를 구현할수있습니다.
그러나 아이템이 100개 정도 되었을때는 기본레이아웃은 초기부터 모든 데이터를 화면에서 보이지도 않는 부분에서도 표시하려고 하기 때문에 퍼포먼스에 좋지 않습니다.
Lazy계열 레이아웃들은 기존의 RecycleView 처럼 똘똘하게 화면에 보이는 아이템과 보이지 않는 아이템을 구분하여 줌으로서 화면에 표시되야할 아이템만 재구성에 참여시켜 화면을 갱신하여 줍니다.

Card Layout

카드형태로 아이템을 표시하기 위한 레이아웃입니다.

Card Layout

카드형태로 아이템을 표시하기 위한 레이아웃입니다.

Scaffold

Scaffold기본 머티리얼 디자인 레이아웃 구조와의 인터페이스를 구현할 수 있습니다. Scaffold 는 안드로이드앱의 전형적인 레이아웃인 topBar,bottomBar,drawer,floatingActionButton 등을 간단히 구현할수 있는 구조로 되어있습니다.
앱이 표준 머터리얼 디자인 레이아웃을 준수하고자 한다면 간단하게 Scaffold에 각 UI요소를 지정함으로서 화면구성을 쉽게 한ㄹ수있습니다.

@Composable
fun Scaffold(
    modifier: Modifier = Modifier,
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    isFloatingActionButtonDocked: Boolean = false,
    drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
    drawerGesturesEnabled: Boolean = true,
    drawerShape: Shape = MaterialTheme.shapes.large,
    drawerElevation: Dp = DrawerDefaults.Elevation,
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    drawerScrimColor: Color = DrawerDefaults.scrimColor,
    backgroundColor: Color = MaterialTheme.colors.background,
    contentColor: Color = contentColorFor(backgroundColor),
    content: @Composable (PaddingValues) -> Unit
): Unit

Spacer

xml 에서 사용하던 margin 이 없어졌지 때문에 Spacer 객체를 통해서 요소간의 간격을 띄울수 있습니다.

Devider

구분선 UI입니다.

Tab

Tab계열의는 LeadingIconTab, Tab, TabRow, ScrollableTabRow가 있습니다.

Poptup (Dialog)

Dialog계열는 # Dialog, Popup, DropdownMenu, DropdownMenuItem가 있습니다.

Checkbox, Switch, Button, Text, InputTextField 등 UI요소

기존에 사용하던 Checkbox, Switch, Button, Text, InputTextField 등의 기본 UI요소도 잇습니다.

Compose 와 코루틴

Jetpack Compose는 인터페이스 레이어에서 코루틴을 안전하게 사용할 수 있는 API를 제공하며, rememberCoroutineScope 함수는 이벤트 처리 스크립트에서 코루틴을 생성하고 Compose Suspend API를 호출하는 데 사용할 수 있는 CoroutineScope를 반환합니다.

onClick = {  
  composableScope.launch {  
  // 중단 함수(suspend) 실행  
  }  
}

Effect 계열의 함수

Compose에서 코루틴을 실행하기 위한 방법으로 XXXEffect 계열의 함수가 있습니다.
LaunchedEffect, DisposableEffect, SideEffect 가 있는 데 이름에서 알수있듯이 Compose가 재구성될때, 파괴될때, 부가효과가 생길때 특정 코드블럭(suspend블럭)을 실행합니다.

LaunchedEffect

단순히 말하면 Compose 에서 launch{}함수입니다. 사용법은 매우 간단합니다.

@Composable
fun test() {
    Text(text="test)
    LaunchedEffect(Unit) { 코드내용-suspend block }
    LaunchedEffect(또는 key) { 코드내용-suspend block }
}

LaunchedEffect를 특정 조건에서 실행하고자 한다면 null 대신에 Key가 될만한 값을 지정하면 해당 키가 유효할때만 블럭내의 함수를 실행하게 됩니다.
만일 블럭을 실행하고 있는 도중에 key 값이 유효하지 않게된다면 해당 시점에서 블럭을 중단( cancel) 시킵니다.
LaunchedEffect는 요소의 재구성과 상관없이 별도의 코루틴에서 실행되며 오로지 Key갑의 변화에의해 실행 여부가 구분됩니다.

DisposableEffect

요소가 Composition을 벗어날때 실행할 블럭을 정의합니다.
Composable라이프사이클에서 더이상 필요가 없게된 요소들은 onDispose를 실행하는데, DisposableEffect는 사용자가 별도로 처리하고자하는 작업을 추가할수있습니다.

DisposableEffect(LocalLifecycleOwner.current) {
   // 처리할 내용
   ...    
   // 먼저 현재앱의 Android LifeCycle을 구독하는 옵져버를 생성하고 해당 라이프사이틀에서 처리해야할 내용이 있다면 추가로 적습니다.
   val observer = LifecycleEventObserver { _, event ->{라이프사이클에서 처리할내용}   }
   lifecycleOwner.lifecycle.addObserver(observer)
   // 현재 요소가 파괴될 단계에서는 아까 추가한 옵져버를 해제하여 줍니다.
   onDispose {
       lifecycleOwner.lifecycle.removeObserver(observer)
   }
}

SideEffect

요소가 재구성 될때 실행할 블럭을 지정합니다. LaunchedEffect는 재구성등과 관계없이 요소가 실행(Active)되는 순간에 블럭을 실행하지만 SideEffect는 요소의 재구성(Recomposition)될때에 매번실행하게 됩니다.

SideEffect {
}

0 comments:

댓글 쓰기