Compose의 뷰 상호 운용성

1. 시작하기 전에

소개

지금까지 이 과정을 통해 Compose를 사용하여 앱을 빌드하는 방법을 자세히 알아봤으며 XML, 뷰, 뷰 결합, 프래그먼트로 앱을 빌드하는 방법도 알아봤습니다. 뷰를 사용하여 앱을 빌드한 후 Compose와 같은 선언적 UI로 앱을 빌드하는 편의성을 알게 되었을 수 있습니다. 그러나 Compose 대신 뷰를 사용하는 것이 적합한 사례도 있습니다. 이 Codelab에서는 뷰 상호 운용성을 사용하여 뷰 구성요소를 최신 Compose 앱에 추가하는 방법을 알아봅니다.

이 Codelab을 작성하는 시점에는 만들려고 하는 UI 구성요소를 아직 Compose에서 사용할 수 없습니다. 뷰 상호 운용성을 활용할 절호의 기회입니다.

기본 요건

필요한 항목

  • 인터넷 액세스가 가능하고 Android 스튜디오가 설치된 컴퓨터
  • 기기 또는 에뮬레이터
  • Juice Tracker 앱의 시작 코드

빌드할 항목

이 Codelab에서는 세 가지 뷰(Spinner, RatingBar, AdView)를 Compose UI에 통합하여 Juice Tracker 앱 UI를 완성해야 합니다. 이러한 구성요소를 빌드하기 위해 뷰 상호 운용성을 사용합니다. 뷰 상호 운용성을 사용하면 뷰를 컴포저블에 래핑하여 실제로 앱에 추가할 수 있습니다.

a02177f6b6277edc.png afc4551fde8c3113.png 5dab7f58a3649c04.png

코드 둘러보기

이 Codelab에서는 뷰를 사용하여 Android 앱 빌드뷰 기반 앱에 Compose 추가 Codelab의 JuiceTracker 앱을 동일하게 사용합니다. 이 버전과의 차이점은 제공된 시작 코드가 전적으로 Compose에 있다는 점입니다. 현재 앱에는 항목 대화상자 시트의 색상 및 평점 입력과 목록 화면 상단의 광고 배너가 누락되어 있습니다.

bottomsheet 디렉터리에는 항목 대화상자와 관련된 UI 구성요소가 모두 포함되어 있습니다. 이 패키지에는 생성 시 색상 및 평점 입력의 UI 구성요소가 포함되어야 합니다.

homescreen에는 홈 화면에서 호스팅되는 UI 구성요소가 포함되어 있으며 여기에는 JuiceTracker 목록이 포함됩니다. 이 패키지는 생성 시 최종적으로 광고 배너를 포함해야 합니다.

하단 시트, 주스 목록과 같은 기본 UI 구성요소는 JuiceTrackerApp.kt 파일에서 호스팅됩니다.

2. 시작 코드 가져오기

시작하려면 시작 코드를 다운로드하세요.

또는 코드에 관한 GitHub 저장소를 클론해도 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout compose-starter
  1. Android 스튜디오에서 basic-android-kotlin-compose-training-juice-tracker 폴더를 엽니다.
  2. Android 스튜디오에서 Juice Tracker 앱 코드를 엽니다.

3. Gradle 구성

build.gradle.kts 파일에 Play 서비스 광고 종속 항목을 추가합니다.

app/build.gradle.kts

android {
   ...
   dependencies {
      ...
      implementation("com.google.android.gms:play-services-ads:22.2.0")
   }
}

4. 설정

테스트할 광고 배너를 사용 설정하려면 activity 태그 위 Android 매니페스트에 다음 값을 추가하세요.

AndroidManifest.xml

...
<meta-data
   android:name="com.google.android.gms.ads.APPLICATION_ID"
   android:value="ca-app-pub-3940256099942544~3347511713" />

...

5. 항목 대화상자 완성

이 섹션에서는 색상 스피너와 평점 막대를 생성하여 항목 대화상자를 완성합니다. 색상 스피너는 색상을 선택할 수 있는 구성요소이고 평점 막대를 사용하면 주스에 관한 평점을 선택할 수 있습니다. 아래 디자인을 참고하세요.

여러 색상이 나열된 색상 스피너

별표 5개 중 4개가 선택된 평점 막대

색상 스피너 만들기

Compose에서 스피너를 구현하려면 Spinner 클래스를 사용해야 합니다. Spinner는 컴포저블과 달리 뷰 구성요소이므로 상호 운용성을 사용하여 구현해야 합니다.

  1. bottomsheet 디렉터리에서 새 파일 ColorSpinnerRow.kt를 만듭니다.
  2. SpinnerAdapter 파일 내에 새 클래스를 만듭니다.
  3. SpinnerAdapter의 생성자에서 Int 매개변수를 사용하는 onColorChange라는 콜백 매개변수를 정의합니다. SpinnerAdapterSpinner의 콜백 함수를 처리합니다.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit){
}
  1. AdapterView.OnItemSelectedListener 인터페이스를 구현합니다.

