Share
Embed
Android KTX, DataBinding으로 코드 생산성 개선하기
At DevFest Android 2019
Sungyong An
December 08, 2019
Transcript
66 slides
-
// Kotlin loadingView .visibility = if (isLoading) View .VISIBLE else
View.GONE // Java loadingView.setVisibility( isLoading ? View.VISIBLE : View.GONE); -
Android KTX Android Jetpack Kotlin extensions Link: https://developer.android.com/kotlin/ktx • Named
parameters • Parameter default values • Coroutines • Extension functions • Extension properties • Lambdas -
Core KTX: View implementation 'androidx.core:core-ktx:1.1.0' // 1.2.0-rc01 loadingView.visibility = if
(isLoading) View.VISIBLE else View.GONE // with ktx loadingView.isVisible = isLoading -
Core KTX: View inline var View.isVisible: Boolean get() = visibility
== View.VISIBLE set(value) { visibility = if (value) View.VISIBLE else View.GONE } inline var View.isInvisible: Boolean inline var View.isGone: Boolean // LayoutParams // Margin // Padding // and more ... -
Core KTX: Text val htmlString = "<b>bold</b> string" textView.text =
Html.fromHtml(htmlString) // with compat textView.text = HtmlCompat.fromHtml(htmlString, FROM_HTML_MODE_LEGACY) // with ktx textView.text = htmlString.parseAsHtml() -
Core KTX: Text inline fun String.parseAsHtml( flags: Int = FROM_HTML_MODE_LEGACY,
imageGetter: ImageGetter? = null, tagHandler: TagHandler? = null ): Spanned = HtmlCompat.fromHtml(this, flags, imageGetter, tagHandler) -
Core KTX: Text val sb = StringBuilder() .append(1) .append("string") //
kotlin function val sb = buildString { append(1) append("string") } 1string -
Core KTX: Text implementation 'androidx.core:core-ktx:1.1.0' // 1.2.0-rc01 val ssb =
SpannableStringBuilder().apply { append("bold") setSpan(StyleSpan(Typeface.BOLD), 0, 4, SPAN_INCLUSIVE_EXCLUSIVE) append("string") } // with ktx val ssb = buildSpannedString { bold { append("bold") } append("string") } boldstring -
Core KTX: Text inline fun buildSpannedString( builderAction: SpannableStringBuilder.() -> Unit):
SpannedString { val builder = SpannableStringBuilder() builder.builderAction() return SpannedString(builder) } ... inline fun SpannableStringBuilder.bold( builderAction: SpannableStringBuilder.() -> Unit ) = inSpans(StyleSpan(BOLD), builderAction = builderAction) -
Core KTX: Text buildSpannedString { click(onClick = { /* click!
*/ }) { color(Color.BLUE) { bold { append("clickable") } } } append("string") } clickablestring Custom -
Core KTX: Text inline fun SpannableStringBuilder.click( crossinline onClick: (View) ->
Unit, builderAction: SpannableStringBuilder.() -> Unit ) = inSpans( object : ClickableSpan() { override fun onClick(widget: View) = onClick(widget) }, builderAction = builderAction ) Custom -
Core KTX: OS fun newInstance(id: Int, title: String): DetailFragment {
return DetailFragment().apply { arguments = Bundle().apply { putInt("EXTRA_ID", id) putCharSequence("EXTRA_TITLE", title) } } } // with ktx arguments = bundleOf( "EXTRA_ID" to id, "EXTRA_TITLE" to title ) -
Core KTX: OS fun bundleOf(vararg pairs: Pair<String, Any?>) = Bundle(pairs.size).apply
{ for ((key, value) in pairs) { when (value) { is Int -> putInt(key, value) is CharSequence -> putCharSequence(key, value) is Parcelable -> putParcelable(key, value) ... } } } -
Collection KTX // Java Collections val array = arrayOf(1, 2,
3) val list = listOf(1, 2, 3) val set = setOf("one", "two", "three") val map = mapOf(1 to "one", 2 to "two") // Android Collections with ktx val arraySet: ArraySet<String> = arraySetOf("one", "two", "three") val arrayMap: ArrayMap<Int, String> = arrayMapOf(1 to "one", 2 to "two") -
Collection KTX inline fun <K, V> arrayMapOf(): ArrayMap<K, V> =
ArrayMap() fun <K, V> arrayMapOf(vararg pairs: Pair<K, V>): ArrayMap<K, V> { val map = ArrayMap<K, V>(pairs.size) for (pair in pairs) { map[pair.first] = pair.second } return map } ... -
Activity, Fragment, Navigation KTX AAC ViewModel View (Activity / Fragment)
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 'androidx.navigation:navigation-ui-ktx:2.1.0' // 2.2.0-rc02 -
Activity, Fragment, Navigation KTX private lateinit var viewModel: HomeViewModel override
fun onCreateView(...): View? { viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java) } // with lazy private val viewModel: HomeViewModel by lazy { ViewModelProviders.of(this).get(HomeViewModel::class.java) } // with ktx private val viewModel: HomeViewModel by viewModels() -
Activity, Fragment, Navigation KTX // Activity private val viewModel: HomeViewModel
by viewModels() // Fragment (scope: activity) private val viewModel: HomeViewModel by activityViewModels() // Fragment (scope: fragment) private val viewModel: HomeViewModel by viewModels() // Fragment (scope: navigation graph) private val viewModel: HomeViewModel by navGraphViewModels(R.id.home_graph) -
Firebase KTX Link: https:// rebase.google.com/support/release-notes/android // build.gradle implementation 'com.google.firebase:firebase-common-ktx:19.3.0' implementation
'com.google.firebase:firebase-firestore-ktx:21.3.0' implementation 'com.google.firebase:firebase-functions-ktx:19.0.1' implementation 'com.google.firebase:firebase-storage-ktx:19.1.0' implementation 'com.google.firebase:firebase-inappmessaging-ktx:19.0.2' implementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.2' implementation 'com.google.firebase:firebase-database-ktx:19.2.0' -
Play Core KTX Link: https://developer.android.com/reference/com/google/android/play/core/release-notes // build.gradle implementation 'com.google.android.play:core-ktx:1.6.4' val
AppUpdateInfo.isFlexibleUpdateAllowed: Boolean val AppUpdateInfo.isImmediateUpdateAllowed: Boolean suspend fun AppUpdateManager.requestAppUpdateInfo(): AppUpdateInfo fun AppUpdateManager.requestUpdateFlow(): Flow<AppUpdateResult> ... -
DataBinding UI Link: https://developer.android.com/topic/libraries/data-binding • Goodbye findViewById() • BindingAdapter •
Two-way data binding // app/build.gradle android { dataBinding { enabled = true } } -
DataBinding: example val loadingView = findViewById(R.id.loading_view) loadingView.visibility = if (isLoading)
View.VISIBLE else View.GONE <View android:id="@+id/loading_view" android:visibility="@{isLoading ? View.VISIBLE : View.GONE}" /> -
DataBinding: example <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 // HomeViewModel.kt private val _isLoading = MutableLiveData<Boolean>() val
isLoading: LiveData<Boolean> get() = _isLoading init { _isLoading.value = true // load data _isLoading.value = false } // HomeActivity.kt private val viewModel: HomeViewModel by viewModels() val binding: HomeActivityBinding = ... viewModel.isLoading.observe(this, Observer { binding.loadingView.isVisible = it }) -
DataBinding: visibility // HomeViewModel.kt private val _isLoading = MutableLiveData<Boolean>() val
isLoading: LiveData<Boolean> get() = _isLoading init { _isLoading.value = true // load data _isLoading.value = false } // HomeActivity.kt private val viewModel: HomeViewModel by viewModels() val binding: HomeActivityBinding = ... binding.viewModel = viewModel -
DataBinding: visibility <layout> <data> <variable name="viewModel" type="HomeViewModel" /> </data> <ViewGroup>
<View android:id="@+id/loading_view" android:visibleIf="@{viewModel.isLoading}" /> </ViewGroup> </layout> @BindingAdapter("android:visibleIf") fun View.setVisibleIf(value: Boolean) { isVisible = value } -
DataBinding: visibility // View.decompiled.java public final class ViewKt { @BindingAdapter({"android:visibleIf"})
public static final void setVisibleIf(@NotNull View view, boolean value) { view.setVisibility(value ? View.VISIBLE : View.GONE); } } // Generated by DataBinding library // HomeActivityBindingImpl.java @Override protected void executeBindings() { com.example.util.ViewKt.setVisibleIf(this.loadingView, isLoading); } -
DataBinding: visibility // View.kt @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 } Custom -
DataBinding: click // HomeViewModel.kt fun onFilterButtonClick() { ... } //
HomeActivity.kt private val viewModel: HomeViewModel by viewModels() val binding: HomeActivityBinding = ... binding.filterButton.setOnClickListener { viewModel.onFilterButtonClick() } -
DataBinding: click // HomeViewModel.kt fun onFilterButtonClick() { ... } //
HomeActivity.kt private val viewModel: HomeViewModel by viewModels() val binding: HomeActivityBinding = ... binding.viewModel = viewModel <layout> <data>...</data> <ViewGroup> <View android:id="@+id/filter_button" android:onClick="@{() -> viewModel.onFilterButtonClick()}" /> </ViewGroup> </layout> -
<layout> <data> <variable name="viewModel" type="HomeViewModel" /> <variable name="item" type="Item" />
</data> <ViewGroup> <TextView android:text="@{item.name}" android:onClick="@{() -> viewModel.onItemClick(item.id)}" /> </ViewGroup> </layout> DataBinding: click class Item( val id: Long, val name: String ) -
DataBinding: checked // TermsViewModel.kt private val _isChecked = MutableLiveData<Boolean>(false) val
isChecked: LiveData<Boolean> get() = _isChecked fun onTermsChecked(value: Boolean) { _isChecked.value = value } // TermsActivity.kt private val viewModel: TermsViewModel by viewModels() val binding: TermsActivityBinding = ... viewModel.isChecked.observe(this, Observer { binding.termsCheckbox.checked = it }) binding.termsCheckbox.setOnCheckedChangeListener { _, isChecked -> viewModel.onTermsChecked(isChecked) } -
DataBinding: checked // TermsViewModel.kt private val _isChecked = MutableLiveData<Boolean>(false) val
isChecked: LiveData<Boolean> get() = _isChecked fun onTermsChecked(value: Boolean) { _isChecked.value = value } // TermsActivity.kt private val viewModel: TermsViewModel by viewModels() val binding: TermsActivityBinding = ... binding.viewModel = viewModel -
DataBinding: checked <layout> <data> <variable name="viewModel" type="TermsViewModel" /> </data> <ViewGroup>
<CheckBox android:id="@+id/terms_checkbox" android:checked="@={viewModel.isChecked}" /> </ViewGroup> </layout> -
UseCase: Load Image URL ImageView , URL . Glide Image
Loading Library , Kotlin . ViewModel View URL ImageView -
DataBinding: Load Image URL val url = "https://..." val imageView:
ImageView = ... GlideApp.with(context).load(url).into(imageView) // kotlin extensions imageView.loadUrlAsync(url) val imageUrlFromApi = null imageView.loadUrlAsync(imageUrlFromApi) imageView.loadUrlAsync(imageUrlFromApi, placeholder = R.drawable.placeholder) fun ImageView.loadUrlAsync(url: Url?) { GlideApp.with(context).load(url).into(this) } -
DataBinding: Load Image URL fun ImageView.loadUrlAsync(url: Url?, placeholder: Drawable? =
null) { if (url == null) { GlideApp.with(context).load(placeholder).into(this) } else { GlideApp.with(context).load(url) .apply { if (placeholder != null) { placeholder(placeholder) } } .into(this) } } Custom @BindingAdapter("srcUrl", "placeholder", requireAll = false) -
DataBinding: Load Image URL <ImageView android:id="@+id/image_view" android:scaleType="centerCrop" android:placeholder="@{@drawable/placeholder}" android:srcUrl="@{item.imageUrl}" tools:src="@tools:sample/avatars"
/> // HomeItemBindingImpl.java @Override protected void executeBindings() { ImageViewKt.loadAsync( this.imageView, itemImageUrl, getDrawableFromResource(imageView, R.drawable.placeholder)); } class Item( val id: Long, val imageUrl: String? ) -
UseCase: Debounce Click . ex) startActivity Activity B Activity A
Click! Click! Activity B -
DataBinding: Debounce Click val view: View = ... view.setOnClickListener {
view -> startActivity(...) } // click! // click! click! view.setOnDebounceClickListener { view -> startActivity(...) } private typealias OnClickListener = (View) -> Unit fun View.setOnDebounceClickListener( listener: OnClickListener?) { if (listener == null) { setOnClickListener(null) } else { setOnClickListener(OnDebounceClickListener { it.run(listener) }) } } -
DataBinding: Debounce Click class OnDebounceClickListener(private val listener: OnClickListener) : View.OnClickListener
{ override fun onClick(v: View?) { val now = System.currentTimeMillis() if (now < lastTime + INTERVAL) return lastTime = now v?.run(listener) } companion object { private const val INTERVAL: Long = 300 private var lastTime: Long = 0 } } Custom -
DataBinding: Debounce Click <layout> <ViewGroup> <View android:id="@+id/filter_button" android:onClick="@{() -> viewModel.onFilterButtonClick()}"
/> </ViewGroup> </layout> <layout> <ViewGroup> <View android:id="@+id/filter_button" android:onDebounceClick="@{() -> viewModel.onFilterButtonClick()}" /> </ViewGroup> </layout> -
DataBinding: Debounce Click @BindingAdapter("onDebounceClick") fun setOnDebounceClickListener(view: View, listener: View.OnClickListener?) {
if (listener == null) {...} else { view.setOnClickListener(OnDebounceClickListener { it.run(listener::onClick) }) } } Custom // extension function fun View.setOnDebounceClickListener(listener: OnClickListener?) { if (listener == null) {...} else { setOnClickListener(OnDebounceClickListener { it.run(listener) }) } } -
Summary Android KTX , View Layer . DataBinding , Layout
XML View . Android KTX, DataBinding .