Monday 30 November 2015

Android dataBinding - The big Picture



Android DataBinding - The Big Picture




You might  have heard recently DataBinding now has a production candidate and it can make your code cleaner. We can use it for MVVM, updating xml views easier or other strategies.  I highly recommend making it apart of your project planning. 

What is DataBinding ? How many times in an Android program do you use FindViewById to hook up the code and the XML markup? Alot ! Performing findViewById multiple times has overhead as it traverse's the view hierarchy to find the appropriate view.  Now with the Data Binding library you can automatically have View ids automatically transformed to public variables in an autogenerated binding class. Consequently,  you can now do away with FindViewById! Yeah, finally ! and Good bye ButterKnife by square, we have dataBinding now.  ButterKnife still has uses though but thats for another article.  



Setup

I am going to work with a real world example to get you kick started, but first the set up. The full projects on github here: Android-DataBinding-Example At the time of writing, we only need to edit the modules build.gradle with the following to get dataBinding to work: 




This should work for all versions of android.  Thats all folks. Your good to go but hopefully your using a recent version of android studio.


Lets take things more slowly though. First create a new project (file-->new project in android studio) like this:


I called my application DataBindingExample. Hit next on everything else and finally press finish. 


Open your build.gradle file on the app level and make sure it looks like mine and notice the dataBinding closure:


apply plugin: 'com.android.application'
android {

    compileSdkVersion 23
    buildToolsVersion "23.0.2"
    defaultConfig {

        applicationId "com.databindingexample.mycompany.databindingexample"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }

    dataBinding {

        enabled = true

    }

    buildTypes {

        release {

            minifyEnabled false

            proguardFiles getDefaultProguardFile('proguard-android.txt')
        }
    }
}