이 인터페이스를 구현하면 스피너의 클릭 동작을 정의할 수 있습니다. 나중에 컴포저블에서 이 어댑터를 설정합니다.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
}
  1. AdapterView.OnItemSelectedListener 멤버 함수 onItemSelected(), onNothingSelected()를 구현합니다.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        TODO("Not yet implemented")
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        TODO("Not yet implemented")
    }
}
  1. 색상을 선택하면 앱이 선택된 값을 UI에서 업데이트하도록 onItemSelected() 함수를 수정하여 onColorChange() 콜백 함수를 호출합니다.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        onColorChange(position)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        TODO("Not yet implemented")
    }
}
  1. 아무것도 선택하지 않으면 기본 색상이 첫 번째 색상인 빨간색이 되도록 onNothingSelected() 함수를 수정하여 색상을 0로 설정합니다.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
   override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
        onColorChange(position)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {
        onColorChange(0)
    }
}

콜백 함수를 통해 스피너의 동작을 정의하는 SpinnerAdapter는 이미 빌드되어 있습니다. 이제 스피너의 콘텐츠를 빌드하고 데이터로 이를 채워야 합니다.

  1. ColorSpinnerRow.kt 파일 내부 그리고 SpinnerAdapter 클래스 외부에서 새 컴포저블 ColorSpinnerRow를 만듭니다.
  2. ColorSpinnerRow()의 메서드 서명에서 스피너 위치의 Int 매개변수, Int 매개변수를 사용하는 콜백 함수, 수정자를 추가합니다.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
}
  1. 함수 내에서 JuiceColor enum을 사용하여 주스 색상 문자열 리소스의 배열을 만듭니다. 이 배열은 스피너를 채우는 콘텐츠 역할을 합니다.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }

}
  1. InputRow() 컴포저블을 추가하고 입력 라벨의 색상 문자열 리소스와 수정자를 전달합니다. 이는 Spinner가 표시되는 입력 행을 정의합니다.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
   InputRow(inputLabel = stringResource(R.string.color), modifier = modifier) {
   }
}

다음으로, Spinner를 만듭니다. Spinner는 뷰 클래스이므로 Compose의 뷰 상호 운용성 API를 활용하여 컴포저블에 래핑해야 합니다. AndroidView 컴포저블을 사용하면 됩니다.

  1. Compose에서 Spinner를 사용하려면 InputRow 람다 본문에서 AndroidView() 컴포저블을 만듭니다. AndroidView() 컴포저블은 컴포저블에서 뷰 요소 또는 계층 구조를 만듭니다.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   val juiceColorArray =
        JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
   InputRow(inputLabel = stringResource(R.string.color), modifier = modifier) {
      AndroidView()
   }
}

AndroidView 컴포저블은 다음과 같은 세 가지 매개변수를 사용합니다.

  • factory 람다. 뷰를 만드는 함수입니다.
  • update 콜백. factory에서 만들어진 뷰가 확장될 때 호출됩니다.
  • 컴포저블 modifier

3bb9f605719b173.png

  1. AndroidView를 구현하려면 먼저 수정자를 전달하고 화면의 최대 너비를 채웁니다.
  2. factory 매개변수에 람다를 전달합니다.
  3. factory 람다는 Context를 매개변수로 사용합니다. Spinner 클래스를 만들고 컨텍스트를 전달합니다.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         modifier = Modifier.fillMaxWidth(),
         factory = { context ->
            Spinner(context)
         }
      )
   }
}

RecyclerView.AdapterRecyclerView에 데이터를 제공하는 것처럼 ArrayAdapterSpinner에 데이터를 제공합니다. Spinner에는 색상 배열을 보유할 어댑터가 필요합니다.

  1. ArrayAdapter를 사용하여 어댑터를 설정합니다. ArrayAdapter에는 컨텍스트, XML 레이아웃, 배열이 필요합니다. 레이아웃에 simple_spinner_dropdown_item을 전달합니다. 이 레이아웃은 Android에서 기본값으로 제공됩니다.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         ​​modifier = Modifier.fillMaxWidth(),
         factory = { context ->
             Spinner(context).apply {
                 adapter =
                     ArrayAdapter(
                         context,
                         android.R.layout.simple_spinner_dropdown_item,
                         juiceColorArray
                     )
             }
         }
      )
   }
}

