Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jetpack Compose, 어디까지 알고 있을까?

Jetpack Compose, 어디까지 알고 있을까?

2022 찰스의 안드로이드 컨퍼런스에서 발표한 PPT 입니다.

미디엄에서도 확인하실 수 있습니다: https://link.medium.com/y5SWJNVhZqb

Ji Sungbin

June 19, 2022
Tweet

More Decks by Ji Sungbin

Other Decks in Programming

Transcript

  1. 2022 찰스의 안드로이드 컨퍼런스 SelectionContainer Text("선택 불기능한 Text") SelectionContainer {

    Column { Text("선택 가능한 Text - 1") Text("선택 가능한 Text - 2") } }
  2. 2022 찰스의 안드로이드 컨퍼런스 DisableSelection Column { Text("선택 불가능한 Text")

    SelectionContainer { Column { Text("선택 가능한 Text - 1") Text("선택 가능한 Text - 2") DisableSelection { Text("다시 선택 불가능한 Text - 1") Text("다시 선택 불가능한 Text - 2") } Text("선택 가능한 Text - 3") Text("선택 가능한 Text - 4") } } }
  3. 2022 찰스의 안드로이드 컨퍼런스 ClickableText val annotatedText = buildAnnotatedString {

    append("클릭 가능한 Text") } ClickableText( text = annotatedText, onClick = { offset -> toast("클릭된 오프셋: $offset") } )
  4. 2022 찰스의 안드로이드 컨퍼런스 ClickableText val annotatedText = buildAnnotatedString {

    withAnnotation( tag = "URL", annotation = "https://sungbin.land" ) { withStyle( style = localTextStyle.copy( color = Color.Green, fontWeight = FontWeight.Bold ).toSpanStyle() ) { append("성빈랜드") } } append(" 바로가기") } onClick = { offset -> annotatedText.getStringAnnotations( tag = "URL", start = offset, end = offset ).firstOrNull()?.let { annotation -> val message = """ 클릭된 URL: ${annotation.item} 클릭된 오프셋: $offset """.trimIndent() toast(message) } }
  5. 2022 찰스의 안드로이드 컨퍼런스 LazyList key LazyColumn { items(count =

    50) { number -> Text( modifier = Modifier.padding(vertical = 30.dp), text = "내 번호: $number" ) } }
  6. 2022 찰스의 안드로이드 컨퍼런스 LazyList key LazyColumn { items(count =

    50) { number -> Text( modifier = Modifier.padding(vertical = 30.dp), text = "내 번호: $number" ) } }
  7. 2022 찰스의 안드로이드 컨퍼런스 LazyList key LazyColumn { items( count

    = 50, key = { number -> number } ) { number -> Text( modifier = Modifier.padding(vertical = 30.dp), text = "내 번호: $number" ) } }
  8. 2022 찰스의 안드로이드 컨퍼런스 LazyList contentType LazyColumn { items( count

    = 50, key = { number -> number }, contentType = { 0 } ) { number -> Text( modifier = Modifier.padding(vertical = 30.dp), text = "내 번호: $number" ) } }
  9. 2022 찰스의 안드로이드 컨퍼런스 spacedBy LazyColumn { items(count = 50)

    { number -> Text( modifier = Modifier.padding(vertical = 30.dp), text = "내 번호: $number" ) } }
  10. 2022 찰스의 안드로이드 컨퍼런스 spacedBy Column( verticalArrangement = Arrangement.spacedBy( space

    = 30.dp, alignment = Alignment.CenterVertically ) ) { repeat(50) { number -> Text(text = "내 번호: $number") } }
  11. 2022 찰스의 안드로이드 컨퍼런스 LazyList contentPadding LazyColumn( contentPadding = PaddingValues(vertical

    = 30.dp), verticalArrangement = Arrangement.spacedBy(30.dp) ) { items(count = 50) { number -> Text(text = "내 번호: $number") } }
  12. 2022 찰스의 안드로이드 컨퍼런스 Arrangement, Alignment Column( modifier = Modifier.fillMaxSize(),

    verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = "가운데 배치되는 텍스트 - 1") Text(text = "가운데 배치되는 텍스트 - 2") }
  13. 2022 찰스의 안드로이드 컨퍼런스 Arrangement, Alignment •Arrangement: 레이아웃의 중심축 방향

    •Alignment: 레이아웃의 중심축 반대 방향 Row Column Arrangement Alignment
  14. 2022 찰스의 안드로이드 컨퍼런스 Arrangement [Arrangement.SpaceBetween] 첫 번째와 마지막 항목을

    각각 맨 끝에 배치하고, 나머지 항목들을 균등하게 가운데에 배치 [Arrangement.SpaceAround] 양 쪽 끝에 동일한 간격으로 패딩을 넣어주고, 가운데에는 양 쪽 끝에 들어간 패딩보다 더 큰 패딩을 사용하여 나머지 아이템들을 균등하게 배치 [Arrangement.SpaceEvenly] 모든 아이템들에 동일한 간격의 패딩을 넣어서 균등하게 배치
  15. 2022 찰스의 안드로이드 컨퍼런스 Arrangement object TopWithFooter : Arrangement.Vertical {

    override fun Density.arrange( totalSize: Int, // 배치 가능한 전체 높이 sizes: IntArray, // 배치될 아이템들의 높이 배열 outPositions: IntArray, // 아이템들이 배치된 위치가 담길 배열 ) { var y = 0 // 배치할 y 위치 sizes.forEachIndexed { index, size -> outPositions[index] = y // 아이템 배치 y += size // y 에 배치된 아이템 높이 만큼 추가 } if (y < totalSize) { // 만약 배치 가능한 공간이 남았다면 // 전체 높이에서 마지막 아이템의 높이 만큼 뺀 위치에 마지막 아이템을 재배치 outPositions[outPositions.lastIndex] = totalSize - sizes.last() } } }
  16. 2022 찰스의 안드로이드 컨퍼런스 Arrangement LazyColumn(verticalArrangement = TopWithFooter) { items(count

    = 5) { index -> Text(text = "${index}번째 항목") } item { Text(text = "© 성빈랜드") } }
  17. 2022 찰스의 안드로이드 컨퍼런스 Modifier var isInvisibleState by remember {

    mutableStateOf(false) } Box( modifier = Modifier .size(250.dp) .noRippleClickable { isInvisibleState = !isInvisibleState } .invisible(isInvisibleState) .background(color = Color.Green) )
  18. 2022 찰스의 안드로이드 컨퍼런스 Standard Modifier object InvisibleModifier : DrawModifier

    { override fun ContentDrawScope.draw() {} } infix fun then(other: Modifier): Modifier = if (other === Modifier) this else CombinedModifier(this, other) fun Modifier.invisible(isInvisible: Boolean) = when (isInvisible) { true -> then(InvisibleModifier) else -> this }
  19. 2022 찰스의 안드로이드 컨퍼런스 Composed Modifier fun Modifier.composed( inspectorInfo: InspectorInfo.()

    -> Unit = NoInspectorInfo, factory: @Composable Modifier.() -> Modifier ): Modifier = this.then(ComposedModifier(inspectorInfo, factory)) inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit) = composed { clickable( indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = { onClick() } ) }
  20. 2022 찰스의 안드로이드 컨퍼런스 Modifier var isInvisibleState by remember {

    mutableStateOf(false) } Box( modifier = Modifier .size(250.dp) .noRippleClickable { isInvisibleState = !isInvisibleState } .invisible(isInvisibleState) .background(color = Color.Green) )
  21. 2022 찰스의 안드로이드 컨퍼런스 BackHandler var isBackPressed by remember {

    mutableStateOf(false) } BackHandler(enabled = !isBackPressed) { isBackPressed = true } Text("뒤로가기 눌림: $isBackPressed")
  22. 2022 찰스의 안드로이드 컨퍼런스 remember, rememberSaveable @Composable inline fun <T>

    remember( vararg keys: Any?, calculation: @DisallowComposableCalls () -> T ): T @Composable fun <T : Any> rememberSaveable( vararg inputs: Any?, saver: Saver<T, out Any> = autoSaver(), key: String? = null, init: () -> T ): T
  23. 2022 찰스의 안드로이드 컨퍼런스 rememberSaveable - key val finalKey =

    if (!key.isNullOrEmpty()) { key } else { // 외부에 저장된 상태를 컴포지션에 매핑하는 데 사용되는 해시 값 currentCompositeKeyHash.toString(MaxSupportedRadix) }
  24. 2022 찰스의 안드로이드 컨퍼런스 rememberSaveable - saver interface Saver<Original, Saveable

    : Any> { // 값을 번들에 저장 가능한 값으로 변환 fun SaverScope.save(value: Original): Saveable? // 번들에 저장된 값을 원래의 값으로 변환 fun restore(value: Saveable): Original? } private val AutoSaver = Saver<Any?, Any>( save = { it }, restore = { it } ) fun <T> autoSaver(): Saver<T, Any> = (AutoSaver as Saver<T, Any>)
  25. 2022 찰스의 안드로이드 컨퍼런스 Saver private val AcceptableClasses = arrayOf(

    Serializable::class.java, Parcelable::class.java, String::class.java, SparseArray::class.java, Binder::class.java, Size::class.java, SizeF::class.java )
  26. 2022 찰스의 안드로이드 컨퍼런스 Saver java.lang.IllegalArgumentException: IntHolder(value=1) cannot be saved

    using the current SaveableStateRegistry. The default implementation only supports types which can be stored inside the Bundle. Please consider implementing a custom Saver for this class and pass it to rememberSaveable(). data class IntHolder(var value: Int) rememberSaveable { IntHolder(1) }
  27. 2022 찰스의 안드로이드 컨퍼런스 Saver val IntHolderSaver = Saver<IntHolder, Int>(

    save = { intHolder -> intHolder.value }, restore = { value -> IntHolder(value) } ) rememberSaveable(saver = IntHolderSaver) { IntHolder(1) }
  28. 2022 찰스의 안드로이드 컨퍼런스 rememberSaveable - Parcelable @Parcelize data class

    IntHolder(var value: Int) : Parcelable rememberSaveable { IntHolder(1) }
  29. 2022 찰스의 안드로이드 컨퍼런스 CompositionLocal? [setContent] root composable first composable

    thrid composable second composable fun main() { setContent { FirstComposable() SecondComposable() ThirdComposable() } } @Composable fun FirstComposable() {} @Composable fun SecondComposable() {} @Composable fun ThirdComposable() {}
  30. 2022 찰스의 안드로이드 컨퍼런스 CompositionLocal 생성 fun <T> compositionLocalOf( policy:

    SnapshotMutationPolicy<T> = structuralEqualityPolicy(), defaultFactory: () -> T ): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(policy, defaultFactory) fun <T> staticCompositionLocalOf(defaultFactory: () -> T): ProvidableCompositionLocal<T> = StaticProvidableCompositionLocal(defaultFactory)
  31. 2022 찰스의 안드로이드 컨퍼런스 CompositionLocal provides val LocalNumber = compositionLocalOf

    { 0 } CompositionLocalProvider(LocalNumber provides 1) { Text(LocalNumber.current.toString()) Text(LocalNumber.current.toString()) Text(LocalNumber.current.toString()) }
  32. 2022 찰스의 안드로이드 컨퍼런스 CompositionLocal nested provides CompositionLocalProvider(LocalNumber provides 1)

    { Text(LocalNumber.current.toString()) CompositionLocalProvider(LocalNumber provides 2) { Text(LocalNumber.current.toString()) } Text(LocalNumber.current.toString()) }
  33. 2022 찰스의 안드로이드 컨퍼런스 CompositionLocal providers @Stable abstract class ProvidableCompositionLocal<T>(defaultFactory:

    () -> T) : CompositionLocal<T>(defaultFactory) { infix fun provides(value: T) = ProvidedValue( compositionLocal = this, value = value, canOverride = true ) infix fun providesDefault(value: T) = ProvidedValue( compositionLocal = this, value = value, canOverride = false ) }
  34. 2022 찰스의 안드로이드 컨퍼런스 CompositionLocal providesDefault CompositionLocalProvider(LocalNumber provides 1) {

    Text(LocalNumber.current.toString()) CompositionLocalProvider(LocalNumber providesDefault 2) { Text(LocalNumber.current.toString()) } Text(LocalNumber.current.toString()) }
  35. 2022 찰스의 안드로이드 컨퍼런스 State - 1 fun main() {

    setContent { var value by remember { mutableStateOf("가") } LaunchedEffect(Unit) { delay(1000) value = "나" } ShowValue(value) } } @Composable fun ShowValue(value: Any) { val valueState by remember { mutableStateOf(value) } Text(valueState.toString()) }
  36. 2022 찰스의 안드로이드 컨퍼런스 State - 1 @Composable fun ShowValue(value:

    Any) { val valueState by remember(value) { mutableStateOf(value) } Text(valueState.toString()) } @Composable fun ShowValue(value: Any) { val valueState by remember { mutableStateOf(value) }.apply { this.value = value } Text(valueState.toString()) }
  37. 2022 찰스의 안드로이드 컨퍼런스 rememberUpdatedState @Composable fun <T> rememberUpdatedState(newValue: T):

    State<T> = remember { mutableStateOf(newValue) }.apply { value = newValue } @Composable fun ShowValue(value: Any) { val valueState by rememberUpdatedState(value) Text(valueState.toString()) }
  38. 2022 찰스의 안드로이드 컨퍼런스 State - 2 @Composable fun ItemLoad(vm:

    MainViewModel) { var loadState by remember { mutableStateOf<LoadState>(LoadState.Loading) } LaunchedEffect(vm) { loadState = vm.loadItems() } Text( text = when (loadState) { LoadState.Loading -> "Loading..." LoadState.Done -> "Done!" } ) }
  39. 2022 찰스의 안드로이드 컨퍼런스 produceState @Composable fun <T> produceState( initialValue:

    T, producer: suspend ProduceStateScope<T>.() -> Unit ): State<T> { val result = remember { mutableStateOf(initialValue) } LaunchedEffect(Unit) { ProduceStateScopeImpl(result, coroutineContext).producer() } return result }
  40. 2022 찰스의 안드로이드 컨퍼런스 produceState @Composable fun ItemLoad(vm: MainViewModel) {

    val loadState by produceState<LoadState>(initialValue = LoadState.Loading, key1 = vm) { value = vm.loadItems() } Text( text = when (loadState) { LoadState.Loading -> "Loading..." LoadState.Done -> "Done!" } ) }
  41. 2022 찰스의 안드로이드 컨퍼런스 State - 3 fun main() {

    setContent { var isEven by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(1000) isEven = true } NumberFilter(isEven) } } @Composable fun NumberFilter(isEven: Boolean) { val number = remember { 1..10 } val isEvenState by rememberUpdatedState(isEven) val filteredNumber by remember(number) { mutableStateOf( number.filter { it % 2 == when (isEvenState) { true -> 0 else -> 1 } } ) } Text(filteredNumber.joinToString(", ")) }
  42. 2022 찰스의 안드로이드 컨퍼런스 derivedStateOf fun main() { setContent {

    var isEven by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(1000) isEven = true } NumberFilter(isEven) } } @Composable fun NumberFilter(isEven: Boolean) { val number = remember { 1..10 } val isEvenState by rememberUpdatedState(isEven) val filteredNumber by remember(number) { derivedStateOf { number.filter { it % 2 == when (isEvenState) { true -> 0 else -> 1 } } } } Text(filteredNumber.joinToString(", ")) }
  43. 2022 찰스의 안드로이드 컨퍼런스 SnapshotMutationPolicy? fun <T> mutableStateOf( value: T,

    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() ): MutableState<T> = createSnapshotMutableState(value, policy) fun <T> compositionLocalOf( policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(), defaultFactory: () -> T ): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(policy, defaultFactory)
  44. 2022 찰스의 안드로이드 컨퍼런스 Snapshot System 소개 컴포지션은 특정 순서에

    구애받지 않고 무작위 병렬로 실행된다. → 하나의 State 에 여러 컴포지션이 동시에 접근할 수 있으며, 교착 상태에 빠질 수 있음 동시성 제어를 위해 MultiVersion Concurrency Control 채택 → ACID 중 I 를 구현하기 위한 방법 중 하나 ACI 를 지키며 구현된 상태 시스템이 Snapshot System
  45. 2022 찰스의 안드로이드 컨퍼런스 ACID 데이터베이스 트랜잭션간 데이터 유효성을 보장하기

    위한 속성 •원자성(Atomicity): 트랜잭션의 결과가 성공/실패 둘 중 하나로 결정돼야 한다. •일관성(Consistency): 트랜잭션을 수행하기 전과 후에 데이터베이스가 일관해야 한다. •고립성(Isolation): 각 트랜잭션은 다른 트랜잭션으로부터 자유로워야 한다. •지속성(Durablility): 트랜잭션의 결과가 영구히 저장돼야 한다. 안드로이드는 데이터를 메모리에 저장하기에 D 를 제외한 ACI 를 컴포즈 스냅샷 시스템에서 지키고 있다.
  46. 2022 찰스의 안드로이드 컨퍼런스 MVCC ACID 의 I 를 구현하는

    방법인 동시성 제어(Concurrency Control)중 하나 read, write 작업에 lock 대신 각 트랜잭션이 진행될 때 데이터베이스 스냅샷을 캡쳐하여 
 해당 스냅샷 안에서 고립된 상태로 트랜잭션을 진행한다. age = 10 [A] age = 20 (커밋 전) [B] age == 10 undo
  47. 2022 찰스의 안드로이드 컨퍼런스 MVCC age = 10 [A] age

    = 20 (커밋 전) [B] age == 10 StateObject StateRecord undo ACID 의 I 를 구현하는 방법인 동시성 제어(Concurrency Control)중 하나 read, write 작업에 lock 대신 각 트랜잭션이 진행될 때 데이터베이스 스냅샷을 캡쳐하여 
 해당 스냅샷 안에서 고립된 상태로 트랜잭션을 진행한다.
  48. 2022 찰스의 안드로이드 컨퍼런스 StateObject interface StateObject { // StateRecord

    의 LinkedList 시작점 val firstStateRecord: StateRecord // StateRecord 의 LinkedList 에 새 레코드 추가 fun prependStateRecord(value: StateRecord) // 스냅샷이 충돌했을 때 병합하기 위한 방법 fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord, ): StateRecord? = null }
  49. 2022 찰스의 안드로이드 컨퍼런스 StateRecord abstract class StateRecord { //

    StateRecord 가 생성된 스냅샷의 아이디 internal var snapshotId: Int = currentSnapshot().id // 다음 StateRecord internal var next: StateRecord? = null // 다른 StateRecord 에서 값 복사 abstract fun assign(value: StateRecord) // 새로운 StateRecord 생성 abstract fun create(): StateRecord }
  50. 2022 찰스의 안드로이드 컨퍼런스 🖨 Snapshot System 1 📸 🖨

    1 1 10 🖨 🖨 12 20 fun main() { var age by mutableStateOf(1) val snap1 = Snapshot.takeMutableSnapshot() val snap2 = Snapshot.takeMutableSnapshot() println(age) // 1 snap1.enter { age = 10 age = 12 println(age) // 12 } snap2.enter { age = 20 println(age) // 20 } println(age) // 1 }
  51. 2022 찰스의 안드로이드 컨퍼런스 Snapshot System 1 1 1 📸

    10 🖨 🖨 12 🖨 🖨 12 20 fun main() { var age by mutableStateOf(1) val snap1 = Snapshot.takeMutableSnapshot() val snap2 = Snapshot.takeMutableSnapshot() println(age) // 1 snap1.enter { age = 10 age = 12 println(age) // 12 } snap1.apply() snap1.dispose() snap2.enter { age = 20 println(age) // 20 } println(age) // 12 }
  52. 2022 찰스의 안드로이드 컨퍼런스 Snapshot System 1 1 🖨 🖨

    🖨 20 1 📸 10 🖨 12 12 fun main() { var age by mutableStateOf(1) // ... snap1.enter { age = 10 age = 12 println(age) // 12 } snap1.apply() snap1.dispose() snap2.enter { age = 20 println(age) // 20 } snap2.apply() snap2.dispose() println(age) // 12 }
  53. 2022 찰스의 안드로이드 컨퍼런스 mergeRecords // 스냅샷이 충돌했을 때 병합하는

    방법 fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord, ): StateRecord? = null
  54. 2022 찰스의 안드로이드 컨퍼런스 policy fun <T> mutableStateOf( value: T,

    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy() ): MutableState<T> = createSnapshotMutableState(value, policy) fun <T> compositionLocalOf( policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(), defaultFactory: () -> T ): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(policy, defaultFactory)
  55. 2022 찰스의 안드로이드 컨퍼런스 SnapshotMutationPolicy interface SnapshotMutationPolicy<T> { // a:

    이전 StateRecord 값 // b: 새로 만들려고 하는 StateRecord 값 // 새로운 StateRecord 값과 이전 StateRecord 값의 일치 비교 // 값이 다를때만 새로운 StateRecord 를 생성한다. fun equivalent(a: T, b: T): Boolean // previous: 스냅샷을 캡쳐했을 당시의 StateRecord 값 // current: 현재의 StateRecord 값 // applied: 적용하려고 한 StateRecord 값 // 스냅샷이 충돌했을 때 병합하는 방법 fun merge(previous: T, current: T, applied: T): T? = null }
  56. 2022 찰스의 안드로이드 컨퍼런스 SnapshotMutationPolicy object ReferentialEqualityPolicy : SnapshotMutationPolicy<Any?> {

    override fun equivalent(a: Any?, b: Any?) = a === b } object StructuralEqualityPolicy : SnapshotMutationPolicy<Any?> { override fun equivalent(a: Any?, b: Any?) = a == b } object NeverEqualPolicy : SnapshotMutationPolicy<Any?> { override fun equivalent(a: Any?, b: Any?) = false }
  57. 2022 찰스의 안드로이드 컨퍼런스 Snapshot System 1 🖨 20 📸

    12 1 1 10 12 11220 merge! fun main() { var age by mutableStateOf( value = 1, policy = object : SnapshotMutationPolicy<Int> { override fun equivalent(a: Int, b: Int) = a == b override fun merge(previous: Int, current: Int, applied: Int) = "$previous$current$applied".toInt() } ) 
 // ... snap1.enter { age = 10 age = 12 } snap1.apply() snap2.enter { age = 20 } snap2.apply() println(age) // 11220 (previous: 1, current: 12, applied: 20) }
  58. 2022 찰스의 안드로이드 컨퍼런스 Snapshot 의문점 🤔 1. 명시적 스냅샷

    생성 없이 바로 StateObject 접근하면 StateRecord 는 어디에 쌓이지? 2. StateObject 변경점을 어떻게 감지하고 리컴포지션을 하지?
  59. 2022 찰스의 안드로이드 컨퍼런스 2. Global Snapshot 변경점 감지 internal

    object GlobalSnapshotManager { private val started = AtomicBoolean(false) fun ensureStarted() { if (started.compareAndSet(false, true)) { val channel = Channel<Unit>(Channel.CONFLATED) CoroutineScope(AndroidUiDispatcher.Main).launch { channel.consumeEach { Snapshot.sendApplyNotifications() // advanced! } } Snapshot.registerGlobalWriteObserver { channel.trySend(Unit) } } } }
  60. 2022 찰스의 안드로이드 컨퍼런스 2. 일반 스냅샷 변경점 감지 fun

    takeMutableSnapshot( readObserver: ((Any) -> Unit)? = null, writeObserver: ((Any) -> Unit)? = null ): MutableSnapshot
  61. 2022 찰스의 안드로이드 컨퍼런스 Composing Snapshot private inline fun <T>

    composing( composition: ControlledComposition, modifiedValues: IdentityArraySet<Any>?, block: () -> T ): T { val snapshot = Snapshot.takeMutableSnapshot( readObserverOf(composition), writeObserverOf(composition, modifiedValues) ) try { return snapshot.enter(block) } finally { applyAndCheck(snapshot) } }
  62. 2022 찰스의 안드로이드 컨퍼런스 @file:NoLiveLiterals @file:NoLiveLiterals package land.sungbin.androidplayground class MainFragment

    : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // ... } }