Binding to Data

With data binding, you can declare data objects within your layout file:

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools">
        <data>
            <variable
                name="crime"
                type="com.bignerdranch.android.criminalintent.Crime"/>
        </data>
        ...
    </layout>

And then use values from those objects directly in your layout file by using the binding mustache operator, @{}:

    <CheckBox
        android:id="@+id/list_item_crime_solved_check_box"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:checked="@{crime.isSolved()}"
        android:padding="4dp"/>

In an object diagram, that would look like Figure 19.7:

Figure 19.7  The ties that bind

The ties that bind

Your goal right now is to put the sound names on their buttons. The most direct way to do that using data binding is to bind directly to a Sound object in list_item_sound.xml (Figure 19.8):

Figure 19.8  Direct hookup

Direct hookup

However, this causes some architectural issues. To see why, look at it from an MVC perspective (Figure 19.9).

Figure 19.9  Broken MVC

Broken MVC

The guiding principle behind any architecture is the Single Responsibility Principle. It says that each class you make should have exactly one responsibility. MVC gave you an idea of what those responsibilities should be: The model represents how your app works, the controller decides how to display your app, and the view displays it on the screen the way you want it to look.

Using data binding as shown in Figure 19.8 would break this division of responsibilities, because the Sound model object would likely end up with code that prepares the data for the view to display. This would quickly make your app a mess, as Sound.kt would become littered with two kinds of code: code that represents how your app works and code that prepares data for the view to display.

Instead of muddying up the responsibility of Sound, you will introduce a new object called a view model to use with data binding. This view model will take on the responsibility of preparing the data for the view to display (Figure 19.10).

Figure 19.10  Model-View-View Model

Model-View-View Model

This architecture, as we have said, is called MVVM, short for Model-View-View Model. Most of the work your controller classes once did at runtime to format data for display will go in the view model. Wiring widgets up with that data will be handled directly in the layout file using data binding to that view model. Your activity or fragment will be in charge of things like initializing the binding and the view model and creating the link between the two.

There is no controller with MVVM; rather, activities and fragments are considered part of the view.

Creating a view model

Let’s create your view model. Create a new class called SoundViewModel and give it two properties: a Sound for it to use and a BeatBox to (eventually) play that sound with.

Listing 19.20  Creating SoundViewModel (SoundViewModel.kt)

class SoundViewModel {

    var sound: Sound? = null
        set(sound) {
            field = sound
        }
}

These properties are the interface your adapter will use. For the layout file, you will want an additional function to get the title that the button should display. Add it now to SoundViewModel.

Listing 19.21  Adding binding functions (SoundViewModel.kt)

class SoundViewModel {

    var sound: Sound? = null
        set(sound) {
            field = sound
        }

    val title: String?
        get() = sound?.name
  }

Binding to a view model

Now to integrate the view model into your layout file. The first step is to declare a property on your layout file, like so:

Listing 19.22  Declaring the view model property (res/layout/list_item_sound.xml)

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="viewModel"
            type="com.bignerdranch.android.beatbox.SoundViewModel"/>
    </data>
    ...
</layout>

This defines a property named viewModel on your binding class, including a getter and setter. Within your binding class, you can use viewModel in binding expressions.

Listing 19.23  Binding your button title (res/layout/list_item_sound.xml)

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="viewModel"
            type="com.bignerdranch.android.beatbox.SoundViewModel"/>
    </data>
    <Button
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:text="@{viewModel.title}"
        tools:text="Sound name"/>
</layout>

Within the binding mustache, you can write simple Java expressions, including chained function calls, math, and most anything else you want to include.

The last step is to hook up your view model. Create a SoundViewModel and attach it to your binding class. Then add a binding function to your SoundHolder.

Listing 19.24  Hooking up the view model (MainActivity.kt)

private inner class SoundHolder(private val binding: ListItemSoundBinding) :
        RecyclerView.ViewHolder(binding.root) {

    init {
        binding.viewModel = SoundViewModel()
    }

    fun bind(sound: Sound) {
        binding.apply {
            viewModel?.sound = sound
            executePendingBindings()
        }
    }
}

Inside your constructor, you construct and attach your view model. Then, in your bind function, you update the data that view model is working with.