dependencies {

    compile fileTree(dir: 'libs', include: ['*.jar'])

    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'}



Ensure your using a gradle plugin thats recent. Open up the other build.gradle file on the top level project scope (ie. where you define your jcenter() and gradle classpths etc).



 Ensure it has the latest gradle plugin. Mine looks like this:


Finally, create a new package called ViewModels and in that package create a class called ViewModel. My directory structure now looks like this at the end:



In the next paragraphs below we will work together to create a very simple project. It will contain a countdown timer that only ticks on prime numbers.  The projects goal is to update any xml files TextView  ( from ANY ACTIVITY/FRAGMENT) anytime a countdowns number is a prime number. What!!? Basically, every time a prime number shows up in a countdown, update a textview that has interest in updating itself. To enforce the power of dataBinding, this will be done directly from xml. But how can we update to an xml file, thats impossible ? Not anymore !  Hmmm, this should be interesting, lets get to it.


The prime # countdown timer

Lets create a countdown timer that only ticks on prime numbers found. After that well create a simple interface for it to communicate with other classes.  Afterwards, we will jump onto the binding code. 


Create a java file called MyCountDownTimer.java and paste in the following code:
public class MyCountDownTimer {
    private CountDownTimer countDownTimer;
    private boolean isExecuting =false;
    private ICountDownListener listener;
    private final String TAG = getClass().getSimpleName();

    public MyCountDownTimer(ICountDownListener listener){
        this.listener = listener;
    }

    public void startTimer(long timeLeftMillis) {
        if (!isExecuting) {
            isExecuting = true;
            countDownTimer = new CountDownTimer(timeLeftMillis, 1000) {            
      
      @Override                                                      
        public void onTick(long millisUntilFinished) {
                    
           if(isPrime(millisUntilFinished/1000)){
             listener.doSomethingWithPrimeCountDown(millisUntilFinished / 1000);

                    }
                }

          @Override                
        public void onFinish() {
             isExecuting = false;
             listener.doSomethingWithPrimeCountDown(0L);
                }
            };
            countDownTimer.start();
        } else {
            Log.i(TAG, "Timer already started");
        }
    }

    public void cancelTimer() {
        if (isExecuting) {
            countDownTimer.cancel();
            isExecuting = false;
        }
    }

    public void restartTimer(Long milli) {
        cancelTimer();
        startTimer(milli);
    }

    //checks whether an int is prime or not.

    boolean  isPrime(Long n) {
        //check if n is a multiple of 2                              
        if (n%2==0) 
          return false;
        //if not, then just check the odds                            
        for(int i=3;i*i<=n;i+=2) {
            if(n%i==0)
                return false;
        }
        return true;
    }
}
The interface looks like this:

public interface ICountDownListener {
    void doSomethingWithPrimeCountDown(Long count);
}

This class isn't that important for this tutorial i just wanted you to see that i actually have a countdown timer class.  Anyone who uses it has to implement a ICountDownListener
to get countdowns that are prime numbers, no big deal.

Binding Data to UI

Data binding works like that image above. We create a model and anytime that model changes it will update the UI (xml file) for us. Our goal was to let this work for any activity/fragments UI without restarting the countdown so lets create a singleton viewModel.

Create a java file called ViewModel.java and let it extend BaseObservable. Anytime we want a Model to bind to xml we should extend BaseObservable at least. There are other things you can do but this is common. Well also implement the ICountDownListener so we can get the prime # count down ticks here. 

package com.databindingexample.mycompany.databindingexample.ViewModels;


import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.util.Log;

import com.databindingexample.mycompany.databindingexample.BR;
import com.databindingexample.mycompany.databindingexample.Interfaces.ICountDownListener;
import com.databindingexample.mycompany.databindingexample.MyCountDownTimer;

//notice we are subclassing BaseObservable 

public class ViewModel extends BaseObservable implements ICountDownListener{

    private static ViewModel instance;
    private long countDownTime;
    private MyCountDownTimer mCountDownTimer;
    private final String TAG = getClass().getSimpleName();

    //lock the constructor as this is a singleton    
     private ViewModel(){
/*todo: inject this into the viewModel or pass it into the constructor to unit test with mock countdowntimer*/
        mCountDownTimer=new MyCountDownTimer(this);

    }

    public static ViewModel instance() {
        if (instance == null) {
            instance = new ViewModel();
        }
        return instance;
    }

    /* this is an important annotation. It tells the 
data binding framework that we are interested in updates to the countDownTime variable. 
Here we are saying every time the countDownTime variable changes, 
check the xml file for anyone calling getCountDownTime() or 
countDownTime directly, and update it*/    

     @Bindable    
      public long getCountDownTime() {
        return countDownTime;
    }


    public void setCountDownTime(long countDownTime) {
        this.countDownTime = countDownTime;

        /*BR is very similar to the R file your use to in Android. 
          data binding generates BR files with our variables.
        notifyPropertyChanged rebinds countDowntime with the view
        */
        notifyPropertyChanged((int) BR.countDownTime);
        Log.d(TAG,"prime tick:"+countDownTime);
    }

    public void startCounting(Long milli){
        mCountDownTimer.restartTimer(milli);
    }
    @Override    
      public void doSomethingWithPrimeCountDown(Long count) {
        setCountDownTime(count);
    }
}


note the comment about BR. This is a databinding generated file. Its similar to R generated files in android. There is a @Bindable annotation that we use here to tell the data binding framework we are interested in updates to the countDownTimer variable. It will check our xml file to see if we are asking for countDownTimer and update it if it changes for us. Hmm, what does the xml file look like then? let's find out. 

Open up activity_main.xml, i made mine look like this:

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

    <!-- This is the databinding declaration section.
    Here we declare a name for our binding so that we can refer to it in our
     java class-->
    <data class="CountdownBinder">
        <!--  Now this is kind of like importing a class. Here we import our
        ViewModel class and we name it viewModel (or whatever you like)-->
        <variable name="viewModel" 
       type="com.databindingexample.mycompany.databindingexample.ViewModels.ViewModel"/>
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context="com.databindingexample.mycompany.databindingexample.MainActivity"
        tools:showIn="@layout/activity_main">

        <!-- Here is the textview we will use to listen for countdown changes that
        are prime #'s. We have to convert it to a string because the method returns
        a long
        -->
        <TextView
            android:id="@+id/tv_green"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textSize="26dp"
            android:text="@{Long.toString(viewModel.getCountDownTime)}" />
    </RelativeLayout>

</layout>

the steps are straight forward, we import the class we want to reference and declare a variable of that type. Then we use the variable just like we would in java but put everything in  braces like this "@{...}".  But how does it know which ViewModel class we are using ? That task is for our activity/fragment.

Open up MainActivity.java . mine now looks like this:

package com.databindingexample.mycompany.databindingexample;

import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

import com.databindingexample.mycompany.databindingexample.ViewModels.ViewModel;
import com.databindingexample.mycompany.databindingexample.databinding.CountdownBinder;

public class MainActivity extends FragmentActivity {
    CountdownBinder mCountdownBinder;

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

       mCountdownBinder = DataBindingUtil.setContentView(this, R.layout.activity_main);

        //Lets reference our textview just for fun
        mCountdownBinder.tvGreen.setText("initial text");
        ViewModel viewModel = ViewModel.instance();
        //now tell databinding about your viewModel below
        mCountdownBinder.setViewModel(viewModel);
        viewModel.startCounting(200000L);
    }
}

