Dynamically inflates UI in Android App

There is a fascinating idea that inflates UI according to an android layout xml downloaded from server. According to the Android API, it looks quite feasible.

One of LayoutInflate.inflate method overloads accept Layout Xml as XmlPullParser.

And XmlPullParser can wrap around an input stream, so as consequence, following code seems to be working:

Inflate view on the fly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DynamicView extends FrameLayout {
public DynamicView(Context context, InputStream layoutData) throws XmlPullParserException {
super(context);
createView(context, layoutData);
}
private void createView(Context context, InputStream layoutData) throws XmlPullParserException {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser parser = factory.newPullParser();
parser.setInput(layoutData, "UTF-8");
inflater.inflate(parser, this, true);
}
}

The code looks great, compiling looks fine, but when the code excuted, an exception is thrown by the inflater.

According to the LayoutInflater document, this approach won’t work(at least for now, it won’t).

For performance reasons, view inflation relies heavily on pre-processing of XML files that is done at build time. Therefore, it is not currently possible to use LayoutInflater with an XmlPullParser over a plain XML file at runtime.

Actually, Android compiler compiles the layout xml files into binary xml block, which has convert attributes into some special format. And in Android SDK, the LayoutInflater uses XmlResourceParser instead of plain XmlPullParser, which is created by XmlBlock.

XmlBlock is an internal class used in Resources to cache the binary xml document.

And there is no way to create XmlResourceParser or other classes to inject custom behavior into this process. Personally assume that it is pretty much related to the Android Resource and Theming mechanism, there are quite a number cross references between resources. To make it works in a efficient way, Android Runtime did a lot work, such as cache and pre-processing. To override this behavior require quite a number of work and need to aware of potential performance issue. Since the inflating could happen quite often during navigation.

As a not that fansy alternative, UI based on HTML hosted in WebView could be considered.

Handle dynamic argument in dynamic language

In dynamic language, most language doesn’t provide the function overload mechanism like static language does, which means you cannot define functions with same name but different arguments, and the language itself won’t help you to dispatch the call according to the arguments.
So you have to deal with the overload by yourself in dynamic languages.

In dynamic language, since you have to dispatch the call by yourself, so you can play some tricks during process the arguments. The most common trick is grammar sugar, which can help you simplify the code and increase the readability. And it is a very important feature when you’re building DSL.

Syntax sugar in argument means you can omit something unnecessary or unimportant parameters in specific context, then the function will try to infer and complete the parameters. For example, developer should be able to omit the password parameter if the system enable authentication mechanism. These kind of tricks are very common in dynamic language frameworks, such as jQuery.

Here is a list of some common grammar sugar cases:

  • Set default value for omit parameter, such as for jQuery animation function jQuery.fadeIn([duration] [, callback] ), you can omit the parameter duration, which is equivalent to provide duration as 400
  • Provide different type of value for a same parameter, still the jQuery animation function jQuery.fadeIn([duration] [, callback] ), you can provide number for duration, or you can provide string “fast” and “slow” as the value of duration.
  • Provide a single value rather than a complex hash, such as function jQuery ajax function jQuery.ajax(settings), you can provide a url string, which is equivalent to provide a hash{url: <url string>}
  • Pass a single element instead of a array of the element.

After we analyze the cases, we will find that all the grammar sugar is to allow user to provide exactly same information but in different formats. Since all the data are same piece of information, so it should be possible to unify all the information into one format! Which means to support grammar sugar, the major problem is to unify the type of parameter.
Besides unify the type, another important problem is to handle the null values, such asnull and undefined in javascript, nil in ruby, etc.

Here is a piece of ruby code that I parse the argument by unifying the parameter type:

code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def apply_to_items(options = nil)
options = unify_type(options, Hash) { |items| {:only => items} }
options[:only] = unify_type(options[:only], Array) { |item| item.nil? ? list_all_items : [item] }
options[:except] = unify_type(options[:except], Array) { |item| item.nil? ? [] : [item] }
options[:only] = unify_array_item_type(options[:only], String) { |symbol| symbol.to_s }
options[:except] = unify_array_item_type(options[:except], String) { |symbol| symbol.to_s }
target_items = options[:only].select { |item| options[:except].exclude? item }
target_items.each do |item|
yield item
end
end
private
def list_all_items
# return all items fetched from database or API
# ...
end
def unify_type(input, type)
if input.is_a?(type)
input
else
yield input
end
end
end

And here is the test for the code:

title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
require 'spec_helper'
describe Module do
before do
extend Moudle
end
it "should populate all items" do
visited = []
apply_to_items do |item|
visited << item
end
visited.should =~ %w(public another_item)
end
describe "should populate the provided items" do
it "provide as string array" do
visited = []
apply_to_items(%w(another_item)) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as symbol array" do
visited = []
apply_to_items([:another_item]) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as string item" do
visited = []
apply_to_items('another_item') do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as symbol item" do
visited = []
apply_to_items(:another_item) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as string array in hash" do
visited = []
apply_to_items(:only => %w(another_item)) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as symbol array in hash" do
visited = []
apply_to_items(:only => [:another_item]) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "provide as string item in hash" do
visited = []
apply_to_items(:only => 'public') do |item|
visited << item
end
visited.should =~ %w(public)
end
it "provide as symbol item in hash" do
visited = []
apply_to_items(:only => :public) do |item|
visited << item
end
visited.should =~ %w(public)
end
end
describe "should except the not used items" do
it "except as string item in hash" do
visited = []
apply_to_items(:except => 'public') do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "except as symbol item in hash" do
visited = []
apply_to_items(:except => :public) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "except as string array in hash" do
visited = []
apply_to_items(:except => %w(public)) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
it "except as symbol array in hash" do
visited = []
apply_to_items(:except => :public) do |item|
visited << item
end
visited.should =~ %w(another_item)
end
end
end

The algorithm in previous code is language independent, so ideally, it could be reused in any language, such as java script or python.