Creating Custom LayoutInflater
Originally published here.
Preface
I think LayoutInflater is one of the most under-researched and under-appreciated components of the Android framework. Which is ironic, in a way, since it’s used in pretty much every application ever written.
Anyway, in this short article I intend to show a simple example of custom LayoutInflater, discuss a few specifics of its inner workings, and hopefully, show why it’s worth using in your application!
Just give me the code
If you don’t really want to follow the article and would rather just poke the code by yourself here’s the repo with the sample to play with.
What is LayoutInflater
While LayoutInflater is used pretty much in every UI component on Android platform, more often than not it’s hidden under the hood (Activity’s getLayoutInflater) and when it’s used explicitly (RecyclerView’s Adapter) - it’s used as black-box.
So what does it do? Well, it’s quite straightforward - it translates XML layout into a View object. And also sometimes it swaps default implementation of views to their AppCompat counter-parts (not directly tho, i.e. there is no AppCompatLayoutInflater, see androidx/appcompat/app/AppCompatDelegateImpl.java instead). And also it uses reflection to create objects (and if you ever wondered why the creation of layout from XML is much slower than the creation of layout in code, here’s the part of the reason why). And also it has factories inside, so you won’t get away with just subclassing LayoutInflater.
Well in order to get it all together let’s concentrate on the practical example. In this article, we will focus on the creation of LayoutInflater which will allow us to define a custom attribute to parse and the behaviour based on the value of this attribute. In this specific example, we will make a “color” attribute which will apply colours from our custom theme, but obviously, you can come up with any other attributes and behaviours you want.
Defining Theme
This will be really straightforward, let’s define a data class with few colours inside:
import android.graphics.Color data class CustomTheme( val darkPurple: Int = Color.parseColor("#330C2F"), val maximumPurple: Int = Color.parseColor("#7B287D"), val violetBlueCrayola: Int = Color.parseColor("#7067CF"), val lavenderBlue: Int = Color.parseColor("#B7C0EE"), val aeroBlue: Int = Color.parseColor("#CBF3D2") )
Defining layout
We will start with defining custom namespace like this:
xmlns:custom="https://syllogismobile.wordpress.com/"
Value is totally up to you, just make sure it starts with “https://” otherwise Android Studio will complain and throw a warning.
We will use attribute named “color” which value will be the name of colour defined in our custom theme above like this:
custom:color="darkPurple"
Our example layout will look like this:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools" xmlns:custom="https://syllogismobile.wordpress.com/" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity" custom:color="darkPurple"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Hello World!" android:textSize="36sp" android:gravity="center" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" custom:color="lavenderBlue"/> <View android:layout_width="match_parent" android:layout_height="50dp" custom:color="aeroBlue"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Hello World!" android:textSize="36sp" android:gravity="center" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" custom:color="violetBlueCrayola"/>
</LinearLayout>
Note that custom namespace is not strictly required, you can use it like this:
color="violetBlueCrayola"
However, Android Studio will highlight it with two warnings (yes, two). As long as you don’t mind it, you can just don’t use a custom namespace.
Implementing custom LayoutInflater
We will implement a very generic LayoutInflater which will accept a definition of attributes from the outside. Let’s start with the class definition.
Constructor
class CustomLayoutInflater( context: Context, private val parent: LayoutInflater = LayoutInflater.from(context)
): LayoutInflater(parent, context)
Note that by default we use the factory method from LayoutInflater, but we also allow to provide a custom value (for example, mock parent for tests).
Holding appliers
Next, we define a map that will hold appliers: lambdas that will apply the value of an attribute to the view:
private val registeredAppliers: MutableMap<String, (View, String) -> Unit> = mutableMapOf()
Injecting Factory2
Now we need to inject our implementation of Factory2 into inflater like this:
init { factory2 = WrapperFactory(factory2)
}
WrapperFactory
WrapperFactory is defined like this:
inner class WrapperFactory(private val originalFactory: Factory2?) : Factory2 { override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { return originalFactory?.onCreateView(name, context, attrs)?.apply { runAppliers(this, attrs) } } override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? { return originalFactory?.onCreateView(parent, name, context, attrs)?.apply { runAppliers(this, attrs) } }
}
As you can see we simply use the original factory (which is set from the parent) and then run appliers if it returns an actual view.
Overriding onCreateView
Note that factory2 is taking precedence in view creation, however, it’s not going to be able to create the view all the time. For this case we need to override the method in the LayoutInflate itself like this:
override fun onCreateView( name: String?, attrs: AttributeSet? ): View? { for (prefix in androidPrefixes) { try { val view = createView(name, prefix, attrs) if (view != null) return view.apply { runAppliers(this, attrs) } } catch (e: ClassNotFoundException) { } } return super.onCreateView(name, attrs)?.apply { runAppliers(this, attrs) }
}
Where androidPrefixes are:
private val androidPrefixes = listOf( "android.widget.", "android.webkit.", "android.app."
)
As you could’ve guessed, default Factory2 implementation won’t instantiate most usual Android views like LinearLayout, which is weird, but well, what can you do.
Implementing runAppliers
Now let’s implement runAppliers:
private fun runAppliers(view: View, attrs: AttributeSet?) { if (attrs == null) return for (registeredTag in registeredAppliers.keys) { attrs.getAttributeValue(NAMESPACE, registeredTag)?.let { value -> registeredAppliers[registeredTag]?.let { it(view, value) } } }
}
Note that here we verify that attribute has a proper namespace. If you opted into approach without custom namespace - you can pass null instead of the namespace.
Putting it all together
Here’s whole code for CustomLayoutInflater:
import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View class CustomLayoutInflater( context: Context, private val parent: LayoutInflater = LayoutInflater.from(context) ) : LayoutInflater(parent, context) { private val registeredAppliers: MutableMap<String, (View, String) -> Unit> = mutableMapOf() init { factory2 = WrapperFactory(factory2) } fun registerApplier(tag: String, applier: (view: View, value: String) -> Unit): CustomLayoutInflater { registeredAppliers[tag] = applier return this } override fun cloneInContext(newContext: Context): LayoutInflater { return CustomLayoutInflater(newContext, this) } override fun onCreateView( name: String?, attrs: AttributeSet? ): View? { for (prefix in androidPrefixes) { try { val view = createView(name, prefix, attrs) if (view != null) return view.apply { runAppliers(this, attrs) } } catch (e: ClassNotFoundException) { } } return super.onCreateView(name, attrs)?.apply { runAppliers(this, attrs) } } private fun runAppliers(view: View, attrs: AttributeSet?) { if (attrs == null) return for (registeredTag in registeredAppliers.keys) { attrs.getAttributeValue(NAMESPACE, registeredTag)?.let { value -> registeredAppliers[registeredTag]?.let { it(view, value) } } } } inner class WrapperFactory(private val originalFactory: Factory2?) : Factory2 { override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { return originalFactory?.onCreateView(name, context, attrs)?.apply { runAppliers(this, attrs) } } override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? { return originalFactory?.onCreateView(parent, name, context, attrs)?.apply { runAppliers(this, attrs) } } } companion object { private val androidPrefixes = listOf( "android.widget.", "android.webkit.", "android.app." ) private const val NAMESPACE = "https://syllogismobile.wordpress.com/" fun from(context: Context): CustomLayoutInflater { return CustomLayoutInflater(context) } }
}
Using inflater in Activity
This is very simple, so I’ll just show the code since it doesn’t have anything worth explaining:
import android.os.Bundle import android.view.View import android.widget.TextView import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { private val theme = CustomTheme() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView( CustomLayoutInflater .from(this) .registerApplier(COLOR_ATTRIBUTE, this::applyColorAttributeValue) .inflate(R.layout.activity_main, null) ) } private fun applyColorAttributeValue(view: View, colorValue: String) { val color = when(colorValue) { "darkPurple" -> theme.darkPurple "maximumPurple" -> theme.maximumPurple "violetBlueCrayola" -> theme.violetBlueCrayola "lavenderBlue" -> theme.lavenderBlue "aeroBlue" -> theme.aeroBlue else -> error("Unexpected color $colorValue") } when(view) { is TextView -> view.setTextColor(color) else -> view.setBackgroundColor(color) } } private companion object { const val COLOR_ATTRIBUTE = "color" }
}
As you can see we handle only TextView and View here, but you can extend/change it and add new attributes to handle as you wish.
Closing words
I hope this article managed to showcase the power of custom LayoutInflaters. While it is not the approach you would take in every single project, I think it’s worth considering for every project you’re working on!
Good luck and see you in the next articles (hopefully)!