Android KTX, DataBinding으로 View Layer 코드 줄이기 ✂️
DevFest Korea Android 2019에서 발표한 내용을 축소하여 글로 정리합니다.
고민의 시작
Android 앱을 개발할 때, View의 visibility를 제어하는 경우가 많습니다.
예를 들어, Loading View를 표시할 때는 아래처럼 if 문을 사용합니다.
val loadingView: View = findViewById(R.id.loading_view)
loadingView.visibility = if (isLoading) View.VISIBLE else View.GONE
이 코드를 아래처럼 간결하게 처리할 수는 없을까요?
loadingView.isVisible = isLoading
다음으로 Layout XML을 살펴봅시다.
흔히 사용하는 기본 visibility를 설정하는 코드입니다.
<View
android:id="@+id/loading_view"
android:visibility="visible " />
Java나 Kotlin 코드처럼 레이아웃에 코드를 넣을 수 있으면 어떨까요?
DataBinding을 이용하면 가능합니다.
<View
android:id="@+id/loading_view"
android:visibility="isLoading ? View.VISIBLE : View.GONE" />
그럼 더 간결하게 사용할 수 없을까요?
<View
android:id="@+id/loading_view"
android:visibleIf="isLoading " />
이 글에서 방법을 알아봅니다. 👀
Android KTX 🚅
Android KTX는 Android Jetpack 혹은 Android Libraries의 코틀린 확장기능으로, 다양한 Kotlin 언어 기능들을 활용하고 있습니다. Android KTX 중에 유용하게 사용할 만한 것들을 간단히 살펴봅시다.
Core KTX
implementation ‘androidx.core:core-ktx:1.1.0’ // 1.2.0-rc01
첫번째는 View 기능입니다.isVisible
/ isInvisible
/ isGone
과 같은 확장 속성을 제공합니다.
loadingView.visibility = if (isLoading) View.VISIBLE else View.GONE// Core KTX
loadingView.isVisible = isLoading
두번째는 Html Text 기능입니다.
예를 들어, <b>bold</b>string
를 boldstring
으로 표시해야 한다고 합시다.
안드로이드 플랫폼 API 대신 KTX를 사용할 수 있습니다.
val htmlString = "<b>bold</b>string"
textView.text =
HtmlCompat.fromHtml(htmlString, FROM_HTML_MODE_LEGACY)// Core KTX
textView.text = htmlString.parseAsHtml()
세번째는 SpannableStringBuilder 기능입니다.
문자열을 조합하여 Span을 적용할 때, 실수하기 쉬운 코드를 만듭니다.
KTX를 사용하여 Index 실수를 줄일 수 있습니다.
val ssb = SpannableStringBuilder().apply {
append("bold")
setSpan(StyleSpan(Typeface.BOLD), 0, 4, SPAN_INCLUSIVE_EXCLUSIVE)
append("string")
}// Core KTX
val ssb = buildSpannedString {
bold { append("bold") }
append("string")
}
네번째는 Bundle 기능입니다.
타입에 따라 다른 API를 사용하던 코드를,
KTX를 사용하면 값만 남도록 코드를 최소화할 수 있습니다.
arguments = Bundle().apply {
putInt("EXTRA_ID", id)
putCharSequence("EXTRA_TITLE", title)
}// Core KTX
arguments = bundleOf(
"EXTRA_ID" to id,
"EXTRA_TITLE" to title
)
Collection KTX
implementation 'androidx.collection:collection-ktx:1.1.0'
Android는 플랫폼에 최적화된 자료구조를 제공하고 있습니다.
SparseArray, ArraySet, ArrayMap 등이 해당됩니다.
Kotlin에서 자료구조를 초기화하는 코드와 동일하게,
Android Collection을 초기화할 수 있습니다.
val set = setOf("one", "two", "three")
val map = mapOf(1 to "one", 2 to "two")// Collection KTX
val arraySet = arraySetOf ("one", "two", "three")
val arrayMap = arrayMapOf (1 to "one", 2 to "two")
Activity, Fragment, Navigation KTX
implementation
'androidx.activity:activity-ktx:1.0.0' // 1.1.0-rc02
'androidx.fragment:fragment-ktx:1.1.0' // 1.2.0-rc02
'androidx.navigation:navigation-fragment-ktx:2.1.0' // 2.2.0-rc02
AAC ViewModel을 사용하고 있다면, 초기화하는 코드를 개선할 수 있습니다.
private lateinit var viewModel: HomeViewModeloverride fun onCreateView(...): View? {
viewModel = ViewModelProviders.of(this)
.get(HomeViewModel::class.java)
}// with KTX
private val viewModel: HomeViewModel by viewModels()
여기서는 극히 일부의 코드만 다뤘습니다.
최근에 KTX 정보만 따로 볼 수 있는 페이지가 추가되었으니,
Preference, Room, WorkManager, Test 등에서 KTX 사용을 고려해보세요.
(참고로 Firebase KTX, Play Core KTX도 있습니다.)
DataBinding 🔗
DataBinding은 선언적인 형태로 데이터와 UI 요소들을 묶을 수 있게 해주는 라이브러리입니다. findViewById를 사용하지 않아도 되고, BindingAdapter 등을 이용해서 Layout XML에 Custom Attribute를 추가할 수 있습니다.
DataBinding을 사용하려면 아래와 같은 코드를 추가해야 합니다.
// app/build.gradle
android {
dataBinding {
enabled = true
}
}
Example
먼저 간단한 코드로 DataBinding을 살펴보겠습니다.
val loadingView = findViewById(R.id.loading_view)
loadingView.visibility = if (isLoading) View.VISIBLE else View.GONE<View
android:id="@+id/loading_view"
android:visibility="gone" />
DataBinding을 사용하려면 기존 XML 코드를 <layout>
태그로 감싸줘야 합니다. 그리고 코드에 관련된 import
/ variable
등의 추가 선언이 필요합니다.
<layout>
<data>
<import type="android.view.View"/>
<variable name="isLoading" type="boolean" />
</data> <ViewGroup>
<View
android:id="@+id/loading_view"
android:visibility="@{isLoading ? View.VISIBLE : View.GONE}"/>
</ViewGroup>
</layout>
더 적은 코드로 동일한 동작을 하도록, 이 코드를 개선해봅시다.
DataBinding: visibility
ViewModel에서는 isLoading
을 노출하고,
Activity에서는 Binding 클래스에 ViewModel 객체를 전달합니다.
BindingAdapter를 이용하여, import
가 없는 코드로 변경할 수 있습니다.
아래와 같은 BindingAdapter를 사용해보세요.
@BindingAdapter("android:visibleIf")
fun View.setVisibleIf(value: Boolean) {
isVisible = value
}@BindingAdapter("android:invisibleIf")
fun View.setInvisibleIf(value: Boolean) {
isInvisible = value
}@BindingAdapter("android:goneIf")
fun View.setGoneIf(value: Boolean) {
isGone = value
}
DataBinding: click
View에서 발생한 UI Event를 ViewModel에 전달하는 경우입니다.
val item: Item = ...
itemView.setOnClickListener {
viewModel.onItemClick(item.id)
}
android:onClick
을 이용하여, XML에서 클릭을 처리할 수도 있습니다.
DataBinding: checked
View에서 UI 상태가 변경되면 ViewModel에 값이 전달되고,
다시 View에 상태가 전파되는 경우입니다.
아래와 같은 코드가 필요할 겁니다.
Two-way Binding을 이용하여, 코드를 좀 더 간결하게 만들 수 있습니다.
일부 Widget에는 Two-way data binding을 제공하는 속성들이 있습니다.
가능한 속성들은 링크에서 확인해보세요.
UseCase 🛁
Android KTX, DataBinding 기능을 활용한 2가지 예제를 살펴봅니다.
UseCase: Load Image URL
ImageView에 이미지 URL을 표시하는 경우가 많습니다.
보통 Glide와 같은 Image Loading Library를 사용하는데요.
val url = "https://..."
GlideApp.with(context).load(url).into(imageView)
Kotlin Extension을 이용하여 코드를 개선해볼 수 있습니다.
imageView.loadUrlAsync(url)fun ImageView.loadUrlAsync(url: Url?) {
GlideApp.with(context).load(url).into(this)
}
만약에 URL이 유효하지 않은 경우가 있으면, PlaceHolder를 보여줘야 합니다.
val imageUrlFromApi = null
imageView.loadUrlAsync(
url = imageUrlFromApi,
placeholder = R.drawable.placeholder // How?
)
아래처럼 PlaceHolder 기능을 추가할 수 있습니다.
여기서 BindingAdapter 코드가 재밌게 동작합니다.
Layout XML에서도 URL / PlaceHolder 기능을 이용할 수 있습니다.
UseCase: Debounce Click
Activity를 실행하는 버튼 등 단시간 내에 클릭 이벤트가 여러번 발생하는 것을 막아야 하는 경우가 있습니다. 이를 방지하는 코드를 작성해 봅시다.
기본적인 Click Listener 처리 코드입니다.
view.setOnClickListener { view ->
startActivity(...)
}<layout>
<ViewGroup>
<View
android:id="@+id/button"
android:onClick="@{() -> viewModel.onButtonClick()}" />
</ViewGroup>
</layout>
아래처럼 코드를 변경할 수 있다면 어떨까요? 🤔
view.setOnDebounceClickListener { view ->
startActivity(...)
}<layout>
<ViewGroup>
<View
android:id="@+id/button"
android:onDebounceClick="@{() -> viewModel.onButtonClick()}" />
</ViewGroup>
</layout>
간단합니다. 2개의 함수를 작성하면 됩니다.
(함수가 분리된 것은 단순히 Kotlin에서 SAM이 적용되지 않기 때문입니다.)
OnDebounceClickListener의 구현체입니다.
단순히 300ms 이내에는 클릭이벤트를 허용하지 않는 방식입니다.
Rx나 Coroutine의 Debounce, Throttle 기능으로 유사한 처리를 할 수 있으나,위의 코드는 기존 코드의 구조 변경없이 동작만 개선할 수 있습니다.
Summary
지금까지의 내용을 요약해봅니다.
- 🚅 Android KTX를 사용하여 Kotlin 코드를 개선할 수 있습니다.
- 🔗 DataBinding을 이용하여 XML에서의 View 기능을 확장할 수 있습니다.
- 🛁 Android KTX, DataBinding 그 자체에 초점을 맞추기보다, 코드를 개선하는 하나의 방법으로 고려해보세요.
본 발표에서는 View Layer의 코드를 줄이는 방법만을 위주로 소개했지만, Kotlin Extension 기능은 Presentation / Domain / Data Layer 등에서도 사용 가능하니 상상력을 잘 발휘하면 유용한 유틸 코드를 만들 수 있습니다. 👀
각자 자신만의 KTX를 만들어보는 것을 추천합니다! 💯
아래에 발표 슬라이드도 공유합니다. 🙇