LayoutInflater Factories

Inflating your own custom views, instead of letting the system do it, gives you some nice benefits such as the ability to skip reflection for performance gains, or to have custom view constructors, or to simply remove the need for fully-qualified class names when declaring your custom views in XML. You could even use it to silently replace system components with your own customised ones, which is exactly how the support library replaces the default system widgets with themed widgets.

Inflating your own views is simple. Each activity has an associated LayoutInflater, which is passed to all of your fragments, and can be retrieved at any time using LayoutInflater.from(Context);. Factory classes can be attached to this LayoutInflater which will provide hooks for you to attempt to inflate views before the default system code takes over.

If you're interested, you can see this happening yourself in LayoutInflater#createViewFromTag(View, String, AttributeSet, boolean).

How to use a LayoutInflater Factory

This section will explain using custom LayoutInflaters vs. using your Activity as a LayoutInflater factory.

Using custom LayoutInflater Factories

Custom LayoutInflater factories can be created by implementing the support library class LayoutInflaterFactory and setting your implementation in your Activity, like so:

@Override public void onCreate(Bundle savedInstanceState) {
    LayoutInflaterCompat.setFactory(getLayoutInflater(), new MyLayoutInflaterFactory(this));
    super.onCreate(savedInstanceState);
    ...
}

Note that I'm calling this before super.onCreate(Bundle)

The major limitation with LayoutInflater factories is that only one factory can be attached to each LayoutInflater. Because the support library already attaches it's own factory, doing this is going to cause problems. You won't be able to inflate your fragments from XML, and you won't be able to inflate your themed views provided in v21.

To work around this, the documentation states:

If you are using your own Factory or Factory2 then you can omit this call [installViewFactory], and instead call createView(android.view.View, String, android.content.Context, android.util.AttributeSet) from your factory to return any compatible widgets.

Basically, it's saying that custom factory implementations are responsible for calling AppCompatDelegate#createView(android.view.View, String, android.content.Context, android.util.AttributeSet) themselves in order to get the support library features. I'll cover this in the last section of the article for anyone interested in how to do this.

Alternatively, you can work around the single factory limitation by using the LayoutInflater#cloneInContext(Context) method to create a new LayoutInflater instance and attach your custom factory to the new instance. This will fix the issue by merging the support library factory with yours. Note that if you do it this way, you'll probably want to override getLayoutInflater() and getSystemService(String) in your Activity to return your customised LayoutInflater instead of the original (example in gist at the end).

Using your Activity as a LayoutInflater factory

All LayoutInflaters already have a default private LayoutInflater factory set on them - its containing Activity (the Activity class implements Factory and Factory2).

What this means is that you can simply override the following methods to handle custom view inflation within your activity itself:

View onCreateView(View, String, Context, AttributeSet);
View onCreateView(String, Context, AttributeSet);

This is a good approach as it is compatible with the support library features without any additional work.

How to write a LayoutInflater Factory

This section will cover a couple example factory classes.

A factory to remove fully qualified class names in XML

Check out this layout file from the u2020 application. It uses a lot of custom views. It might be nice to remove those package names for every custom view so that you can more freely refactor views in the future without having to update layout files, or to simply just make this file slightly more readable.

An example factory that does this could look like the following:

public class MyLayoutInflaterFactory implements LayoutInflaterFactory {
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if (TextUtils.equals(name, "DebugDrawerLayout")) {
            return new DebugDrawerLayout(context, attrs);
        } else if (TextUtils.equals(name, "ScrimInsetsFrameLayout") {
            return new ScrimInsetsFrameLayout(context, attrs);
        }
        // and so on...
    }
}

Pretty simple right? But perhaps you don't want to have to manually list out all of your custom views in an inflater like the example above. If that's the case, you could take a page out of the Android framework's book and use reflection to instantiate your views instead:

public class MyLayoutInflaterFactory implements LayoutInflaterFactory {
    private static final String CUSTOM_VIEWS_PACKAGE = "com.example.ui.customviews.";

    private static final Class<?>[] constructorSignature = new Class[] { 
            Context.class, AttributeSet.class };

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        Constructor<? extends View> constructor = null;
        Class<? extends View> clazz = context.getClassLoader()
                .loadClass(CUSTOM_VIEWS_PACKAGE + name).asSubclass(View.class);
        constructor = clazz.getConstructor(constructorSignature);
        constructor.setAccessible(true);
        return constructor.newInstance(context, attrs);
    }
}

Error handling and caching ignored for clarity

The main caveat with this code is that all of your custom views must be contained within a single package. This is similar to how the Android Framework works, as all of the Framework views are provided in one of 3 packages:

See PhoneLayoutInflater.java, from AOSP, has the following package list it tries to inflate views from:

private static final String[] sClassPrefixList = {
    "android.widget.",
    "android.webkit.",
    "android.app."
}

The other (pretty major) issue with the above code is that it is completely un-optimised. If you look at the Android Framework implementations, it does a lot of caching - particularly around the reflection calls. I'll provide a github gist with properly cached example code at the end of the article.

A factory to allow for custom view constructors

This is a particularly useful trick, and actually the main reason why I started looking at LayoutInflater factories in the first place. I wanted a way to cleanly pass dependencies of my custom views to each instance. In my specific example, I wanted to pass my dagger 2 activity-scoped component directly to any interested view without writing manual getters and setters for each view.

Note that this can be achieved other ways too. A pretty common example nowadays is hooking into the getSystemService method of context. See Mortar as an example of that.

Let's look at the end goal, we want a view with a constructor that looks like this:

public MyCustomView(Context context, AttributeSet attrs, MyDagger2Component component) {
    // Inject dependencies using component
}

This is pretty nice because it means we have type safety for getting our component!

Achieving this isn't too hard either. See the constructor signature defined in the previous example?

private static final Class<?>[] constructorSignature = new Class[] { 
        Context.class, AttributeSet.class };

We simply just need to change it to the following:

private static final Class<?>[] constructorSignature = new Class[] { 
        Context.class, AttributeSet.class, MyDagger2Component.class };

Now the only thing we need to do is pass our component into the newInstance method, and we're done!

How to keep using the support library features with your custom LayoutInflater factory

Note that this section only applies if you are creating and attaching your custom LayoutInflater factory to your LayoutInflater. If you are simply overriding onCreateView(View, String, Context, AttributeSet) in your Activity, then you can skip this section.

This is actually a bit more tricky than it should be, and I think there is actually a bug in the framework code in this area. (Fixed in v22.2 of the support library)

Basically you have to do 2 things 1 thing to have the support library operate properly:

1. Call FragmentActivity#onCreateView(String, Context, AttributeSet) in order to inflate support library fragments from XML. This step wouldn't be necessary if FragmentActivity actually overrides Activity#onCreateView(View, String, Context, AttributeSet). Right now it only overrides Activity#onCreateView(String, Context, AttributeSet), so the default LayoutInflater in Android misses any support library fragment creation. More information about the bug can be found here.
2. Call AppCompatDelegate#createView(View, String, Context, AttributeSet) in order to inflate AppCompat custom views.

This can be achieved like so:

public class CustomViewsLayoutInflaterFactory implements LayoutInflaterFactory {
    private AppCompatDelegate appCompatDelegate;

    public CustomViewsLayoutInflaterFactory(AppCompatDelegate appCompatDelegate) {
        this.appCompatDelegate = appCompatDelegate;
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View result = null;

        // todo: your custom inflation code here!

        if (result == null) {
            // Get themed views from AppCompat
            result = appCompatDelegate.createView(parent, name, context, attrs);
        }
        return result;
    }
}

As you can see, it's just a little bit more work on the end of your custom factory.


That's everything! Code examples for all of these can be found here.

Thanks for reading!

Brad Campbell

Android application developer, currently working as the lead on the ANZ goMoney NZ applications. All of the opinions and code on this blog are my own, and not that of ANZ.