Skip to content

Android dsl databinding for View framework. There are RxJava and Coroutines support. Later will be adapted for Android Compose. Some lifecycle quirks as a bonus :)

Notifications You must be signed in to change notification settings

iamthevoid/android-dsl-databinding

Repository files navigation

Android DSL Data Binding

Library for bind observable data with view from Android View Framework

Setter

Setter is abstract type. There are its subclasses for RxJava Flowable and Kotlin Coroutines Flow.

Setter assigns with Android View by extension method

fun VIEW.addSetter(FLOWABLE_TYPE<DATA>, VIEW.(DATA) -> Unit)

So we can add any realization for any data and any view. For example

fun <T : CharSequence> TextView.setText(text: Flowable<T>) =
    addSetter(text) { this.text = it }

Under the hood there is ObserveListener object associated with each view. This object store flowables cache for view and subscribe on data changes when view onViewAttachedToWindow and unsubscribe when view onViewDetachedFromWindow.

There are many default setters already defined in library (and many will be added in next releases)

You can also easyly add your own setter with addSetter method. For example if you're using Glide for image loading setter will looks like this

fun ImageView.setImageUrl(image : Flowable<String>) =
    addSetter(image) { url ->
        Glide.with(this)
            .load(url)
            .diskCacheStrategy(DiskCacheStrategy.ALL)
            .into(this)
    }

and then use it in your DSL layout (JetBrains Anko in example, but addSetter is extension for generic type T : View, so you can use it anywhere, even in ButterKnifed xml layouts)

frameLayout {
    imageView {
        setImageUrl(vm.user.map { it.avatarUrl })
    }.lparams(matchParent, wrapContent)
}

Two Way Binding

There is also support for Two-Way Data Binding

View emits changes into FlowableProcessor (like Subject for Observable, but for Flowable).

Field and Item abstractions are deprecated and will be removed in release 1.0.0

It is representing by Fields and Items.

Field is observable property, that emit Optional element when element set to field. Allows 'null'.

Item is observable property, that emit T element when element set to field. Allows 'non-null' only.

for both, rx and coroutines provided base types

- Rx(Coroutine)Field

- Rx(Coroutine)Item

- Rx(Coroutine)CharSequence

- Rx(Coroutine)Boolean

- Rx(Coroutine)Long

- Rx(Coroutine)Int

- Rx(Coroutine)Double

- Rx(Coroutine)Float

Recycler View

RecyclerView part deserves own block. Just recall typical RecyclerView usage:

  1. Creating layout with RecyclerView
  2. Creating layouts for ALL ViewHolders (I saw up to 9 different holders in one Recycler, are you?)
  3. Creating Adapter, which handle ViewHolders logic
  4. Creating ViewHolders
  5. Writing ViewHolders manipulating logic in onBind method

This library reduce described complexity, but bit harder for first dive. So look carefully :)

You just need:

  1. Create layout with RecyclerView
  2. Also create layouts for ALL ViewHolders
  3. Create binding between items types (classes) and ViewHolders layouts.
  4. Nothing. It is all

In example below I used JetBrains Anko and AnkoCoroutinesLayout from this library. In this class I needs to override only View creation method.

But you can extend own RxLayout or CoroutinesLayout or even Layout.

Layout is not depend on any library and can be any and adapted to any DSL library and any data flow. It describes entity, that get item changes and provide method for creating View:

abstract fun createView(parent: ViewGroup): View

RxLayout and CoroutinesLayout are layouts that adapted for data flow concrete types. They both override on own manner:

fun set(item: T)

And both provide:

  • (observable) property item with type Rx(Coroutine)Field described above
  • property itemChanges that emits only non-null items
  • fun changeItem(itemChange: T?.() -> Unit) lets you to change item with Recycler will be notified about item change automatically

Example can be found Here. One screen app, that receive currency rates every second, and represent it according to entered value. On top position of RecyclerView is base currency with editable field for it value, below fields with another currencies and values, based on base value, entered in header ViewHolder (rate * entered base value). Clicked rate becomes base rate.

Extractions here:

Describing Recycler in DSL:

