Trifork Blog

Exploring the world of Android :: Part 3

October 8th, 2009 by
| Reply

In my travels through the world of Android I faced a lot of challenges. Brave as I am, *cough* I conquered each one of them. A few of the challenges include saving activity state, asynchronous tasks, pagination, error handling, context/option menu's and even drawing custom application/tab icons in Photoshop! Some challenges I already shared with you guys, but there is one challenge in particular I would like to elaborate on this time.

The app I am currently building is getting larger every day, and so is the main Activity class! Because my main activity contains a TabHost with a bunch of tabs, it also contains references to all individual view components contained in those tabs. All kinds of listeners are registered on those components so the activity contains some inner and anonymous classes as well. So you could say that this activity now has way too much responsibility! What I was looking for, is a way to separate the main activity into multiple parts, each with its own clear responsibility.

As it turns out, you can create custom components for a single piece of functionality within an Activity. Exactly what I was looking for!

Writing custom components

There are different ways to write custom components for your Android application.

  • Simply extend a view class - If you want to add some behavior to an existing view class (like a TextView or a ListView) you can simply extend the class and override any method you like.
  • Create an entirely customized component - Extend the View class and implement everything from scratch. Create listeners, properties, implement onMeasure() and onDraw(), etcetera. This might be useful if you want a very specific component and fine grained for your needs, for example a slider control.

Read about the above approaches here: http://developer.android.com/guide/topics/ui/custom-components.html.

Compound components

Unfortunately both of the above approaches are not what I was looking for in this case. I was looking for a way to simply group a bunch of view components in a separate class, also containing all related behavior logic. Luckily there is a way to do this! In the documentation they call it "compound components", which is exactly what I was looking for. To explain it, I will try to demonstrate it with the following example:

public class MyActivity extends TabActivity {

    // First tab
    private LinearLayout linearLayout;
    private EditText editText;
    private ListView listView;

    // Second tab
    private RelativeLayout relativeLayout;
    private ImageView imageView;
    private TextView textView;
    private TextView anotherTextView;

    // Third tab
    private TableLayout tableLayout;
    private TextView tableLabel;

    @Override
    protected void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.main);

        // Initialization logic for all the above components
    }

    // All kinds of helper methods for things like the initialization logic, 
    // listeners, context and option menu's ...
    
    private void handleSomeClick() {
        // ...
    }

    private class FirstTabEditTextClickListener implements View.OnClickListener {
        public void onClick(View view) {
            // On click logic for the editText component
        }
    }

    // Some more listener inner classes ...
}

The "main.xml" layout file would look something like this:

<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@android:id/tabhost"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent">

    <LinearLayout android:orientation="vertical"
                  android:layout_width="fill_parent"
                  android:layout_height="fill_parent">

        <TabWidget android:id="@android:id/tabs"
                   android:layout_width="fill_parent"
                   android:layout_height="wrap_content"/>

        <FrameLayout android:id="@android:id/tabcontent"
                     android:layout_width="fill_parent"
                     android:layout_height="fill_parent">
            <LinearLayout android:id="@+id/firstTab"
                          android:layout_width="fill_parent"
                          android:layout_height="fill_parent">
                <!-- All view components of the first tab -->
            </LinearLayout>
            <RelativeLayout android:id="@+id/secondTab"
                          android:layout_width="fill_parent"
                          android:layout_height="fill_parent">
                <!-- All view components of the second tab -->
            </RelativeLayout>
            <TableLayout android:id="@+id/thirdTab"
                          android:layout_width="fill_parent"
                          android:layout_height="fill_parent">
                <!-- All view components of the third tab -->
            </TableLayout>
        </FrameLayout>
    </LinearLayout>
</TabHost>

Identify the components

The first thing to do is to identify the individual components you want to separate. In this case we can simply split this activity into three components, one for each tab. Of course, the Activity itself will still exist for obvious reasons (handling menu items, displaying dialogs, etc.). So in the end we'll have a total of 4 classes.

Create new component classes

Second step is to create a new class for each component you want to create, which extends some layout class. In the case of this example those layout classes are RelativeLayout, LinearLayout and TableLayout (see above layout definition). Next, move all components contained in those layouts to the appropriate classes along with all related logic (listeners, etc.).

Example of such a component:

public class FirstTab extends LinearLayout {
    private ImageView imageView;
    private TextView textView;
    private TextView anotherTextView;

    public FirstTab(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);

        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.firstTab, this);
    }
}

Classes called SecondTab and ThirdTab can be implemented in a similar way. First thing to notice about the above implementation is the constructor. It should always have the following 2 arguments: Context and AttributeSet. Without such a constructor this component can't be instantiated from XML. Another thing to notice is that a layout called "firstTab" is inflated on instantiation. All instantiated views in that layout are added as a child of FirstTab

Now, let's try to create the "firstTab" layout that is being instantiated from the FirstTab component class:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- All view components of the first tab -->
</LinearLayout>

There is a problem with this layout definition. If you think about it, you'll notice what's wrong with it. The problem is that when this layout is inflated, an instance of LinearLayout is created with all it's defined child components attached. This is a problem because after the LinearLayout is instantiated, it is attached as a child component of the FirstTab, which itself already is a LinearLayout. So using this layout you will get a redundant LinearLayout in between the FirstTab and its child components.

The solution for this problem took me a while to figure out, but is actually quite simple. Instead of LinearLayout you should use the merge tag like this:

<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- All view components of the first tab -->
</merge>

The merge tag makes sure that all its child views are attached to the merge tag's parent instead. This is exactly what we want! Because now no new LinearLayout is instantiated and all child components are attached directly to the FirstTab.

