Tuesday, December 9, 2014

Building Data Driven Hierarchical Views

    One common scenario for applications is displaying a UI that is driven by event data. In this tutorial I will go over a very useful technique that I work with almost daily, which I learned from one of the incredibly talented engineers that taught me early on, for building multiple custom views that can be used in various layouts and updated by passing data to one container view per layout. All code for this tutorial can be found on GitHub.

    The first thing we need to do for this example is create an interface that will be used by all of our updateable views. This interface only has one method, update, which is used by our views in order to update their state.

public interface Updateable {
    public void update( Weather weather );
}

    Next we'll implement a simple data model to house all of our data that will drive the views

public class Weather {
    private int temperature;
    private int windSpeed;
    private WindDirection windDirection;
    private WeatherCondition condition;

    public Weather( int temperature, int windSpeed, WindDirection windDirection, WeatherCondition condition ) {
        this.temperature = temperature;
        this.windSpeed = windSpeed;
        this.windDirection = windDirection;
        this.condition = condition;

    }

    public void setTemperature( int temperature ) {
        this.temperature = temperature;
    }

    public int getTemperature() {
        return temperature;
    }
...

    where WindDirection and WeatherCondition simply are enums defined like so

public enum WeatherCondition {
    RAIN,
    SNOW,
    LIGHTNING,
    SUN,
    CLOUDY,
    FOG
}

    Now that we have our data models and interface set up, it's time to start creating the updateable views for our project. To start, we'll go over the standard child views, in this case modified TextView and ImageViews, and then we'll move on to the container view that triggers the state update in all of its child views. Our first example of an updateable view is WeatherImage, which simply implements our Updateable interface and extends ImageView.

public class WeatherImage extends ImageView implements Updateable

    In the update method that comes from the Updateable interface, we check the Weather object for data pertaining to the weather condition, and then display an image based on that data.

@Override
public void update( Weather weather ) {
    Log.e( "WeatherImage", "update!" );
    if( weather == null || weather.getWeatherCondition() == null )
        return;

    switch( weather.getWeatherCondition() ) {
        case CLOUDY: {
            setImageResource( R.drawable.cloudy );
            break;
        }
        case FOG: {
            setImageResource( R.drawable.fog );
            break;
        }
        case LIGHTNING: {
            setImageResource( R.drawable.lightning );
            break;
        }
        case RAIN: {
            setImageResource( R.drawable.rain );
            break;
        }
        case SNOW: {
            setImageResource( R.drawable.snow );
            break;
        }
        case SUN: {
            setImageResource( R.drawable.sun );
            break;
        }
    }
}

    We use this technique for children views for WindWeatherTextView, WeatherTextView and WeatherTemperatureTextView as well, though instead of extending an ImageView, we extend a TextView, as the class names imply.

    Where the magic really happens is in the container view for these updateable child views, UpdateableLinearLayout. Our container view implements Updateable as well, however instead of adjusting itself during update, it calls update on all of its child views.

public class UpdateableLinearLayout extends LinearLayout implements Updateable

@Override
public void update( Weather weather ) {
    Log.e("UpdateableLinearLayout", "Update!" );
    if( weather != null && mUpdateableViews != null && !mUpdateableViews.isEmpty() ) {
        for( Updateable view : mUpdateableViews ) {
            view.update( weather );
        }
    }
}

    mUpdateableViews is simply a list of Updateable objects, and it is populated in onFinishInflate() by looping and recursing through all of the children views under UpdateableLinearLayout and its children in order to create a comprehensive list of views in the current layout that implement Updateable

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mUpdateableViews = findTopLevelUpdateables( this );
}

public List<Updateable> findTopLevelUpdateables( ViewGroup view ) {
    ArrayList<Updateable> results = new ArrayList<Updateable>();

    int childCount = view.getChildCount();
    for( int i = 0; i < childCount; i++ ) {
        results = findTopLevelUpdateables( view.getChildAt( i ), results );
    }
    return results;
}

protected ArrayList<Updateable> findTopLevelUpdateables( View view,
                                                        ArrayList<Updateable> results ) {

    if( ( view instanceof ViewGroup ) && !( view instanceof Updateable ) ) {
        ViewGroup viewGroup = (ViewGroup) view;
        int childCount = viewGroup.getChildCount();
        for( int i = 0; i < childCount; i++ ) {
            findTopLevelUpdateables( viewGroup.getChildAt( i ), results );
        }
    }

    Updateable result = ( view != null && view instanceof Updateable ) ? (Updateable) view : null;
    if( result != null ) {
        results.add( result );
    }
    results.trimToSize();
    return results;
}

    These same methods could easily be used for a RelativeLayout class, or any other ViewGroup that a developer would want to use for their event driven layout.

    Once our view classes are created, we can start to implement them in a custom layout file, as shown in activity_main.xml

<com.ptrprograms.eventdrivenhierarchicalviews.view.UpdateableLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.ptrprograms.eventdrivenhierarchicalviews.view.WeatherImage
        android:layout_width="match_parent"
        android:layout_height="300dp" />

    <com.ptrprograms.eventdrivenhierarchicalviews.view.WeatherTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24sp" />

    <com.ptrprograms.eventdrivenhierarchicalviews.view.WeatherTemperatureTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24sp" />

    <com.ptrprograms.eventdrivenhierarchicalviews.view.WindWeatherTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24sp" />

</com.ptrprograms.eventdrivenhierarchicalviews.view.UpdateableLinearLayout>

    As you can see, our UpdateableLinearLayout is our root ViewGroup for this layout, and all of its children are custom views that implement our Updateable interface. It is good to note that our UpdateableLinearLayout can support other children views that do not implement Updateable, though they will not automatically update when new data is present.

    For this example we implement this layout in our MainActivity and save a reference to the root UpdateableLinearLayout in onCreate.

setContentView(R.layout.activity_main);
mRootView = (UpdateableLinearLayout) findViewById( R.id.root );

    For this example, since we aren't using a live API in order to drive our data, I have created a method that simply goes through random Weather data every three seconds and calls update on the root view in order to simulate fresh data changing our view.

private void startSimulation() {
    mHandler = new Handler();
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if( mCurrentItem >= mWeather.size() )
                mCurrentItem = 0;

            mRootView.update( mWeather.get( mCurrentItem++ ) );

            if( mHandler != null )
                mHandler.postDelayed( this, 3000 );
        }
    };

    runnable.run();
}

   And with that we now have custom views that change how they are displayed based on updating data, and can be reused in multiple places across our app by simply building multiple layout files. This technique has been invaluable in my every day development, and I hope it'll be useful for other Android developers as well.



5 comments:

  1. 1. You create dependency on your classes.
    2. If you change viewGroup to RelativeLayout - you should change\ add your class as well.
    3. Isn't it too much work to change olny few views.? You create Class for each view, each ViewGroup and.
    4. Isn't is easier to implement Updateable interface in Activity or Fragment?
    Thanks

    ReplyDelete
    Replies
    1. It depends on what you're doing. If you're going to reuse those views from a library across multiple apps, this way works amazingly. You could have a LinearLayout updateable, and a RelativeLayout, so switching them is as easy as switching a normal layout file. In my case this design pattern has been great because I do a lot with sports data, so all of my views are constantly changing all at once, so when the logic is baked into the view and I only need one line to actually control it, it makes life a lot easier when I can reuse the same views in different layouts across multiple places in my app.

      Delete
  2. good article, but how about with many models, so i have to implement each custom view for each model, and then, i have too much custom view and how to control this custome views.

    ReplyDelete