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 SecondTab(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!
Tags: Android, Application Design, Java