It’s common practice in the Android SDK, but you may be surprised to find that there is a wrong way to use LayoutInflater, and your application might be one of the offenders. If you’ve ever written something like the following code usingLayoutInflater in your Android application:
1
|
;
html
-
script
:
false
]
inflater
.
inflate
(
R
.
layout
.
my_layout
,
null
)
;
|
PLEASE read on, because you’re doing it wrong and I want to explain to you why.
Get to Know LayoutInflater
Let’s first take a look at how LayoutInflater works. There are two usable versions of the inflate() method for a standard application:
1
|
;
html
-
script
:
false
]
inflate
(
int
resource
,
ViewGroup
root
)
|
1
|
;
html
-
script
:
false
]
inflate
(
int
resource
,
ViewGroup
root
,
boolean
attachToRoot
)
|
The first parameter points to the layout resource you want to inflate. The second parameter is the root view of the hierarchy you are inflating the resource to attach to. When the third parameter is present, it governs whether or not the inflated view is attached to the supplied root after inflation.
It is these last two parameters that can cause a bit of confusion. With the two parameter version of this method,LayoutInflater will automatically attempt to attach the inflated view to the supplied root. However, the framework has a check in place that if you pass null for the root it bypasses this attempt to avoid an application crash.
Many developers take this behavior to mean that the proper way to disable attachment on inflation is by passing null as root; in many cases not even realizing that the three parameter version of inflate() exists. By doing things this way, we also disable another very important function the root view has…but I’m getting ahead of myself.
Examples from the Framework
Let’s examine some situations in Android where the framework expects you as a developer to interactively inflate portions of the view.
Adapters are the most common case for using LayoutInflater is custom ListView adapters overriding getView(), which has the following method signature:
1
|
;
html
-
script
:
false
]
getView
(
int
position
,
View
convertView
,
ViewGroup
parent
)
|
Fragments also use inflation often when creating views via onCreateView(); notice its method signature:
1
|
;
html
-
script
:
false
]
onCreateView
(
LayoutInflater
inflater
,
ViewGroup
container
,
Bundle
savedInstanceState
)
|
Have you noticed that every time the framework wants you to inflate a layout, they also pass you the parent ViewGroup it will eventually be attached to? Notice also that in most cases (including the above two examples), it will throw an Exception later on if LayoutInflater is allowed to automatically attach the inflated view to the root.
So why do you suppose we are given this ViewGroup if we are not supposed to attach to it? It turns out the parent view is a very important part of the inflation process because it is necessary in order to evaluate the LayoutParams declared in the root element of the XML being inflated. Passing nothing here is akin to telling the framework “I don’t know what parent this view will be attached to, sorry.”
The problem with this is android:layout_xxx attributes are always be evaluated in the context of the parent view. As a result, without any known parent, all LayoutParams you declared on the root element of your XML tree will just get thrown away, and then you’ll be left asking “why is the framework ignoring the layout customizations I defined? I’d better check SO and then file a bug.”
Without LayoutParams, the ViewGroup that eventually hosts the inflated layout is left to generate a default set for you. If you are lucky (and in many cases you are) these default parameters are the same as what you had in XML…masking the fact that something is amiss.
Application Example
So you claim you’ve never seen this happen in an application? Take the following simple layout that we want to inflate for aListView row:
R.layout.item_row
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
;
html
-
script
:
false
]
<
LinearLayout
xmlns
:
android
=
"http://schemas.android.com/apk/res/android"
android
:
layout_width
=
"match_parent"
android
:
layout_height
=
"?android:attr/listPreferredItemHeight"
android
:
gravity
=
"center_vertical"
android
:
orientation
=
"horizontal"
>
<
TextView
android
:
id
=
"@+id/text1"
android
:
layout_width
=
"wrap_content"
android
:
layout_height
=
"wrap_content"
android
:
paddingRight
=
"15dp"
android
:
text
=
"Text1"
/
>
<
TextView
android
:
id
=
"@+id/text2"
android
:
layout_width
=
"0dp"
android
:
layout_height
=
"wrap_content"
android
:
layout_weight
=
"1"
android
:
text
=
"Text2"
/
>
<
/
LinearLayout
>
|
We want to set the height of our row to be a fixed height, in this case the preferred item height for the current theme…seems reasonable.
However, when we inflate this layout the wrong way
1
2
3
4
5
6
7
|
;
html
-
script
:
false
]
public
View
getView
(
int
position
,
View
convertView
,
ViewGroup
parent
)
{
if
(
convertView
==
null
)
{
convertView
=
inflate
(
R
.
layout
.
item_row
,
null
)
;
}
return
convertView
;
}
|
we end up with a result that looks like this
What happened to the fixed height we set?? This is usually where you end up setting the fixed height on all your child views, switching the root elements height to wrap_content, and move on without really knowing why it broke (you may have even cursed at Google in the process).
If we instead inflate the same layout this way
1
2
3
4
5
6
7
|
;
html
-
script
:
false
]
public
View
getView
(
int
position
,
View
convertView
,
ViewGroup
parent
)
{
if
(
convertView
==
null
)
{
convertView
=
inflate
(
R
.
layout
.
item_row
,
parent
,
false
)
;
}
return
convertView
;
}
|
we end up with what we expected in the first place.
Hooray!
Every Rule Has An Exception
There are of course instances where you can truly justify a null parent during inflation, but they are few. One such instance occurs when you are inflating a custom layout to be attached to an AlertDialog. Consider the following example where we want to use our same XML layout but set it as the dialog view:
1
2
3
4
5
6
7
|
;
html
-
script
:
false
]
AlertDialog
.
Builder
builder
=
new
AlertDialog
.
Builder
(
context
)
;
View
content
=
LayoutInflater
.
from
(
context
)
.
inflate
(
R
.
layout
.
item_row
,
null
)
;
builder
.
setTitle
(
"My Dialog"
)
;
builder
.
setView
(
content
)
;
builder
.
setPositiveButton
(
"OK"
,
null
)
;
builder
.
show
(
)
;
|
The issue here is that AlertDialog.Builder supports a custom view, but does not provide an implementation of setView() that takes a layout resource; so you must inflate the XML manually. However, because the result will go into the dialog, which does not expose its root view (in fact, it doesn’t exist yet), we do not have access to the eventual parent of the layout, so we cannot use it for inflation. It turns out, this is irrelevant, because AlertDialog will erase any LayoutParams on the layout anyway and replace them with match_parent.
I've investigated this issue, referring to the LayoutInflater docs and setting up a small sample demonstration project. The following tutorials shows how to dynamically populate a layout using LayoutInflater
.
Before we get started see what LayoutInflater.inflate()
parameters look like:
- resource: ID for an XML layout resource to load (e.g.,
R.layout.main_page
) - root: Optional view to be the parent of the generated hierarchy (if
attachToRoot
istrue
), or else simply an object that provides a set ofLayoutParams
values for root of the returned hierarchy (ifattachToRoot
isfalse
.) -
attachToRoot: Whether the inflated hierarchy should be attached to the root parameter? If false, root is only used to create the correct subclass of
LayoutParams
for the root view in the XML. -
Returns: The root View of the inflated hierarchy. If root was supplied and
attachToRoot
istrue
, this is root; otherwise it is the root of the inflated XML file.
Now for the sample layout and code.
Main layout (main.xml
):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
Added into this container is a separate TextView, visible as small red square if layout parameters are successfully applied from XML (red.xml
):
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="25dp"
android:layout_height="25dp"
android:background="#ff0000"
android:text="red" />
Now LayoutInflater
is used with several variations of call parameters
public class InflaterTest extends Activity {
private View view;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
ViewGroup parent = (ViewGroup) findViewById(R.id.container);
// result: layout_height=wrap_content layout_width=match_parent
view = LayoutInflater.from(this).inflate(R.layout.red, null);
parent.addView(view);
// result: layout_height=100 layout_width=100
view = LayoutInflater.from(this).inflate(R.layout.red, null);
parent.addView(view, 100, 100);
// result: layout_height=25dp layout_width=25dp
// view=textView due to attachRoot=false
view = LayoutInflater.from(this).inflate(R.layout.red, parent, false);
parent.addView(view);
// result: layout_height=25dp layout_width=25dp
// parent.addView not necessary as this is already done by attachRoot=true
// view=root due to parent supplied as hierarchy root and attachRoot=true
view = LayoutInflater.from(this).inflate(R.layout.red, parent, true);
}
}
The actual results of the parameter variations are documented in the code.
SYNOPSIS: Calling LayoutInflater
without specifying root leads to inflate call ignoring the layout parameters from the XML. Calling inflate with root not equal null
and attachRoot=true
does load the layout parameters, but returns the root object again, which prevents further layout changes to the loaded object (unless you can find it using findViewById()
). The calling convention you most likely would like to use is therefore this one:
loadedView = LayoutInflater.from(context)
.inflate(R.layout.layout_to_load, parent, false);