Writing a maintainable custom view in Android

If you are an Android developer at some point you will write a custom view. It is easy to implement it, but there are a couple of things you have to be careful about. This blog post will show you a few tips on how to write a maintainable custom view. As a result, future you will be happy and anyone who takes over your project will hate you a little less. If you are not familiar with custom views I encourage you to read the official documentation.

Do I need a custom view?

I have to admit, I love custom views a little too much. When I write a layout I am always thinking if I could convert it into a custom view. Common sense prevails and I only write them if it makes sense. There are two main use cases when you would write a custom view:

1. The view is used across your application and you do not want to copy-paste the code.
2. Android built-in components are not enough because you need something cool, something wild.

Here are a few tips to help you write better and less error-prone views.

Custom view example

Custom view example

Developing custom views

1. Do not write your own constructor, use setters.
When you extend the ViewGroup you have to create constructors which match the superclass. Those constructors are the only ones you need. Here is an example of a custom constructor which you should never add to your custom view.

    public CustomView (Context context, String title, String subtitle) {
        ...
    }

I can assure you that someone using your custom view will do this:

public CustomView(Context context) {
    ...
}

It does not seem like a big deal, but this will force the person to check the implementation to see how to set the title and subtitle. Nobody wants to do that and you should think of a custom view as an API. Expose properties so that anyone can use your custom view without thinking too much.

public void setTitle(String title) {
    ...
}

2. Make sure you initialize the UI
If your custom view has XML layout make sure that you inflate and bound it no matter what. When someone creates your view via a constructor which does not initialize the UI they will probably get a null pointer exception at some point.

I do this by invoking other constructors and the last constructor initializes the view. But you can do this any way you want.

public CustomView(Context context, AttributeSet attrs) {
   this(context, attrs, 0);
}

public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
   super(context, attrs, defStyleAttr);
   init(attrs, defStyleAttr);
}

private void init(AttributeSet attrs, int defStyleAttr) {
    //UI initialization
}

3. Use attachToRoot and Butterknife
It is easy to forget that LayoutInflater has a method which allows you to add the view to its parent. Instead of calling the addView(View view) method you can do it implicitly by passing true to inflate method.

View view = LayoutInflater.from(getContext()).inflate(R.layout.custom_view, this, true);
ButterKnife.bind(this, view);

Also, use ButterKnife because it helps you get rid off all that boilerplate code which does nothing useful.

4. Use <merge/> tag
You should think about performance and do your best to reduce layout complexity. Layouts with a high number of views are hard to read and they can impact the performance of your app. I wrote a blog post regarding lightweight layouts, but in this one I want to draw your attention to a tag which is not used as much as it should be.

Let’s see a simple example in which you extend a LinearLayout.

public class CustomView extends LinearLayout {
...
}

If the root view of the layout you are adding to this CustomView is LinearLayout you can replace it with <merge/> tag. That way instead of two LinearLayouts in your view hierarchy you have only one.

<merge xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
</merge>

There is one thing you should keep in mind. You can use <merge/> tag only with a valid ViewGroup root and attachToRoot has to be set to true.

5. Do not forget to call invalidate() and requestLayout() methods
If your setter methods change the appearance of the view you have to call invalidate() and/or requestLayout() methods. Calling them causes the view to redraw itself and check for property changes. As Android documentation states, forgetting these method calls can cause hard-to-find bugs.

public void setViewTitleColor(int titleColor) {
    this.titleColor = titleColor;
    invalidate();
    requestLayout();
}

6. Separate view from business logic
Depending on the size of your custom view, it is a good idea to think about some architectural pattern. By abstracting a view’s state and behavior it will become less error-prone and easier to test. I prefer MVP or MVVM, but it is up to you to decide what you are going to use.

7. Save view state on rotation change
I am sure that you know this, but it is worth mentioning. Android framework has one “cool feature” which triggers when the device orientation changes. It kills your activity and recreates it from the scratch.

Instead of saving the instance state in activitiy or fragment, you can do it in your custom view. It is not as easy as retaining the state of an activity or fragment, but it is worth the trouble.

Retaining the state is way out of the scope for this blog post, but you can find a great read about this here. That post gives you a great example how to retain a custom view state works.

If the view has a lot of states which you should save on rotation change make sure you handle them in your custom view.

8. Define Custom Attributes
Custom attributes allow you to control the appearance and behaviour of your view from XML. Often, this is preferable than setters. By adding them you can reuse your code by extracting it as a style. Here is how you custom view looks like in XML with custom attributes.

<xyz.ivankocijan.customview.CustomView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:title="I am Batman"
    app:isEnabled="true"/>

Use custom attributes as much as you can because they make your custom view a lot easier to use. You can read more about them in the official documentation.

Write less error-prone custom view

Custom views help you reuse layout and implement UI you want, but before you start writing them think if you can do the same job with something else. Depending on your use case maybe something else will do the trick.

Next time you write a custom view remember these tips. You do not have to follow them, but in my experience they will make your life easier and your code less error-prone.

Thanks to Željko for proofreading this blog post.

  • Angel Romero

    Thanks for the very interesting tips!

    I am writing a library project which will be effectively a custom view (quite a complex one!). Because of its complexity, I would like to extract all the presentation logic into a different class so I can test it and it is not tightly couple with the Android Framework. However, because the clients are going to interact with this component by adding it to a Layout file, all the public API has to live in the actual custom ViewGroup.

    You mention that you like to separate your business logic following a MVP or MVVM architecture/design and it would be great if you could expand a little bit more on that. I would like to do the same but I have the following problems:
    – There’s no Activities or Fragments, just Views and ViewGroups.
    – How to have dependency injection in the actual library (which is a completely different topic to the one in your article, but would be nice if you had any suggestion about it).
    – How do I test my library code. At the moment I am just providing test doubles for the Framework classes that my Views use.

    I would be really interesting if you could share any of your thoughts on these topics.

    Thanks again,
    Angel