티스토리 뷰

pathway1-2: MigrationCodelab 정리 (스터디)

기존 뷰 기반 레이아웃을 컴포즈 레이아웃으로 마이그레이션 하는 방법을 알아보자.

view-based → compose

컴포즈로 대체할 뷰 기반 레이아웃을 모두 주석 처리해주자. (xml 기준이다.)

주석으로 대체한 부분에는 아래와 같이 컴포즈 뷰를 삽입해주자. (xml 에 마이그레이션 하는 것이기 때문에 어쩔 수 없이 얘도 뷰가 되는 듯하다.)

<androidx.compose.ui.platform.ComposeView
  android:id="@+id/compose_view"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />

안드로이드 뷰 에서 컴포즈 사용하기

composeView.setContent {
    // You're in Compose world!
    MaterialTheme {
        PlantDetailDescription()
    }
}

기존 방식의 안드로이드 뷰(액티비티 등) 에서 컴포즈 레이아웃을 사용하는 방법이다.

본격적인 migration

기존에 주석처리한 텍스트뷰를 대체할 컴포저블을 정의하자.

@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.h5,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName("Apple")
    }
}

ui 마이그레이션이라 기존 뷰 속성과 상응하는 컴포저블을 작성해 주기만 하면된다.

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {

기존에 뷰모델을 사용한 데이터바인딩을 해주고 있었으므로, 동일하게 컴포저블에서도 뷰모델을 받아준다. (이전 코드랩에서는 뷰모델을 직접 받지 말라고 권고하였는데, 요 코드랩은 마이그레이션 가이드이므로 일단 넘어가주자)

composeView.setContent {
                // You're in Compose world!
                MaterialTheme {
                    PlantDetailDescription(plantDetailViewModel)
                }
            }

동일하게 setContent 에서도 전달해준다.

observe livedata

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM's LiveData<Plant> field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent(it)
    }
}

@Composable
fun PlantDetailContent(plant: Plant) {
    PlantName(plant.name)
}

뷰모델에 있는 라이브데이터를 observeAsState 로 관찰하여 컴포저블을 방출한다.

PlantWatering 컴포저블의 정의. 마찬가지로 기존 뷰와 동일하게 보이도록 해준다.

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        // Same modifier used by both Texts
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(R.dimen.margin_normal)

        Text(
            text = stringResource(R.string.watering_needs_prefix),
            color = MaterialTheme.colors.primaryVariant,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = pluralStringResource(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}

PlantDetailContent 에 기본 패딩을 추가하고, PlantWatering 를 포함시킨다.

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}

Lifecycle of ComposeView

Compose는 ComposeView가 창에서 분리될 때마다 컴포지션을 삭제한다. 이것은 여러 가지 이유로 ComposeView가 프래그먼트에서 사용될 때 바람직하지 않다. 화면 전환이 일어나는 경우에는 유지하고, 실제 라이프사이클이 제거 이벤트인 경우에만 제거되도록 수정해야한다.

Migrating to Jetpack Compose | Android Developers

composeView.apply {
      // Dispose the Composition when the view's LifecycleOwner
      // is destroyed
      setViewCompositionStrategy(
          ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
      )
      setContent {
          // You're in Compose world!
          MaterialTheme {
              PlantDetailDescription(plantDetailViewModel)
          }
      }
  }

setViewCompositionStrategy 으로 컴포즈뷰의 라이프사이클을 뷰 라이프사이클과 맞춰준다.

MdcTheme

setContent {
    // You're in Compose world!
    MdcTheme {
        PlantDetailDescription(plantDetailViewModel)
    }
}

MdcTheme 함수는 호스트 컨텍스트의 MDC 테마를 자동으로 읽고 사용자를 대신하여 밝은 테마와 어두운 테마 모두를 위해 MaterialTheme로 전달한다.

dark theme preview

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MdcTheme {
        PlantDetailContent(plant)
    }
}

PlantDescription 컴포저블 추가, html 을 보여주기 위해서는 html 을 string 으로 받아 이를 변환하여 보여주어야 한다.

@Composable
private fun PlantDescription(description: String) {
    // Remembers the HTML formatted description. Re-executes on a new description
    val htmlDescription = remember(description) {
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    // Displays the TextView on the screen and updates with the HTML description when inflated
    // Updates to htmlDescription will make AndroidView recompose and update the text
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = {
            it.text = htmlDescription
        }
    )
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MaterialTheme {
        PlantDescription("HTML<br><br>description")
    }
}

테스트 코드 작성

기존에 작성되어있던 테스트 코드를 수정해보자.

@Rule
    @JvmField
    val activityTestRule = ActivityScenarioRule(GardenActivity::class.java)

@Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<GardenActivity>()

activityTestRule → composeTestRule

activityTestRule.scenario.onActivity { gardenActivity ->
            activity = gardenActivity

            val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }

composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
            activity = gardenActivity

            val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }

activityTestRule.scenario.onActivity → composeTestRule.activityRule.

@Test
    fun testPlantName() {
        onView(ViewMatchers.withText("Apple"))
            .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
    }

@Test
    fun testPlantName() { // 화면에 표시되는 식물의 이름을 확인합니다.
        composeTestRule.onNodeWithText("Apple").assertIsDisplayed()

    }

보너스 : 공유 버튼 클릭 동작을 테스트하는 방법

공유 버튼을 탭한 후 알맞은 인텐트가 트리거되는지 확인한다.

@Test
    fun testShareTextIntent() { // 
        val shareText = activity.getString(R.string.share_text_plant, testPlant.name)

        Intents.init()
        onView(withId(R.id.action_share)).perform(click())
        intended(
            chooser(
                allOf(
                    hasAction(Intent.ACTION_SEND),
                    hasType("text/plain"),
                    hasExtra(Intent.EXTRA_TEXT, shareText)
                )
            )
        )
        Intents.release()

        // dismiss the Share Dialog
        InstrumentationRegistry.getInstrumentation()
            .uiAutomation
            .performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
    }

    // TODO: This workaround is needed due to the real database being used in tests.
    //  A fake database created with a Room.inMemoryDatabaseBuilder should be used instead.
    //  That's difficult to do in the current state of the project since there are no
    //  dependency injection best practices in place.
    private fun populateDatabase() {
        val request = TestListenableWorkerBuilder<SeedDatabaseWorker>(
            InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
        ).build()
        runBlocking {
            request.doWork()
        }
    }

Reference

 

Compose 기본사항  |  Jetpack Compose for Android Developers - Compose essentials

Jetpack Compose를 처음으로 사용해 보세요. 구성 가능한 함수, 기본 레이아웃 및 상태, Material Design, 목록, 애니메이션에 관해 알아보세요.

developer.android.com

2022.12.15 - [Android/Jetpack Compose] - [Jetpack Compose] 테마 Theming 기본 정리

2022.12.14 - [Android/Jetpack Compose] - [Android] compose basic state (상태의 기본 내용 정리)

2022.10.15 - [Android/Google Play] - 구글 플레이스토어 Android 11(target API 31) 으로 마이그레이션

댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함