factory 콜백은 내부에서 만들어진 뷰의 인스턴스를 반환합니다. updatefactory 콜백에서 반환한 동일한 유형의 매개변수를 사용하는 콜백입니다. 이 매개변수는 factory에 의해 확장되는 뷰의 인스턴스입니다. 이 경우 Spinner가 팩토리에서 만들어졌으므로 해당 Spinner의 인스턴스는 update 람다 본문에서 액세스할 수 있습니다.

  1. spinner를 전달하는 update 콜백을 추가합니다. update에 제공된 콜백을 사용하여 setSelection() 메서드를 호출합니다.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      //...
         },
         update = { spinner ->
             spinner.setSelection(colorSpinnerPosition)
             spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
         }
      )
   }
}
  1. 앞에서 만든 SpinnerAdapter를 사용하여 update에서 onItemSelectedListener() 콜백을 설정합니다.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      AndroidView(
         // ...
         },
         update = { spinner ->
             spinner.setSelection(colorSpinnerPosition)
             spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
         }
      )
   }
}

이제 색상 스피너 구성요소의 코드가 완료되었습니다.

  1. 다음 유틸리티 함수를 추가하여 JuiceColor의 enum 색인을 가져옵니다. 이 색인은 다음 단계에서 사용합니다.
private fun findColorIndex(color: String): Int {
   val juiceColor = JuiceColor.valueOf(color)
   return JuiceColor.values().indexOf(juiceColor)
}
  1. EntryBottomSheet.kt 파일의 SheetForm 컴포저블에서 ColorSpinnerRow를 구현합니다. 색상 스피너를 'Description' 텍스트 뒤, 버튼 위에 배치합니다.

bottomsheet/EntryBottomSheet.kt

...
@Composable
fun SheetForm(
   juice: Juice,
   onUpdateJuice: (Juice) -> Unit,
   onCancel: () -> Unit,
   onSubmit: () -> Unit,
   modifier: Modifier = Modifier,
) {
   ...
   TextInputRow(
            inputLabel = stringResource(R.string.juice_description),
            fieldValue = juice.description,
            onValueChange = { description -> onUpdateJuice(juice.copy(description = description)) },
            modifier = Modifier.fillMaxWidth()
        )
        ColorSpinnerRow(
            colorSpinnerPosition = findColorIndex(juice.color),
            onColorChange = { color ->
                onUpdateJuice(juice.copy(color = JuiceColor.values()[color].name))
            }
        )
   ButtonRow(
            modifier = Modifier
                .align(Alignment.End)
                .padding(bottom = dimensionResource(R.dimen.padding_medium)),
            onCancel = onCancel,
            onSubmit = onSubmit,
            submitButtonEnabled = juice.name.isNotEmpty()
        )
    }
}

평점 입력 만들기

  1. bottomsheet 디렉터리에서 새 파일 RatingInputRow.kt를 만듭니다.
  2. RatingInputRow.kt 파일에서 새 컴포저블 RatingInputRow()를 만듭니다.
  3. 메서드 서명에서 평점에 관한 Int, 선택 변경을 처리하는 Int 매개변수가 포함된 콜백, 수정자를 전달합니다.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
}
  1. ColorSpinnerRow와 마찬가지로 AndroidView가 포함된 컴포저블에 다음 코드 예와 같이 InputRow를 추가합니다.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = {},
            update = {}
        )
    }
}
  1. factory 람다 본문에서 RatingBar 클래스의 인스턴스를 만듭니다. 이는 이 디자인에 필요한 평점 막대 유형을 제공합니다. stepSize1f로 설정하여 평점이 정수로만 적용되도록 합니다.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = { context ->
                RatingBar(context).apply {
                    stepSize = 1f
                }
            },
            update = {}
        )
    }
}

뷰가 확장되면 평점이 설정됩니다. factoryRatingBar의 인스턴스를 업데이트 콜백에 반환합니다.

  1. 컴포저블에 전달된 평점을 사용하여 update 람다 본문에서 RatingBar 인스턴스의 평점을 설정합니다.
  2. 새 평점이 설정되면 RatingBar 콜백을 사용하여, UI에서 평점을 업데이트하는 onRatingChange() 콜백 함수를 호출합니다.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
    InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
        AndroidView(
            factory = { context ->
                RatingBar(context).apply {
                    stepSize = 1f
                }
            },
            update = { ratingBar ->
                ratingBar.rating = rating.toFloat()
                ratingBar.setOnRatingBarChangeListener { _, _, _ ->
                    onRatingChange(ratingBar.rating.toInt())
                }
            }
        )
    }
}

이제 평점 입력 컴포저블이 완성되었습니다.

  1. EntryBottomSheet에서 RatingInputRow() 컴포저블을 사용합니다. 컬러 스피너 뒤, 버튼 위에 이를 배치합니다.

bottomsheet/EntryBottomSheet.kt

