Async view stubs

Or how to use ViewStub with async inflation

Michael Spitsin
4 min readOct 25, 2017

When application page uses complex (in terms of views count)layout, it can became pain for user to see it, since inflation of heavy layouts will take much more time, than just simple one. If you will surf through internet you will see many tips and tricks to avoid it. For me, one of the most effective tricks is to come to PO of project or designer and tell him to make UI much much lighter than now, since in most cases very complex layout points to lack of UX.

But what if we have to use such cumbersome layout? Then there are big amount of articles (including official documentation) that will describe you how to optimize inflation time. So now, let’s imagine that we have not just bad written xml layouts, but just big page with tonnes of graphs, texts and different things. From my point of view, here we can use one of two approaches:

  1. ScrollView — in this case we will face with a problem of big inflation time, since we have really big page (suppose its height will be 5 times bigger than screen height). On the other hand it is really easy to implement, since all views will be there and also scrolling will be really smooth.
  2. RecyclerView (ListView) — we can use recycling mechanism to split our big page on several logical parts. Each of them will be represented by special list item with its own type. All parts will be used in adapter of RecyclerView. It will get a fast inflation for us, but main disadvantage of that approach is lagging on first scrolling (since all parts will be inflated one by one). Also, it will take more time to implement that approach.

So, suppose we want to use first approach, since it will give smooth scrolling for us. So our task is to optimize inflation time of big layout. To do that we need to use ViewStubs as suggested in the docs to split layout inflation on several steps: first of all we will inflate part of layout that will be visible on that screen immediately and then we can inflate remain parts while user will investigate information on screen that he already received.

But there is only one problem that remained: inflation of view stubs happens in the main thread, so if user will scroll screen or will see some animation while inflation of ViewStub will happened, then he will noticed lags, which is not good.

To fix it we can use asynchronous layout inflation.

Async stub

The principle I’ve proposing includes usage of ViewStub and AsyncLayoutInflater. Here is a sample of how you can create an AsyncViewStub:

class AsyncViewStub @JvmOverloads constructor(
context: Context, set: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, set, defStyleAttr) {
private val inflater: AsyncLayoutInflater by lazy { AsyncLayoutInflater(context) }

private var inflatedId
= NO_ID
get
private var layoutRes
= 0

init {
val attrs = intArrayOf(android.R.attr.layout, android.R.attr.inflatedId)
context.obtainStyledAttributes(set, attrs, defStyleAttr, 0).use {
layoutRes
= getResourceId(0, 0)
inflatedId = getResourceId(1, NO_ID)
}

visibility = View.GONE
setWillNotDraw(true)
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (isInEditMode) {
val view = inflate(context, layoutRes, null)
(parent as? ViewGroup)?.let {
it
.addViewSmart(view, it.indexOfChild(this), layoutParams)
it.removeViewInLayout(this)
}
}
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(0, 0)
}

override fun draw(canvas: Canvas) {}

override fun dispatchDraw(canvas: Canvas) {}

fun inflate(listener: AsyncLayoutInflater.OnInflateFinishedListener? = null) {
val viewParent = parent

if (viewParent != null && viewParent is ViewGroup) {
if (layoutRes != 0) {
inflater.inflate(layoutRes, viewParent) { view, resId, parent ->
if
(inflatedId != NO_ID) {
view.id = inflatedId
}

val stub = this
val
index = parent.removeViewInLayoutIndexed(stub)
parent.addViewSmart(view, index, layoutParams)
listener?.onInflateFinished(view, resId, parent)
}
} else {
throw IllegalArgumentException("AsyncViewStub must have a valid layoutResource")
}
} else {
throw IllegalStateException("AsyncViewStub must have a non-null ViewGroup viewParent")
}
}

private fun ViewGroup.removeViewInLayoutIndexed(view: View): Int {
val index = indexOfChild(view)
removeViewInLayout(view)
return index
}

private fun ViewGroup.addViewSmart(child: View, index: Int, params: LayoutParams? = null) {
if (params == null) addView(child, index)
else addView(child, index, params)
}
}

Main point of that class is method inflate which is different from original one from ViewStub :

//Signature of method of original ViewStub class
public View inflate() {
//synchronous inflation of layout
}

And signature of inflate method in AsyncViewStub looks like:

fun inflate(listener: AsyncLayoutInflater.OnInflateFinishedListener? = null) {
//asynchronous inflation of layout
}

Under the hood AsynViewStub just uses AsyncLayoutInflater so properly speaking it will not be always asynchronous. See this article for more information about async layout inflation.

Usage

Because of ViewStub and AsyncViewStub similarity and same approach of handling xml attributes, they have very similar api.

First, you need to define AsyncViewStub in xml and point what layout it will be represent:

<widgets.AsyncViewStub
android:id=
"@+id/avs_stub"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:inflatedId=
"@+id/awesome_view"
android:layout=
"@layout/awesome_layout"/>

Then in code when you will need to show to the user awesome_view or when it will be idle (to not notice changes through small lags, when you will be adding view to the parent), you can call it like:

private inline fun <T : View> prepareStubView(
stub: AsyncViewStub,
crossinline prepareBlock: T.() -> Unit
) {
val inflatedView = getBinding().root.findViewById(stub.inflatedId) as? T
if (inflatedView != null) {
inflatedView.prepareBlock()
} else {
stub.inflate(AsyncLayoutInflater.OnInflateFinishedListener { view, _, _ ->
(view as? T)?.prepareBlock()
})
}
}

Where getBinding returns binding of layout from Data Binding Library. So you can replace it with your own checking of view existence.

Afterwards

There are some cases, when you have to use big layout with numerous data for user, but you can not create RecyclerView for it. So you need to invent some mechanism of delayed view inflation. AsyncViewStub could be one of possible variants of that mechanism. It could be one of the small steps in the path of optimization such big layout.

Thank you.

--

--

Michael Spitsin

Love being creative to solve some problems with an simple and elegant ways