Calling executePendingBindings() is not normally necessary. Here, though, you are updating binding data inside a RecyclerView, which updates views at a very high speed. By calling this function, you force the layout to immediately update itself, rather than waiting a millisecond or two. This keeps your RecyclerView in sync with its RecyclerView.Adapter.

Finally, finish hooking up your view model by implementing onBindViewHolder(…).

Listing 19.25  Calling bind(Sound) (MainActivity.kt)

private inner class SoundAdapter(private val sounds: List<Sound>) :
        RecyclerView.Adapter<SoundHolder>() {
    ...
    override fun onBindViewHolder(holder: SoundHolder, position: Int) {
        val sound = sounds[position]
        holder.bind(sound)
    }

    override fun getItemCount() = sounds.size
}

Run your app, and you will see titles on all the buttons on your screen (Figure 19.11).

Figure 19.11  Button titles filled in

Button titles filled in

Observable data

All may appear to be well, but darkness lies hidden in your code. You can see it if you scroll down (Figure 19.12).

Figure 19.12  Déjà vu

Déjà vu

Did you see an item 67_INDIOS2 at the top, and then one that looks just like it at the bottom? Scroll up and down repeatedly, and you will see other file titles repeatedly appearing in unexpected, seemingly random places. If you do not see this, rotate the device into landscape mode and try again to see how deep the rabbit hole goes. Remember: We only offered you the truth – nothing more…

Worry not! This is not a glitch in the Matrix. This is happening because your layout has no way of knowing that you updated SoundViewModel’s Sound inside SoundHolder.bind(Sound). In other words, the view model is not properly pushing data to your layout file as shown in Figure 19.10. In addition to having a clear division of responsibilities, this step is the secret sauce that separates MVVM from other architectures like MVC, as discussed in the section called Different Architectures: Why Bother? earlier in this chapter.

Your next job will be to make the view model communicate with the layout file when a change occurs. To do this, your view model needs to implement data binding’s Observable interface. This interface lets your binding class set listeners on your view model so that it can automatically receive callbacks when its fields are modified.

Implementing the whole interface is possible, but it requires extra work. We do not shy away from extra work here at Big Nerd Ranch, but we prefer to avoid it when we can. So we will instead show you how to do it the easy way, with data binding’s BaseObservable class.

Three steps are required:

  1. Subclass BaseObservable in your view model.

  2. Annotate your view model’s bindable properties with @Bindable.

  3. Call notifyChange() or notifyPropertyChanged(Int) each time a bindable property’s value changes.

In SoundViewModel, this is only a few lines of code. Update SoundViewModel to be observable.

Listing 19.26  Making view model observable (SoundViewModel.kt)

class SoundViewModel : BaseObservable() {

    var sound: Sound? = null
        set(sound) {
            field = sound
            notifyChange()
        }

    @get:Bindable
    val title: String?
        get() = sound?.name
}

When you call notifyChange() here, it notifies your binding class that all of the Bindable properties on your objects have been updated. The binding class then runs the code inside the binding mustaches again to repopulate the view. So now, when the value of sound is set, ListItemSoundBinding will be notified and call Button.setText(String) as you specified in list_item_sound.xml.

Above, we mentioned another function: notifyPropertyChanged(Int). The notifyPropertyChanged(Int) function does the same thing as notifyChange(), except it is more particular. By writing notifyChange(), you say, All of my bindable properties have changed; please update everything. By writing notifyPropertyChanged(BR.title), you can instead say, Only title’s value has changed.

BR.title is a constant that is generated by the data binding library. The BR class name is short for binding resource. Each property you annotate with @Bindable results in a generated BR constant with the same name.

Here are a few other examples:

    @get:Bindable val title: String // yields BR.title
    @get:Bindable val volume: Int // yields BR.volume
    @get:Bindable val etcetera: String // yields BR.etcetera

You may be thinking that using Observable seems similar to using LiveData, as you learned about in Chapter 11 – and you would be right. In fact, you can use LiveData with data binding instead of the Observable interface. You will find out more about this in the section called For the More Curious: LiveData and Data Binding at the end of this chapter.

Run BeatBox one more time. This time, you should see the right thing when you scroll around (Figure 19.13).

Figure 19.13  All done

All done
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.254.122