Combining data binding with Lifecycle components

We can use data binding along with Lifecycle components (e.g. ViewModel and LiveData) to remove the controller (Activity or Fragment) as middle-man from the process of passing data from the ViewModel to the View.

Without data binding (at least, only using data binding to avoid findViewById):

import ...

class MyFragment : Fragment() {

    private lateinit var viewModel: MyViewModel // Declare a MyViewModel object

    private lateinit var binding: MyFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Inflate view and obtain an instance of the binding class.
        val binding: ScoreFragmentBinding = DataBindingUtil.inflate(
                inflater,
                R.layout.my_fragment,
                container,
                false
        )

        viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java) // Initialise the MyViewModel object to a MyViewModel instance
        viewModel.myVar.observe(this, Observer { myVar -> // Observe a variable in the ViewModel class
            binding.myTextview.text = myVar.toString() // Use it to change our UI
        })

        binding.myButton.setOnClickListener { viewModel.changeVar() } // In this example, we have a button with an onClickListener which calls a MyViewModel function to change the variable

        return binding.root
    }
}

With data binding:

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

    <data> // Add a data tag
        <variable // With a variable
            name="myViewModel" // Give it a name
            type="com.example.android.myapp.MyViewModel" /> // Point to the ViewModel class
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/app_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/my_textview"
            android:text="@{@string/string_format(myViewModel.myVar)}" // myVar is a variable within the ViewModel with a public getter. Don't need myVar.value thanks to data binding. Defaults to empty String. This has been wrapped in a String format defined in strings.xml ( <string name="string_format">\"%s\"</string> ) which wraps the String in quotes. Customise this for other types e.g. Ints, or use String.valueOf()
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <Button
            android:id="@+id/my_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> myViewModel.changeVar()}" /> // Add an onClick to provide a listener for the button

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

And our Fragment looks like:

import ...

class MyFragment : Fragment() {

    private lateinit var viewModel: MyViewModel // Declare a MyViewModel object

    private lateinit var binding: MyFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Inflate view and obtain an instance of the binding class.
        val binding: ScoreFragmentBinding = DataBindingUtil.inflate(
                inflater,
                R.layout.my_fragment,
                container,
                false
        )

        viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
        // We have removed our onClickListener and Observer

        binding.myViewModel = viewModel // Link your ViewModel with the data tag in the xml file. Allows 

        binding.setLifecycleOwner(this) // Set the Lifecycleowner of the bindingto the current Fragment. Allows us to automatically update layouts

        return binding.root
    }
}

For anything more complicated than plain text we meed to move the logic and data manipulation to the ViewModel and use the likesĀ  of Transformations.map (see this post).

Data class

Kotlin Data class.

fun main() {

    val emp = Employee("John", true) // Regular class instantiation
    println(emp)
    val car = Car("blue", "Toyota", 2015) // Data class instantiation
    println(car) // Improved toString(), shows all labels and values, instead of reference

    val emp2 = Employee("John", true)
    println(emp == emp2) // false as are referentially inequal

    val car2 = Car("blue", "Toyota", 2015)
    println(car == car2) // Compares structurally instead of referentially. Uses equals() function

    val car3 = car.copy() // Makes a structurally identical object
    println(car3)
    val car4 = car.copy(model = "Audi") // Copy with variations
    println(car4)
    val car5 = car.copy("Silver", "Audi", 2004) // Copy with variations. Can exclude identifier if changing all, but then why would you copy?
    println(car5)
}


// Main purpose of data class is to store state
// Data classes have improved toString() function, custom implementation of the equals() and hashcode() functions and have copy() function. All can be overridden
// Have all that is needed for a destructuring declaration
// Cannot be abstract, sealed or inner classes
data class Car(val colour: String, val model: String, val year: Int) { // Must have at least one parameter in constructor, all must have val or var i.e. must be declared in constructor
    val notMe: String = "nope" // will not benefit from improved functions as not declared in constructor
}

//Regular class for comparison
class Employee (val firstName: String, val fullTime: Boolean)

 

onSaveInstanceState

How to persist data across changes in state such as device rotation. The onSavedInstanceState function stores information in a Bundle and is limited in the data it can contain (small amounts of data which can be easily serialised and de-serialised). It does not persist on app closure. **Try to keep the contents to considerably less than 100KB as this resides in RAM**

This example overrides the onPause and onSaveInstanceState functions to append an update to the screen when these events occur. When the screen is rotated the onSaveInstanceState function is called – it takes the contents of the screen (in particular the tv_lifecycle_events_display TextView) and stores it in the savedInstanceState Bundle. When onCreate is called after screen rotation, it checks for the LIFECYCLE_CALLBACKS_TEXT_KEY in the Bundle and, if present, writes the data back to the screen.

Add the following to your Activity file:

private static final String LIFECYCLE_CALLBACKS_TEXT_KEY = "callbacks"; // Key string constant for the data you will store
private static final String ON_PAUSE = "onPause"; // The text that will be displayed onscreen when the onPause state occurs
private static final String ON_SAVE_INSTANCE_STATE = "onSaveInstanceState"; // The text that will be displayed onscreen when the onSaveInstanceState state occurs

    protected void onCreate(Bundle savedInstanceState) { // Standard onCreate override
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        private TextView mLifecycleDisplay = findViewById(R.id.tv_lifecycle_events_display); // TextView the data will be printed to


        /*
         * If savedInstanceState is not null, that means our Activity is not being started for the
         * first time. Even if the savedInstanceState is not null, it is smart to check if the
         * bundle contains the key we are looking for. In our case, the key we are looking for maps
         * to the contents of the TextView that displays our list of callbacks. If the bundle
         * contains that key, we set the contents of the TextView accordingly.
         */
        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey(LIFECYCLE_CALLBACKS_TEXT_KEY)) { // If key is present...
                String allPreviousLifecycleCallbacks = savedInstanceState.getString(LIFECYCLE_CALLBACKS_TEXT_KEY);
                mLifecycleDisplay.setText(allPreviousLifecycleCallbacks); // ... display all of the previous contents to the TextView
            }
        }


    @Override
    protected void onPause() {
        super.onPause();
        mLifecycleDisplay.append(ON_PAUSE + "\n"); // Displays the event onscreen
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) { // Override onSaveInstanceState
        super.onSaveInstanceState(outState); // Call super class - ALWAYS DO THIS
        mLifecycleDisplay.append(ON_SAVE_INSTANCE_STATE + "\n"); // Displays the event onscreen
        String lifecycleDisplayTextViewContents = mLifecycleDisplay.getText().toString(); // Gets all of the content from the TextView
        outState.putString(LIFECYCLE_CALLBACKS_TEXT_KEY, lifecycleDisplayTextViewContents); // Puts TextView content into savedInstanceState Bundle
    }

}