You never get a second chance to make a good first impression. This old adage is particularly true for apps. A bad first impression can often drive users to abandon an app before they have properly tried it. Slow app startup can certainly contribute to a poor first experience. But the Jetpack Startup library can certainly help improve startup times. However, there are other tools to help us improve our startup times. In this article, we’ll look at one of them: AsyncLayoutInflater
.
The specific area that we’ll consider is layout inflation within our Activity. This can be a relatively slow process because it involves binary XML parsing and View object creation. The more complex our layout, then more work this requires.
This can be further slowed down if we are using the libraries such as Jetpack Navigation. When the NavHostFragment
is inflated, this will require the navigation graph to then be inflated.
Similarly any embedded Fragment
instances in the layout will also require inflation of their own layouts.
While none of these individually may take too much time, the inflation of the entire Activity
layout hierarchy may take a little time. To give some basic idea: A layout consisting of a ConstraintLayout
with a single TextView
child takes around 100ms to inflate on a Pixel 3a XL. Obviously, a more complex layout may take considerably longer.
The knock-on effect
The user will barely notice a few hundred milliseconds. But we must remember that this is not happening in isolation. In many apps, we might trigger a network request from the onCreate()
method in the Activity
. If we wait until after layout inflation has completed these operations will be happening serially. This means that the user may be waiting for longer than necessary before they see any content.
The obvious way of addressing this is to trigger the network request before we perform layout inflation. If we do not perform the network request asynchronously, then we’re doing it wrong. However, there is another option – perform the layout inflation asynchronously as well as other potentially time-consuming operations.
AsyncLayoutInflator
The Jetpack library named asynclayoutinflator does precisely that. It’s not a new library nor one which has many updates – it has been at release 1.0.0 since September 2018. The API itself is fairly straightforward. However, it may not be immediately clear how to use it with either Data Binding or View Binding. So we’ll actually look at how to implement it along with View Binding.
Let’s start by adding the library dependency:
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' android { . . . } dependencies { . . . implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" . . . } . . .
Our simple Activity looks like this:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val content = findViewById(android.R.id.content) AsyncLayoutInflater(this) .inflate(R.layout.activity_main, content) { view, _, _ -> binding = ActivityMainBinding.bind(view) setContentView(view) } } }
We first need to find the content view. This is the parent view which will hold the layout that we’ll inflate. Next, we create an instance of AsyncLayoutInflater
which takes a Context
as it’s constructor argument. We call the inflate()
method on this to initialise the inflation.
The inflate()
method takes three arguments: The first is the ID of the layout that will be inflated. The second is the parent view – in this case, it is the content view that we just obtained. The third is an instance of OnInflateFinishedListener
which is a SAM interface, so we can reduce its single method to a lambda. The lambda will be invoked once inflation is complete.
The lambda represents the onInflateFinished
method of OnInflateFinishedListener
. There are three arguments to this method: The first is the root view of the newly inflated view hierarchy. The second is the layout ID that was inflated. The third is the parent view – the content view that we passed to inflate()
.
We first bind our ActivityMainBinding
to the new hierarchy using its static bind()
method. We can now use the binding
instance as we would normally use a View binding instance. Next we call setContentView()
on the Activity
to add the newly inflated view hierarchy as the Activity
content. AsyncLayoutInflater
does not add the inflated hierarch to the parent ViewGroup
so we need to do this manually. It is equivalent to calling:
layoutInflater.inflate(R.layout.activity_main, content, false)
Caution
It is worth remembering that performing different operations in parallel can result in race conditions. Therefore we must be careful to check that the binding
instance has been initialised before we attempt to use it. For example, it may not have been initialised by the time onResume()
is called. So we should check ::binding.isInitialized
before using it in such cases.
Fragments
The next logical consideration is whether we should use this within Fragment
s for their layout inflation. Unfortunately AsyncLayoutInflator
is not well suited to this because Fragment
s have layout inflation tied into their lifecycle methods. Whereas in Activity
we typically perform layout inflation during onCreate()
, in a Fragment
we do it in onCreateView()
. This method must return the inflated view hierarchy and using AsyncLayoutInflater
within it does not allow this.
It is worth bearing in mind that most Activity
layouts will be far more complex than those of Fragment
s. The latter are a smaller logical UI controller. So this may not be such a big deal. Typically it is Activities
that will have NavHostFragment
instances within their layouts, and any other embedded Fragment
instances within the layout will also be inflated within the Activity
‘s AsyncLayoutInflater
invocation.
Conclusion
Using AsyncLayoutInflater
isn’t something that we need to use in all cases. However, when we are trying to reduce the initial wait time before we can display something to the user it can permit layout inflation to occur in parallel with other operations. But, we must be careful to avoid race conditions that may result from binding
not being initialised before onCreate()
returns.
The source code for this article is available here.
© 2020 – 2021, Mark Allison. All rights reserved.
Copyright © 2020 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.