To reference your new components from the original layout definition "main.xml", you should use the fully qualified class name of the components. So this is how the layout could look like now:

<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@android:id/tabhost"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent">

    <LinearLayout android:orientation="vertical"
                  android:layout_width="fill_parent"
                  android:layout_height="fill_parent">

        <TabWidget android:id="@android:id/tabs"
                   android:layout_width="fill_parent"
                   android:layout_height="wrap_content"/>

        <FrameLayout android:id="@android:id/tabcontent"
                     android:layout_width="fill_parent"
                     android:layout_height="fill_parent">
            <com.example.android.FirstTab android:id="@+id/firstTab"
                          android:layout_width="fill_parent"
                          android:layout_height="fill_parent"/>
            <com.example.android.SecondTab android:id="@+id/secondTab"
                          android:layout_width="fill_parent"
                          android:layout_height="fill_parent"/>
            <com.example.android.ThirdTab android:id="@+id/thirdTab"
                          android:layout_width="fill_parent"
                          android:layout_height="fill_parent"/>
        </FrameLayout>
    </LinearLayout>
</TabHost>

This is it! Now you have divided your views over several components (classes).

Keep components loosely coupled

Now that you have all your view and behavior logic divided into multiple classes, you will probably run into some issues. Examples:

  • FirstTab wants to refresh the SecondTab after a click on certain button
  • Menu items are handled in MyActivity, but actually need to make changes to ThirdTab when selected

Listeners

The issue with the first example is, that FirstTab doesn't have a reference to SecondTab to be able to refresh it. Under no circumstance should you give the FirstTab a direct reference to the SecondTab. This would result in very tight coupling between the components, which generally is not a good idea. Instead try to solve the issue using listeners. This is already a pattern implemented in a lot of places in the Android SDK itself, for example here: View#setOnClickListener(OnClickListener). So in the above example, you should create your own listener interface:

public interface SearchListener {
    void onSearch(SearchResult searchResult);
}

This could for example be a listener for when a search is performed in the FirstTab. The MyActivity class could register an anonymous implementation of this listener interface and call the SecondTab to refresh it.

firstTab.setSearchListener(new SearchListener() {
    public void onSearch(SearchResult searchResult) {
        secondTab.refresh(searchResult);
    }
});

A similar thing could be done when handling menu item clicks in the MyActivity, only this time listeners are not needed.

public boolean onMenuItemSelected(int featureId, MenuItem item) {
    switch (item.getItemId()) {
        case MENU_ADD_ENTRY:
            thirdTab.addEntry();
            break;
    }
}

So, create methods on your custom components if you need to control them from your Activity. Never expose any child components of your custom component to the Activity. The Activity should not have to know about them at all. Instead, create methods on your custom components that controls these child components like the above examples (refresh(...) and addEntry()).

I think with similar approaches, all issues can be solved when splitting your activity in multiple components. When you solve all issues you might have, you will have a cleanly separated Android application! This improves the overview of the application and makes it more maintainable.

Summary

I've shown you how to avoid making a mess of your application's Activities by splitting clear chunks of functionality into custom components. Also, to avoid tight coupling between the custom components, I suggested to use Listeners.

If you have ever tried to deal with this before and have other suggestions to approach this, please leave a comment! Also, if you decided to try this structure in your application, let me know how that works out for you by leaving a comment!

9 Responses

  1. May 19, 2010 at 18:24 by Leigh

    Exactly what I was looking for, thank you very much.
    I saw your question on StackOverflow, which was exactly the sort of thing I was after. Thanks for following it up with a blog post.

  2. September 14, 2010 at 08:53 by Zarah

    Hi! Thanks for this! I am trying to create an app with a TabHost and I used to have Activities inside each tab, which according to a LOT of guys on StackOverflow is a no-no. Then I found this and tried your approach. :)

    Looks pretty good! But somehow I can't make it to work. :( I get the tabs, but the content are is blank. Would debug some more and let you know. (I would also accept suggestions!)

    Thanks again!

  3. September 14, 2010 at 09:04 by Zarah

    Oooops, maybe I spoke too soon. Found the problem with the empty tab contents, turned out it's just from my layout. I wonder, if you have a button in one of your tab contents and you want to show a dialog onClick of that button, how do you tell your tab activity to show it?

    Thanks again! :)

  4. September 17, 2010 at 15:40 by Martin

    Very well written/explained - thanking you

    But one question - say our composite control has a textview to display a caption - how do we use the attributes in the main.xml layout to pass a string value to be used as the caption?

  5. December 10, 2010 at 16:45 by GiUmaTo

    Hi. Well done, good explanation. I saw your question on stackoverflow too ... maybe it would be nice to cite the guy who answered :).
    Anyway, probably there's a typo in the code listing for 'public class FirstTab': the constructor found afterwards is 'public SecondTab(Context context...', it should be for the same FirstTab.
    Regards

  6. December 24, 2010 at 16:21 by Raja V

    Great stuff.. After so many thought of doing this with inheriting the custom Activity, I found this tutorial as very simple and cool stuff... Thanks a lot..

  7. January 11, 2011 at 22:33 by Lamont

    Great article! I think there is a small error in the code listing for FirstTab. The constructor is named SecondTab.

  8. January 12, 2011 at 09:08 by Tom van Zummeren

    @Lamont You are totally right! Thank you.

  9. March 5, 2012 at 15:24 by sakis

    amazing article!
    I managed to get it working in minutes!
    thanks a lot for your effort.

Leave a Reply