Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Droidcon Berlin: Leveraging Android Data Binding with Kotlin

Droidcon Berlin: Leveraging Android Data Binding with Kotlin

Android databinding is considered as both a powerful toolchain, empowering your views with access to view data without the necessity to build cumbersome presenters, and conversely as an overly complex, convoluted mess of binding statements opening the door to unnecessary, irresponsible domain logic in your view layouts.

Whilst the latter of these statements can be true, databinding offers a very powerful code generation syntax, allowing you to utilise the power of the compiler to ensure that your binding statements are runtime safe. Combining this with the concise syntax afforded by Kotlin allows us to dramatically cut down on boilerplate and build complex user interfaces with relative ease.

In this talk, you can learn how to utilise extension bindings and property delegates with your layouts, and manage your screen state transformations with true, and safe two-way binding, allowing you to build a fully reactive observable view layout.

Ash Davies

June 27, 2018
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. Leveraging Android Databinding
    with Kotlin
    ashdavies.io 1

    View Slide

  2. What is Data Binding?
    ashdavies.io 2

    View Slide

  3. Data Binding Beta
    2015
    ashdavies.io 3

    View Slide

  4. Data Binding v2
    ashdavies.io 4

    View Slide

  5. Data Binding v2
    Incremental class generation ⚡
    ashdavies.io 5

    View Slide

  6. Data Binding v2
    Incremental class generation ⚡
    Pre-compilation class generation !
    ashdavies.io 6

    View Slide

  7. Data Binding
    class RepoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_repo)
    }
    }
    ashdavies.io 7

    View Slide

  8. Data Binding
    class RepoActivity : AppCompatActivity() {
    private lateinit var binding: ActivityRepoBinding
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_repo)
    }
    }
    ashdavies.io 8

    View Slide

  9. Data Binding
    internal class RepoActivity : AppCompatActivity() {
    private lateinit var binding: ActivityRepoBinding
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_repo)
    binding.setLifecycleOwner(this)
    }
    }
    ashdavies.io 9

    View Slide

  10. Data Binding
    class RepoActivity : AppCompatActivity() {
    private lateinit var binding: ActivityRepoBinding
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_home)
    binding.setLifecycleOwner(this)
    }
    }
    ashdavies.io 10

    View Slide

  11. Delegated Properties
    ashdavies.io 11

    View Slide

  12. Delegated Properties
    val value: String by lazy {
    println("computed!")
    "Hello"
    }
    ashdavies.io 12

    View Slide

  13. Delegated Properties
    interface ReadOnlyProperty {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    }
    interface ReadWriteProperty {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
    }
    ashdavies.io 13

    View Slide

  14. Delegated Properties
    class ActivityBindingProperty(
    ) : ReadOnlyProperty {
    override operator fun getValue(thisRef: Activity, property: KProperty<*>): T {
    TODO("not implemented")
    }
    }
    ashdavies.io 14

    View Slide

  15. Delegated Properties
    class ActivityBindingProperty(
    @LayoutRes private val resId: Int
    ) : ReadOnlyProperty {
    private var binding: T? = null
    override operator fun getValue(thisRef: Activity, property: KProperty<*>): T {
    return binding ?: createBinding(thisRef).also { binding = it }
    }
    private fun createBinding(activity: Activity): T {
    return DataBindingUtil.setContentView(activity, resId)
    }
    }
    ashdavies.io 15

    View Slide

  16. ProTip: Extension Function! !
    class RepoActivity : AppCompatActivity() {
    private val binding by activityBinding(R.layout.activity_repo)
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding.setLifecycleOwner(this)
    }
    }
    fun FragmentActivity.activityBinding(
    @LayoutRes resId: Int
    ) = ActivityBindingProperty(resId)
    ashdavies.io 16

    View Slide

  17. Introducing
    Android Architecture Components
    ashdavies.io 17

    View Slide

  18. Model-View-ViewModel
    ashdavies.io 18

    View Slide

  19. ashdavies.io 19

    View Slide

  20. Model-View-ViewModel
    class RepoActivity : AppCompatActivity() {
    private val binding by activityBinding(R.layout.activity_repo)
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding.setLifecycleOwner(this)
    }
    }
    ashdavies.io 20

    View Slide

  21. Model-View-ViewModel
    class RepoActivity : AppCompatActivity() {
    private val binding by activityBinding(R.layout.activity_repo)
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding.setLifecycleOwner(this)
    binding.model = ViewModelProviders
    .of(this, RepoViewModelFactory())
    .get(RepoViewModel::class.java)
    }
    }
    ashdavies.io 21

    View Slide

  22. ProTip: Extension Function! !
    class RepoActivity : AppCompatActivity() {
    private val binding by activityBinding(R.layout.activity_repo)
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding.setLifecycleOwner(this)
    binding.model = getViewModel(RepoViewModelFactory())
    }
    }
    inline fun FragmentActivity.getViewModel(
    factory: ViewModelProvider.Factory = ViewModelProvider.NewInstanceFactory()
    ) = ViewModelProviders.of(this, factory).get(T::class.java)
    ashdavies.io 22

    View Slide

  23. Model-View-ViewModel
    class RepoActivity : AppCompatActivity() {
    private val binding by activityBinding(R.layout.activity_repo)
    private val model by lazy { getViewModel(RepoViewModelFactory()) }
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding.setLifecycleOwner(this)
    binding.model = model
    // val items = model.items.get()
    }
    }
    ashdavies.io 23

    View Slide

  24. ViewModel
    class RepoViewModel(service: RepoService) : ViewModel {
    val items: ObservableField> = ObservableField()
    fun query(value: String) {
    service.fetch(value, items::set)
    }
    }
    ashdavies.io 24

    View Slide

  25. Observable
    class RepoViewModel(service: RepoService) : ViewModel {
    val items: ObservableField> = ObservableField()
    fun query(value: String) { /* ... */ }
    }
    ashdavies.io 25

    View Slide

  26. LiveData Data Binding
    ashdavies.io 26

    View Slide

  27. LiveData Data Binding
    class RepoViewModel(service: RepoService) : ViewModel {
    val items = MutableLiveData>()
    fun query(value: String) { /* ... */ }
    }
    ashdavies.io 27

    View Slide

  28. ProTip: Extension Functions! !
    class RepoViewModel(service: RepoService) : ViewModel {
    val items = mutableLiveData>()
    fun query(value: String) { /* ... */ }
    }
    fun ViewModel.mutableLiveDataOf() = MutableLiveData()
    ashdavies.io 28

    View Slide

  29. ViewModel
    class RepoViewModel(private val service: RepoService) : ViewModel {
    val items = mutableLiveDataOf>()
    fun query(value: String) {
    service.fetch(value) {
    items.value = it
    }
    }
    }
    ashdavies.io 29

    View Slide

  30. ViewModel
    class RepoViewModel(private val service: RepoService) : ViewModel {
    val items = mutableLiveDataOf>()
    val loading = mutableLiveDataOf()
    fun query(value: String) {
    loading.value = true
    service.fetch(value) {
    loading.value = false
    items.value = it
    }
    }
    }
    ashdavies.io 30

    View Slide

  31. Layout Expressions
    xmlns:app="http://schemas.android.com/apk/res-auto">


    name="model"
    type="io.ashdavies.databinding.RepoViewModel"/>

    android:visibility="@{model.loading ? View.VISIBLE : View.GONE}"/>

    ashdavies.io 31

    View Slide

  32. Layout Expressions
    xmlns:app="http://schemas.android.com/apk/res-auto">


    name="model"
    type="io.ashdavies.databinding.RepoViewModel"/>

    android:text="@string/activity_repo_empty"/>
    android:visibility="@{model.loading ? View.VISIBLE : View.GONE}"/>

    ashdavies.io 32

    View Slide

  33. Layout Expressions
    xmlns:app="http://schemas.android.com/apk/res-auto">


    name="model"
    type="io.ashdavies.databinding.RepoViewModel"/>

    android:text="@string/activity_repo_empty"
    android:visibility="@{model.items.count == 0 ? View.VISIBLE : View.GONE}"/>
    android:visibility="@{model.loading ? View.VISIBLE : View.GONE}"/>

    ashdavies.io 33

    View Slide

  34. Layout Expressions
    xmlns:app="http://schemas.android.com/apk/res-auto">


    name="model"
    type="io.ashdavies.databinding.RepoViewModel"/>

    android:text="@string/activity_repo_empty"
    android:visibility="@{model.items.count == 0 && !model.loading ? View.VISIBLE : View.GONE}"/>
    android:visibility="@{model.loading ? View.VISIBLE : View.GONE}"/>

    ashdavies.io 34

    View Slide

  35. Too Complicated!
    ashdavies.io 35

    View Slide

  36. class RepoViewModel(private val service: RepoService) : ViewModel {
    val items = mutableLiveDataOf>()
    val loading = mutableLiveDataOf()
    val empty = mutableLiveDataOf()
    fun query(value: String) {
    loading.value = true
    empty.value = true
    service.fetch(value) {
    items.value = it
    empty.value = it.isEmpty()
    loading.value = false
    }
    }
    }
    ashdavies.io 36

    View Slide

  37. xmlns:app="http://schemas.android.com/apk/res-auto">


    name="model"
    type="io.ashdavies.databinding.RepoViewModel"/>

    android:text="@string/activity_repo_empty"
    android:visibility="@{model.empty ? View.VISIBLE : View.GONE}"/>
    android:visibility="@{model.loading ? View.VISIBLE : View.GONE}"/>

    ashdavies.io 37

    View Slide

  38. @BindingAdapter
    ashdavies.io 38

    View Slide

  39. @BindingAdapter
    @BindingAdapter("goneUnless")
    fun goneUnless(view: View, visible: Boolean) {
    view.visibility = if (visible) View.VISIBLE else View.GONE
    }
    ashdavies.io 39

    View Slide

  40. ProTip: Extension Property! !
    @set:BindingAdapter("isVisible")
    inline var View.isVisible: Boolean
    get() = visibility == View.VISIBLE
    set(value) {
    visibility = if (value) View.VISIBLE else View.GONE
    }
    ashdavies.io 40

    View Slide

  41. @BindingAdapter
    xmlns:app="http://schemas.android.com/apk/res-auto">


    name="model"
    type="io.ashdavies.databinding.RepoViewModel"/>

    android:text="@string/activity_repo_empty"
    app:isVisible="@{model.empty}"/>
    app:isVisible="@{model.loading}"/>

    ashdavies.io 41

    View Slide

  42. @Bindable
    ashdavies.io 42

    View Slide

  43. @Bindable
    ObservableViewModel
    ashdavies.io 43

    View Slide

  44. @Bindable
    PropertyChangeRegistry
    ashdavies.io 44

    View Slide

  45. @Bindable
    abstract class ObservableViewModel : ViewModel(), Observable {
    private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
    override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
    callbacks.add(callback)
    }
    override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
    callbacks.remove(callback)
    }
    fun notifyChange() {
    callbacks.notifyCallbacks(this, 0, null)
    }
    fun notifyPropertyChanged(fieldId: Int) {
    callbacks.notifyCallbacks(this, fieldId, null)
    }
    }
    ashdavies.io 45

    View Slide

  46. @Bindable
    class RepoViewModel(private val service: RepoService) : ObservableViewModel {
    val items = mutableLiveDataOf>()
    val loading = mutableLiveDataOf()
    fun query(value: String) {
    loading.value = true
    service.fetch(value) {
    loading.value = false
    items.value = it
    }
    }
    }
    ashdavies.io 46

    View Slide

  47. @Bindable
    class RepoViewModel(private val service: RepoService) : ObservableViewModel {
    @get:Bindable
    var items: List = emptyList()
    private set(value) {
    notifyPropertyChanged(BR.items)
    field = value
    }
    val loading = mutableLiveDataOf()
    fun query(value: String) {
    loading.value = true
    service.fetch(value) {
    loading.value = false
    items = it
    }
    }
    }
    ashdavies.io 47

    View Slide

  48. @Bindable
    Delegated Properties
    ashdavies.io 48

    View Slide

  49. Property Delegate
    ObservableProperty
    ashdavies.io 49

    View Slide

  50. @Bindable
    class BindableProperty(
    initial: T, private val observable: ObservableViewModel, private val id: Int
    ) : ObservableProperty(initial) {
    override fun beforeChange(
    property: KProperty<*>, oldValue: T, newValue: T
    ): Boolean = { /* ... */ }
    override fun afterChange(
    property: KProperty<*>, oldValue: T, newValue: T
    ) { /* ... */ }
    }
    ashdavies.io 50

    View Slide

  51. @Bindable
    class BindableProperty(
    initial: T, private val observable: ObservableViewModel, private val id: Int
    ) : ObservableProperty(initial) {
    override fun beforeChange(
    property: KProperty<*>, oldValue: T, newValue: T
    ): Boolean = oldValue != newValue
    override fun afterChange(
    property: KProperty<*>, oldValue: T, newValue: T
    ) { /* ... */ }
    }
    ashdavies.io 51

    View Slide

  52. @Bindable
    class BindableProperty(
    initial: T, private val observable: ObservableViewModel, private val id: Int
    ) : ObservableProperty(initial) {
    override fun beforeChange(
    property: KProperty<*>, oldValue: T, newValue: T
    ): Boolean = oldValue != newValue
    override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
    observable.notifyPropertyChanged(id)
    }
    }
    ashdavies.io 52

    View Slide

  53. ProTip: Extension Function! !
    @Bindable
    class RepoViewModel(private val service: RepoService) : ObservableViewModel() {
    @get:Bindable
    var items by bindable>(emptyList(), BR.items)
    private set
    val items = mutableLifeDataOf>()
    init {
    service.fetch(items::setValue)
    }
    }
    fun ObservableViewModel.bindable(initial: T, id: Int): BindableProperty {
    return BindableProperty(initial, this, id)
    }
    ashdavies.io 53

    View Slide

  54. ! Awesome! !
    ashdavies.io 54

    View Slide

  55. Data Binding Sample
    github.com/ashdavies/data-binding
    ashdavies.io 55

    View Slide

  56. Android Data Binding with Kotlin
    bit.ly/2yU8qUz
    ashdavies.io 56

    View Slide

  57. Additional Resources
    ashdavies.io 57

    View Slide

  58. 4 Android Data Binding Library samples
    Google Samples - [bit.ly/2MK5GMb]
    4 Make your view-model properties great again
    Aiden McWilliams - [bit.ly/2llVVHz]
    4 MVVM, Viewmodel and architecture components
    Danny Preussler - [bit.ly/2yxvGay]
    4 Android Data Binding: RecyclerView
    George Mount - [https://bit.ly/2IgwY9w]
    ashdavies.io 58

    View Slide

  59. Cheers! !
    _ashdavies
    ashdavies.io 59

    View Slide