If you are like me, you may occasionally receive design requests to build a more complex screen in an Android app. So, you set about building the layout, making it look just like the design mocks, and everything works great, except for one small problem. Animating to that screen either stutters or hangs until the screen is loaded. You may have gone back to the layout and refactored everything to be in one ConstraintLayout
as Google suggests doing, but it still takes a bit of time to inflate the view because it's so large. Another trick you may have tried if it's a long scrolling view is using a library like Epoxy to only load what is currently visible in the scrolled section. This can work in some cases, but it may cause stuttering or undesired visual artifacts while scrolling due to loading each view on the screen as it appears. It may also just be too much time and effort to refactor a complex view in this way.
Thankfully, Google gave us an option to inflate a view asynchronously. It's called, unsurprisingly, AsyncLayoutInflater and is available both in the v4 support library and is part of AndroidX. Until I discovered this, I thought that all UI (including inflating views) had to be run on the main thread. It turns out that entire layouts can actually be inflated on a background thread with very little change to existing code. There are a few caveats to be aware of before going down this route, but this method works for many different situations.
Caveats
AsyncLayoutInflater
does not support inflating layouts that contain fragment. However, a layout can be inflated within a fragment.- The layout's parent must have a thread-safe implementation of
generateLayoutParams(AttributeSet)
- All the views being constructed as part of inflation must not create any
Handler
s or otherwise callmyLooper()
- When a layout is inflated with
AsyncLayoutInflator
it does not automatically get added to the parent (i.e.attachToRoot
is false)
Some of these caveats may sound like a big deal, but in practice they are not typically an issue.
Converting an existing fragment
The first thing to note if you are converting an existing fragment to inflate asynchronously, is that you can't just swap out LayoutInflator
for AsyncLayoutInflator
. The reason being that onCreateView
, where the layout inflation occurs, expects a view to be returned. The solution to both asynchronously inflating a layout for a fragment and returning a view from onCreateView
is to inflate a small view that the larger view can be attached to. To start with, I created loading_view.xml
with a ConstraintLayout
and a ProgressBar
so there isn't just an empty screen while loading the rest of the content:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
All that is left then is to inflate this view, asynchronously inflate the large layout and attach it to the loading_view
. We should also hide the ProgressBar
once the large layout has been loaded.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
// get view binding
val v = inflater.inflate(R.layout.loading_view, container, false)
val asyncLayoutInflater = context?.let { AsyncLayoutInflater(it) }
asyncLayoutInflater?.inflate(R.layout.large_layout, null) { view, resid, parent ->
(v as? ViewGroup)?.addView(view) // add large view to already inflated view
progressBar.visibility = View.GONE // hide progress bar
}
return v
}
And, there you have it. A way to load a fragment's layout asynchronously.