@Composable
fun SheetForm(
    juice: Juice,
    onUpdateJuice: (Juice) -> Unit,
    onCancel: () -> Unit,
    onSubmit: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(4.dp)
    ) {
        ...
        ColorSpinnerRow(
            colorSpinnerPosition = findColorIndex(juice.color),
            onColorChange = { color ->
                onUpdateJuice(juice.copy(color = JuiceColor.values()[color].name))
            }
        )
        RatingInputRow(
            rating = juice.rating,
            onRatingChange = { rating -> onUpdateJuice(juice.copy(rating = rating)) }
        )
        ButtonRow(
            modifier = Modifier.align(Alignment.CenterHorizontally),
            onCancel = onCancel,
            onSubmit = onSubmit,
            submitButtonEnabled = juice.name.isNotEmpty()
        )
    }
}

광고 배너 만들기

  1. homescreen 패키지에서 새 파일 AdBanner.kt를 만듭니다.
  2. AdBanner.kt 파일에서 새 컴포저블 AdBanner()를 만듭니다.

앞서 만든 컴포저블과 달리 AdBanner에는 입력이 필요하지 않습니다. 따라서 InputRow 컴포저블에 래핑하지 않아도 됩니다. 그러나 AndroidView는 필요합니다.

  1. AdView 클래스를 사용하여 배너를 직접 빌드해 봅니다. 광고 크기는 AdSize.BANNER로, 광고 단위 ID는 "ca-app-pub-3940256099942544/6300978111"로 설정해야 합니다.
  2. AdView가 확장되면 AdRequest Builder를 사용하여 광고를 로드합니다.

homescreen/AdBanner.kt

@Composable
fun AdBanner(modifier: Modifier = Modifier) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            AdView(context).apply {
                setAdSize(AdSize.BANNER)
                // Use test ad unit ID
                adUnitId = "ca-app-pub-3940256099942544/6300978111"
            }
        },
        update = { adView ->
            adView.loadAd(AdRequest.Builder().build())
        }
    )
}
  1. AdBannerJuiceTrackerAppJuiceTrackerList 앞에 배치합니다. JuiceTrackerList는 83번 줄에서 선언됩니다.

ui/JuiceTrackerApp.kt

...
AdBanner(
   Modifier
       .fillMaxWidth()
       .padding(
           top = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_small)
       )
)

JuiceTrackerList(
    juices = trackerState,
    onDelete = { juice -> juiceTrackerViewModel.deleteJuice(juice) },
    onUpdate = { juice ->
        juiceTrackerViewModel.updateCurrentJuice(juice)
        scope.launch {
            bottomSheetScaffoldState.bottomSheetState.expand()
        }
     },
)

6. 솔루션 코드 가져오기

완료된 Codelab의 코드를 다운로드하려면 다음 git 명령어를 사용하면 됩니다.

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout compose-with-views

또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.

솔루션 코드를 보려면 GitHub에서 확인하세요.

7. 자세히 알아보기

8. 마무리

이 과정은 여기서 끝나지만 이는 Android 앱 개발이라는 여정의 시작에 불과합니다.

이 과정에서는 네이티브 Android 앱을 빌드하는 최신 UI 툴킷인 Jetpack Compose를 사용하여 앱을 빌드하는 방법을 알아봤습니다. 이 과정에서는 목록과 단일 또는 여러 화면이 있는 앱을 빌드하고 이들 간을 탐색했습니다. 대화형 앱을 만드는 방법을 배웠고 앱이 사용자 입력에 응답하도록 했으며 UI를 업데이트했습니다. Material Design을 적용하고 색상, 도형, 서체를 사용하여 앱의 테마를 설정했습니다. 또한 Jetpack 및 기타 서드 파티 라이브러리를 사용하여 작업을 예약하고, 원격 서버에서 데이터를 검색하고, 데이터를 로컬로 유지하는 등의 작업을 실행했습니다.

이 과정을 완료함으로써 Jetpack Compose를 사용하여 멋진 반응형 앱을 만드는 방법을 잘 알게 될 뿐만 아니라 효율적이고 유지관리가 쉬우며 시각적으로 매력적인 Android 앱을 만드는 데 필요한 지식과 기술을 갖추게 됩니다. 이 기초는 Modern Android Development 및 Compose로 기술을 계속 배우고 발전시키는 데 도움이 됩니다.

이 과정에 참여해 주셔서 감사합니다. 다음과 같은 추가 리소스를 통해 학습을 이어 나가 기술을 발전시키길 바랍니다. Android 개발자 문서, Android 개발자용 Jetpack Compose 과정, 최신 Android 앱 아키텍처, Android 개발자 블로그, 기타 Codelab샘플 프로젝트

마지막으로, 소셜 미디어에 빌드한 내용을 공유하고 해시태그 #AndroidBasics를 사용해 보세요. 그러면 Google과 다른 Android 개발자 커뮤니티도 학습 여정을 따라갈 수 있습니다.

즐겁게 작성해 보세요.