Notice we are using the CountDownBinder we created in the xml file previously. The static method DataBindingUtil gives us a binder and with that binder we can reference views without calling findViewById !!  Check the xml file, our textview is called "tvGreen" and notice our mCountdownBinder has a reference to it already.  Now we make sure to pass the binder our viewModel thats instantiated. Now dataBinding framework knows which ViewModel we want to use and it can bind it to the xml file. We start the countdown and because of the @Bindable annotation in the viewModel we will always get updates when the countdown changes. And because the ViewModel is a singleton, in another activity we will get a continuation of the countdown, cool !

One concern i have with the binder keeping a reference to the views is when examining a java file now we wont know what views are available. Before we could go to the top of the class and check class members for what views its dealing with. Now we have to go to xml to see what views we can access in the layout we binded to. I mean we could still put the views as class members its just slightly less efficient.

For bonus lets show how we can apply a custom attribute to dataBinding. We are going to change the textviews color to pink. Go back to the xml file and 
make the textview look like the following:

<TextView android:id="@+id/tv_green" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:textSize="26dp" app:primeColor='@{"pink"}' android:text="@{Long.toString(viewModel.getCountDownTime)}" />

notice the custom attribute "primeColor". Here we are passing in a string called pink. I am going to change the textviews text color to pink through databinding. This is a trival example but it shows the power as we could have used an equation or predicate here.

Go back to the viewModel.java class and add the following:

@BindingAdapter({"app:primeColor"})
    public static void setTextColor(TextView view, String color) {

        if("green".equals(color))
            view.setTextColor(Color.parseColor("#63f421"));

        else  if("pink".equals(color))
            view.setTextColor(Color.parseColor("#ffc0cb"));
    }

Here we tell dataBinding we have a custom attribute called primeColor, we do so by using the BindAdapter annotation. It will always pass you a view and in our case since there is one string it passes that also.  If this was a EditText the view would be type EditText etc. Then we set the color accordingly.  This is a static method and in this case we dont have to even tell dataBinding framework about our viewModel as we did in the MainActivity because of the BindAdapter annotation. It just goes hunting for a method to resolve the custom attribute.


Using Observable primitives

There are ways to trigger bindings between view an data without using the annotation @bindable and the notifyPropertyChanged method.

You can look up all the primitives available but the two i usually work with are ObservableBoolean and ObservableInt. Lets look at the boolean observable.  

Let's add a button to our layout that controls restarting the timer. 

<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:textSize="26dp" android:enabled='@{viewModel.canRestart ? true:false}' android:text="@{viewModel.canRestart ? `restart timer`:`unable to restart`}" />