recyclerView {
    // Just remove BlinkAnimation on items change
    (itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
    // Same as usualy - providing LayoutManager 
    layoutManager = LinearLayoutManager(context)
    // Pay attention for line below, it will be described later
    setItems(vm.data, revolutBindings(vm))
    // Hide view based on observable boolean loading
    hideUntilLoaded(vm.loading)
}

Then layouts. Layouts must extends Layout class, and looks like this (left only important part, removed static data):

// Rates requests every second, that both of layouts will get holding item changes

// This class extends Layout which generic type is 'String'. Pay attention
class CurrencyHeaderItem(private val vm : RevolutViewModel, viewGroup: ViewGroup) : AnkoCoroutinesLayout<String>(viewGroup) {

    override fun createView(ui: AnkoContext<AnkoCoroutinesLayout<String>>): View =
        ui.frameLayout {
            imageView {
	        // custom setter for image view, if item've change - image also have change 
                setImageUrl(itemChanges.map { toImageUrl(it) })
            }
            textView {
                // AnkoCoroutinesLayout generic type is String, then 'itemChanges' property is observable String 
                setText(itemChanges)
            }
            textView {
                // We can also do any modiications with data
                setText(itemChanges.flatMapConcat { flow { emit(codeToValue(it)) } })
            }
            editText {
                // Seting text from ViewModel
                setText(vm.currentValue)
                // After text changes bind it to back to field
                afterTextChanges(vm.currentValue)
            }
        }
}

Second layout different, looks like very similar to this, but i wouldn't bring full listing. Just pay attention for generic type:

// Generic type is CurrencyRate differs from String in previous layout
// so 'itemChanges' property is observable CurrencyRate 
class CurrencySimpleItem(private val vm : RevolutViewModel, viewGroup: ViewGroup) : AnkoCoroutinesLayout<CurrencyRate>(viewGroup) 

It is all. One second... No! :) Just a little part, binding. Do you remember calling method 'setItems' on RecyclerView? Look to it:

setItems(vm.data, revolutBindings(vm))

vm.data is observable list of items, but we need bind it to recycler:

fun revolutBindings(vm : RevolutViewModel) =
    ItemBindings
        .of(String::class.java) { CurrencyHeaderItem(vm, it) }
        .addBinding(CurrencyRate::class) { CurrencySimpleItem(vm, it) }

Now it is completely all. You can run example and look how recycler represents data and works without providing adapter, or view holder.

Full signature of setItems fun looks like this (Coroutines here, but also there is Rx port in library)

fun <T : Any> RecyclerView.setItems(  
  // Observable data
  items: Flow<List<T>>,  
  
  // Bindings
  itemBindings: ItemBindings, 
   
  // Diff callback factory. You can use custom or default provided in library.
  // Default check if item is implements Diffable interface or compare by 'equals' calling
  // So you can implement Diffable or use data classes from Kotlin
  diffCallbackFactory: ((old: List<T>, new: List<T>) -> DiffCallback<T>) = diffCallback()  
) = addSetter(items) { setItems(it, itemBindings, diffCallbackFactory) }  
  
inline fun <T : Any, reified A : StandaloneRecyclerAdapter<T>> RecyclerView.setItems(  
  items: Flow<List<T>>,  
  itemBindings: ItemBindings,  
  
  // Here you can provide Custom Adapter extends StandaloneRecyclerAdapter, if you purpose more       
  // complex, than just adapter. 
  crossinline adapterFactory: (List<T>) -> A? = { null },  
  noinline diffCallbackFactory: ((old: List<T>, new: List<T>) -> DiffCallback<T>) = diffCallback()  
) = addSetter(items) { setItems( it, itemBindings, adapterFactory, diffCallbackFactory) }

Implementation

RxJava 2

implementation 'io.github.iamthevoid.reactivebinding:core:1.0.0-beta12'
implementation 'io.github.iamthevoid.reactivebinding:rx2-core:1.0.0-beta12'

Optional:

  • Recycler View
implementation 'io.github.iamthevoid.reactivebinding:core-recycler:1.0.0-beta12'
implementation 'io.github.iamthevoid.reactivebinding:rx2-recycler:1.0.0-beta12'
  • Swipe Refresh Layout
implementation 'io.github.iamthevoid.reactivebinding:core-swiperefreshlayout:1.0.0-beta12'
implementation 'io.github.iamthevoid.reactivebinding:rx2-swiperefreshlayout:1.0.0-beta12'
  • Material
implementation 'io.github.iamthevoid.reactivebinding:core-design:1.0.0-beta12'
implementation 'io.github.iamthevoid.reactivebinding:rx2-design:1.0.0-beta12'

Kotlin Coroutines

implementation 'io.github.iamthevoid.reactivebinding:core:1.0.0-beta12'
implementation 'io.github.iamthevoid.reactivebinding:coroutines-core:1.0.0-beta12'

Optional:

  • Recycler View
implementation 'io.github.iamthevoid.reactivebinding:core-recycler:1.0.0-beta12'
implementation 'io.github.iamthevoid.reactivebinding:coroutines-recycler:1.0.0-beta12'
  • Swipe Refresh Layout
implementation 'io.github.iamthevoid.reactivebinding:core-swiperefreshlayout:1.0.0-beta12'
implementation 'io.github.iamthevoid.reactivebinding:coroutines-swiperefreshlayout:1.0.0-beta12'
  • Material
implementation 'io.github.iamthevoid.reactivebinding:core-design:1.0.0-beta12'
implementation 'io.github.iamthevoid.reactivebinding:coroutines-design:1.0.0-beta12'

About

Android dsl databinding for View framework. There are RxJava and Coroutines support. Later will be adapted for Android Compose. Some lifecycle quirks as a bonus :)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages