Android架构组件DataBinding

Android架构组件DataBinding

Android小彩虹2021-08-25 22:27:52170A+A-

Android Architecture Components: DataBinding - Dependent Properties

Ever heard of DataBinding? I myself would like to live in a world where every Android developer knows about it. A world where the concept of findViewById and handwritten boilerplate glue code does not exist. Where you can easily tie your data to the UI and forget about updating it in case the data changes.

To achieve this, my fellow developers, I decided to spread the word about a very powerful feature of DataBinding: DEPENDENT PROPERTIES.

“What are dependent properties?” you may ask.

About a year ago, I found this short article by George Mount which introduced a really neat feature of DataBinding * drrrrrrrrrrrrrrrrrum-roll * the Bindable properties which depend on other Bindable properties.

The cool thing about dependent properties is that any change in dependencies will automatically trigger an update to the field that declared them as dependencies. This is really powerful on screens where you have input views that rely upon the state of other views.

Let’s look at an example, so you can easily get the gist of it and see how useful this can be when implementing a reactive UI.

For the sake of simplicity I chose to have a first name and a last name as input fields, format them somehow and display the result. In the followings I’ll refer to this as the “displayName”.

This is how it looks in action:

视频
Dependent Property Demo

But how can this be implemented? Fair question. I’ll show how I did it.

If you’ve gotten this far, I assume you’re familiar with the DataBinding library, so I won’t explain how to set it up, but if you’d like to dust off your knowledge here’s a guide on how to do it.

As a first step I created the layout file.

< ?xml version="1.0" encoding="utf-8"?>
< layout xmlns:android="http://schemas.android.com/apk/res/android">

    < data class=".MainBinding">
        < variable
            name="viewModel"
            type="com.arthurnagy.databindingplayground.UserViewModel" />
    < /data>

    < LinearLayout>

        < TextView
            android:text="@{viewModel.displayName}" />

        < EditText
            android:text="@={viewModel.firstName}" />

        < EditText
            android:text="@={viewModel.lastName}" />

    < /LinearLayout>

< /layout>

What you’ve just seen is a short version of the layout. I’ve left out the irrelevant tags to focus on what’s important right now. I declared a variable, the view model that stores and handles the data, then bound the displayName property from within the ViewModel to the TextView. Next, the firstName and lastName properties were bound using the two-way data binding syntax to the EditTexts (meaning that any changes made in the EditTexts, will show up in the ViewModel’s String properties too).

Following this, in the view model class, I declared the displayName bindable property to depend on the firstName and lastName bindable properties, by enumerating them in the @Bindable annotation as follows:

class UserViewModel(private val resourceProvider: ResourceProvider) : BaseObservable() {

    @get:Bindable
    var firstName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.firstName)
        }

    @get:Bindable
    var lastName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.lastName)
        }

    val displayName: String
        @Bindable(value = ["firstName", "lastName"])
        get() = resourceProvider.getString(R.string.display_name, firstName, lastName)

}

To use the notifyPropertyChanged method which signals whenever the @Bindable properties change, the ViewModel had to extend the BaseObservable class. With this, every time the firstName & lastName changes the notifyPropertyChanged is called. This updates the displayName, which on its turn updates the UI with the new value.

To wrap this up, as a final step, I tied the UserViewModel instance to the XML layout:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val binding: MainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        
        binding.viewModel = UserViewModel(ResourceProvider(applicationContext))
    }
}

In the activity class I created a binding instance and set a UserViewModel to it.

Aaaaand that’s it.

Or is it?

Well, not exactly. I mean it’s nice and everything, but as you can see I filled the UserViewModel with boilerplate code in order to make this work.

Luckily, not so long ago I stumbled upon this interesting talk by Lisa Wray about using DataBinding together with Kotlin. She used Kotlin’s delegated properties feature to create a custom delegate for bindable fields, making the code more readable this way. Following her steps, this is what I ended up having:

class BindableDelegate(
    private var value: T,
    private val bindingId: Int
) {
    operator fun getValue(receiver: R, property: KProperty<*>): T = value

    operator fun setValue(receiver: R, property: KProperty<*>, value: T) {
        this.value = value
        receiver.notifyPropertyChanged(bindingId)
    }
}

fun  bindable(value: T, bindingId: Int): BindableDelegate =
    BindableDelegate(value, bindingId)

The BindableDelegate has a receiver of R which extends BaseObservable and handles any kind a property of type T. In the constructor of the delegate it expects an initial value of the bindable property as well as its binding resource identifier. So now, when a value is set, notifyPropertyChanged will be called on the receiver class.

After rewriting the ViewModel to use the custom delegate I had something like this:

class UserViewModel(private val resourceProvider: ResourceProvider) : BaseObservable() {

    @get:Bindable
    var firstName: String by bindable("", BR.firstName)

    @get:Bindable
    var lastName: String by bindable("", BR.lastName)

    val displayName: String
        @Bindable(value = ["firstName", "lastName"])
        get() = resourceProvider.getString(R.string.display_name, firstName, lastName)

}

All right, now the code is clean and concise. It can’t get any better, I’m pretty sure!