Notice we've added a canRestart to the viewModel. And notice we can use the ternary operator (the elvis operator known by some, its the "?:" syntax) for conditionals.  Lets see what that looks like:

Add a variable to the class declaration of ViewModel.java like this:


public
ObservableBoolean canRestart;


The ObservableBoolean tells dataBinding that anytime this variable changes update the xml file reference to it.

Lets write a method in the ViewModel.java that changes this variable if the prime number count is less then a 191 threshold:



@Override
    public void doSomethingWithPrimeCountDown(Long count) {

        checkIfCanRestart(count);//update the button if necessary
        setCountDownTime(count);
    }

    private void checkIfCanRestart(Long count) {
        if(count > RESTART_THRESHOLD)//threshold here is 191
            canRestart.set(true);
        else
            canRestart.set(false);
    }

the final viewModel looks like this:

public class ViewModel extends BaseObservable implements ICountDownListener{
    private static final Long RESTART_THRESHOLD = 191L;

    private static ViewModel instance;
    private long countDownTime;

   /*any changes to canRestart trigger a UI update automatically*/
    public ObservableBoolean canRestart;

    private MyCountDownTimer mCountDownTimer;
    private final String TAG = getClass().getSimpleName();


    //lock the constructor as this is a singleton
    private ViewModel(){
        mCountDownTimer=new MyCountDownTimer(this);
        canRestart=new ObservableBoolean(false);

    }

    public static ViewModel instance() {
        if (instance == null) {
            instance = new ViewModel();
        }
        return instance;
    }

    @Bindable
    public long getCountDownTime() {
        return countDownTime;
    }


    public void setCountDownTime(long countDownTime) {
        this.countDownTime = countDownTime;

     
        notifyPropertyChanged((int) BR.countDownTime);
        Log.d(TAG,"prime tick:"+countDownTime);
    }

    @BindingAdapter({"app:primeColor"})
    public static void setTextColor(TextView view, String color) {

        if("green".equals(color))
            view.setTextColor(Color.parseColor("#63f421"));

        else  if("pink".equals(color))
            view.setTextColor(Color.parseColor("#ffc0cb"));
    }

    public void startCounting(Long milli){
        mCountDownTimer.restartTimer(milli);
    }

    @Override
    public void doSomethingWithPrimeCountDown(Long count) {

        checkIfCanRestart(count);
        setCountDownTime(count);
    }
//changes to canRestart variable here will trigger a UI update
    private void checkIfCanRestart(Long count) {
        if(count > RESTART_THRESHOLD)
            canRestart.set(true);
        else
            canRestart.set(false);
    }
}
Things to look out for    


  • BR might not get generated right way. Rebuild the project from android studio if necessary. 
  • Don't put business logic in your xml file, its bad practice.
  • Put complex code (ie big expressions) into your ViewModel to encapsulate it. Don't put it in the xml file.
  • You can create custom attributes with static methods (like we did using @BindingAdapter) instead of using attrs.xml

A quick Note on Architecture

For those familiar with MVP architecture: although i've only touched on how to use the DataBinding framework in this article take note that i don't see the relevance of MVP anymore.  MVVM with databinding wipes out the presenter layer. Business logic is moved into the viewModel and model making code easier to test and re-use.  For example, i could take the ViewModel and just drop it into another module and its ready to go (just needs a xml file to bind to).  


Conclusion


I realize this is alot to take in but the rewards are great.  In a few years i can see many developers using this to implement better architecture like MVC and MVVM.  DataBinding structures your code and removes alot of dead weight code we dont want to see. Remember to never calculate business logic in data binding. DataBinding should be used to update the UI. Its for UI changes only, please dont tie your business logic to it and you'll be fine.  Some Developers are comparing ButterKnife to databinding, i don't see the comparision as the architecture you can create with dataBinding far outwieghts anything ButterKnife can do. 

A man may imagine things that are false, but he can only understand things that are true, for if the things be false, the apprehension of them is not understanding. -Isaac Newton

No comments:

Post a Comment