1. 시작하기 전에
소개
지금까지 이 과정을 통해 Compose를 사용하여 앱을 빌드하는 방법을 자세히 알아봤으며 XML, 뷰, 뷰 결합, 프래그먼트로 앱을 빌드하는 방법도 알아봤습니다. 뷰를 사용하여 앱을 빌드한 후 Compose와 같은 선언적 UI로 앱을 빌드하는 편의성을 알게 되었을 수 있습니다. 그러나 Compose 대신 뷰를 사용하는 것이 적합한 사례도 있습니다. 이 Codelab에서는 뷰 상호 운용성을 사용하여 뷰 구성요소를 최신 Compose 앱에 추가하는 방법을 알아봅니다.
이 Codelab을 작성하는 시점에는 만들려고 하는 UI 구성요소를 아직 Compose에서 사용할 수 없습니다. 뷰 상호 운용성을 활용할 절호의 기회입니다.
기본 요건
- 뷰를 사용하여 Android 앱 빌드 Codelab을 통해 Compose 사용 시 알아야 하는 Android 기본사항을 완료합니다.
필요한 항목
- 인터넷 액세스가 가능하고 Android 스튜디오가 설치된 컴퓨터
- 기기 또는 에뮬레이터
- Juice Tracker 앱의 시작 코드
빌드할 항목
이 Codelab에서는 세 가지 뷰(Spinner, RatingBar, AdView)를 Compose UI에 통합하여 Juice Tracker 앱 UI를 완성해야 합니다. 이러한 구성요소를 빌드하기 위해 뷰 상호 운용성을 사용합니다. 뷰 상호 운용성을 사용하면 뷰를 컴포저블에 래핑하여 실제로 앱에 추가할 수 있습니다.
코드 둘러보기
이 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
- Android 스튜디오에서
basic-android-kotlin-compose-training-juice-tracker
폴더를 엽니다. - 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. 항목 대화상자 완성
이 섹션에서는 색상 스피너와 평점 막대를 생성하여 항목 대화상자를 완성합니다. 색상 스피너는 색상을 선택할 수 있는 구성요소이고 평점 막대를 사용하면 주스에 관한 평점을 선택할 수 있습니다. 아래 디자인을 참고하세요.
색상 스피너 만들기
Compose에서 스피너를 구현하려면 Spinner
클래스를 사용해야 합니다. Spinner
는 컴포저블과 달리 뷰 구성요소이므로 상호 운용성을 사용하여 구현해야 합니다.
bottomsheet
디렉터리에서 새 파일ColorSpinnerRow.kt
를 만듭니다.SpinnerAdapter
파일 내에 새 클래스를 만듭니다.SpinnerAdapter
의 생성자에서Int
매개변수를 사용하는onColorChange
라는 콜백 매개변수를 정의합니다.SpinnerAdapter
는Spinner
의 콜백 함수를 처리합니다.
bottomsheet/ColorSpinnerRow.kt
class SpinnerAdapter(val onColorChange: (Int) -> Unit){
}
AdapterView.OnItemSelectedListener
인터페이스를 구현합니다.
이 인터페이스를 구현하면 스피너의 클릭 동작을 정의할 수 있습니다. 나중에 컴포저블에서 이 어댑터를 설정합니다.
bottomsheet/ColorSpinnerRow.kt
class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
}
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")
}
}
- 색상을 선택하면 앱이 선택된 값을 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")
}
}
- 아무것도 선택하지 않으면 기본 색상이 첫 번째 색상인 빨간색이 되도록
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
는 이미 빌드되어 있습니다. 이제 스피너의 콘텐츠를 빌드하고 데이터로 이를 채워야 합니다.
ColorSpinnerRow.kt
파일 내부 그리고SpinnerAdapter
클래스 외부에서 새 컴포저블ColorSpinnerRow
를 만듭니다.ColorSpinnerRow()
의 메서드 서명에서 스피너 위치의Int
매개변수,Int
매개변수를 사용하는 콜백 함수, 수정자를 추가합니다.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
}
- 함수 내에서
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) }
}
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
컴포저블을 사용하면 됩니다.
- 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
AndroidView
를 구현하려면 먼저 수정자를 전달하고 화면의 최대 너비를 채웁니다.factory
매개변수에 람다를 전달합니다.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.Adapter
가 RecyclerView
에 데이터를 제공하는 것처럼 ArrayAdapter
는 Spinner
에 데이터를 제공합니다. Spinner
에는 색상 배열을 보유할 어댑터가 필요합니다.
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
콜백은 내부에서 만들어진 뷰의 인스턴스를 반환합니다. update
는 factory
콜백에서 반환한 동일한 유형의 매개변수를 사용하는 콜백입니다. 이 매개변수는 factory
에 의해 확장되는 뷰의 인스턴스입니다. 이 경우 Spinner
가 팩토리에서 만들어졌으므로 해당 Spinner
의 인스턴스는 update
람다 본문에서 액세스할 수 있습니다.
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)
}
)
}
}
- 앞에서 만든
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)
}
)
}
}
이제 색상 스피너 구성요소의 코드가 완료되었습니다.
- 다음 유틸리티 함수를 추가하여
JuiceColor
의 enum 색인을 가져옵니다. 이 색인은 다음 단계에서 사용합니다.
private fun findColorIndex(color: String): Int {
val juiceColor = JuiceColor.valueOf(color)
return JuiceColor.values().indexOf(juiceColor)
}
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()
)
}
}
평점 입력 만들기
bottomsheet
디렉터리에서 새 파일RatingInputRow.kt
를 만듭니다.RatingInputRow.kt
파일에서 새 컴포저블RatingInputRow()
를 만듭니다.- 메서드 서명에서 평점에 관한
Int
, 선택 변경을 처리하는Int
매개변수가 포함된 콜백, 수정자를 전달합니다.
bottomsheet/RatingInputRow.kt
@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
}
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 = {}
)
}
}
factory
람다 본문에서RatingBar
클래스의 인스턴스를 만듭니다. 이는 이 디자인에 필요한 평점 막대 유형을 제공합니다.stepSize
를1f
로 설정하여 평점이 정수로만 적용되도록 합니다.
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 = {}
)
}
}
뷰가 확장되면 평점이 설정됩니다. factory
는 RatingBar
의 인스턴스를 업데이트 콜백에 반환합니다.
- 컴포저블에 전달된 평점을 사용하여
update
람다 본문에서RatingBar
인스턴스의 평점을 설정합니다. - 새 평점이 설정되면
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())
}
}
)
}
}
이제 평점 입력 컴포저블이 완성되었습니다.
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()
)
}
}
광고 배너 만들기
homescreen
패키지에서 새 파일AdBanner.kt
를 만듭니다.AdBanner.kt
파일에서 새 컴포저블AdBanner()
를 만듭니다.
앞서 만든 컴포저블과 달리 AdBanner
에는 입력이 필요하지 않습니다. 따라서 InputRow
컴포저블에 래핑하지 않아도 됩니다. 그러나 AndroidView
는 필요합니다.
AdView
클래스를 사용하여 배너를 직접 빌드해 봅니다. 광고 크기는AdSize.BANNER
로, 광고 단위 ID는"ca-app-pub-3940256099942544/6300978111"
로 설정해야 합니다.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())
}
)
}
AdBanner
를JuiceTrackerApp
의JuiceTrackerList
앞에 배치합니다.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 개발자 커뮤니티도 학습 여정을 따라갈 수 있습니다.
즐겁게 작성해 보세요.