What seems to be the problem?

视频
Activity config change

Turns out, the data doesn’t survive configuration changes such as screen rotations or entering multi-window mode.

So that’s where Android Architecture Components: ViewModel came into play. ViewModel is designed to store and manage UI-related data in a lifecycle conscious way. Well then, let’s extend it, use it and problem solved, right? It’s not exactly that simple, the UserViewModel already extends BaseObservable, in order to use bindable properties.

Since extending multiple classes is not possible, my options were limited. I could have extended the Architecture Components provided ViewModel and copy-paste the whole code from the BaseObservable class. It’s doable, but let’s be honest, that’s an ugly solution, so I chose not to pursue it.

Fortunately, the DataBinding library provides Observable classes, more specifically an ObservableField(or it’s primitive variants) which behaves like a BaseObservable but also wraps a value. Instances of this class can be used as properties in the view model instead of the @Bindable properties. This way properties can be bound to the view and retain the data on configuration change events by extending the ViewModel class.

Mmmkay, but what about the dependent properties? How can one deliver this feature with ObservableField? In the Android Studio 3.1 update, Google silently updated the DataBinding library, and one of the changes was:

The ObservableField class can now accept other Observable objects in its constructor.

Crystal clear, right? It turns out, using this, the same dependent property behaviour can be achieved as with @Bindable properties. So I refactored the UserViewModel to use the Architecture Component ViewModel and ObservableFields as properties:

import androidx.databinding.ObservableField
import androidx.lifecycle.ViewModel

class UserViewModel(private val resourceProvider: ResourceProvider) : ViewModel() {

    val firstName = ObservableField()
    val lastName = ObservableField()

    val displayName: ObservableField = object : ObservableField(firstName, lastName) {
    override fun get() =
        resourceProvider.getString(R.string.display_name, firstName.get().orEmpty(), lastName.get().orEmpty())
    }

}

To make the code more readable, I created a top-level inline function which hides the ObservableField instantiation and overrides the get() method:

inline fun  dependantObservableField(vararg dependencies: Observable, crossinline mapper: () -> T?) =
    object : ObservableField(*dependencies) {
        override fun get(): T? {
            return mapper()
        }
    }


//Usage in VM:

val displayName = dependantObservableField(firstName, lastName) {
    resourceProvider.getString(R.string.display_name, firstName.get().orEmpty(), lastName.get().orEmpty())
}

I also modified the view model creation logic in the activity:

binding.viewModel = provideViewModel { UserViewModel(ResourceProvider(applicationContext)) }


//The provideViewModel method is just an extension function which handles the retrieval 
//or creation of our desired view model class:
inline fun  FragmentActivity.provideViewModel(crossinline createVM: () -> VM): VM =
    ViewModelProviders.of(this, object : ViewModelProvider.Factory {
        override fun  create(modelClass: Class): T = createVM() as T
    }).get(VM::class.java)

Et voila! Problem solved! Now the data is retained on configuration change and the dependent property is working as expected.

视频
Results with working config change.

Bonus: LiveData

Like ViewModel, LiveData is another class from the Lifecycle Architecture Component library. LiveData is a data holder which also happens to be lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, this ensures that if an activity or fragment is observing a LiveData, it will only send an update to them in an active lifecycle state.

Android Studio 3.1 brought another sweet update to the DataBinding library:

You can now use a LiveData object as an observable field in data binding expressions. The ViewDataBindingclass now includes a new setLifecycleOwner() method that you use to observe LiveData objects.

So instead of using ObservableField’s I used LiveData for binding the data to the UI. The property dependency can be achieved by using a MediatorLiveData: I created another top-level inline function only this time returning a MediatorLiveData instead of ObservableField and its dependencies were LiveData instead of Observable:

inline fun < T> dependantLiveData(vararg dependencies: LiveData<*>, crossinline mapper: () -> T?) =
    MediatorLiveData< T>().also { mediatorLiveData ->
        val observer = Observer< Any> { mediatorLiveData.value = mapper() }
        dependencies.forEach { dependencyLiveData ->
            mediatorLiveData.addSource(dependencyLiveData, observer)
        }
    }

All that was left do is swapping out all ObservableField properties to LiveData in the UserViewModel:

class UserViewModel(private val resourceProvider: ResourceProvider) : ViewModel() {

    val firstName = MutableLiveData()
    val lastName = MutableLiveData()

    val displayName = dependantLiveData(firstName, lastName) {
        resourceProvider.getString(R.string.display_name, firstName.value.orEmpty(), lastName.value.orEmpty())
    }

}

and calling setLifecycleOwner() on the binding instance in the activity:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding: MainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.setLifecycleOwner(this)
        binding.viewModel = provideViewModel { UserViewModel(ResourceProvider(applicationContext)) }
    }
}

The end!

Hope you find this feature useful and now that you’ve seen how easy it is to integrate it into your codebase, you’ll start experimenting with it. I’d be thrilled to hear your thought on this functionality, as well as ideas about cases you see yourself using it.

You can find all the code here. Any feedback, and of course shares and claps are more than welcome.

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们