Otto and Android Annotations Compatibility Issue Analysis

Introduction

Otto is a great Android event bus solution developed by SquareUp guys. These guys extract the event bus related classes from Google’s Guava, and optimized it for Android. Event Bus is a great solution to keep your code away from messy anonymous or internal classes only used for event handling! And Otto’s simplicity and performance makes it used to be the most popular event bus solution in Android development.

Android Annotations, as known as AA, is another great library that makes Android developers’ lives a lot easier. Just annotating the code, AA helps developer handles the most boring or error-proning tasks for you, including binding widgets binding, asynchronous tasks, life time management, etc…

Issue

Otto and AA are the libraries focusing on different aspects. Theoretically speaking, there shouldn’t be any compatibility issue between them. But the reality Otto event is never delivered to AA annotated classes. By inspecting the code with step by step debugging, I found the root cause of the issue is Otto failed to located @Produce and @Subscribe annotated methods in AA generated code.

Analysis

After a few study work, I finally understood what is the reason behind:

For performance consideration, Android Annotations actually does it work during the compilation time instead of Runtime. AA will derive the annotated classes, and generate some code according to the annotations applied. During the runtime, the classes got instantiated is actually the derived classes instead of the original one, although in the code is accessed via the original class as interface.

Here is a simple example:

I have the class ServerListAdapter, which is used to provide data for a grid view.

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
@EBean
public class ServerListAdapter extends AdvBaseAdapter<InetAddress, ServerView> {
@RootContext
protected Context context;
@SystemService
protected LayoutInflater inflater;
@Bean
protected Bus bus;
public ServerListAdapter() {
super(new ArrayList<InetAddress>());
}
@AfterInject
protected void afterInject() {
bus.register(this);
}
@Override
protected ServerView createView(ViewGroup parent) {
return ServerView_.build(context);
}
@Override
protected ServerView updateView(ServerView itemView, InetAddress item) {
itemView.update(item);
return itemView;
}
@Subscribe
public void fetchServerStatus(DiscoveryStatusEvent event) {
setItems(event.addresses);
}
@Subscribe
public void onServerStatusUpdated(DiscoveryStatusChangedEvent event) {
switch (event.type) {
case SERVER_ONLINE:
getItems().add(event.address);
break;
case SERVER_OFFLINE:
getItems().remove(event.address);
break;
}
notifyDataSetChanged();
}
}

And this is the derived class generated by AA during the compiling-time:

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
public final class ServerListAdapter_
extends ServerListAdapter
{
private Context context_;
private ServerListAdapter_(Context context) {
context_ = context;
init_();
}
public static ServerListAdapter_ getInstance_(Context context) {
return new ServerListAdapter_(context);
}
private void init_() {
inflater = ((LayoutInflater) context_.getSystemService(Context.LAYOUT_INFLATER_SERVICE));
context = context_;
bus = Bus_.getInstance_(context_);
afterInject();
}
public void rebind(Context context) {
context_ = context;
init_();
}
}

And here is how it is consumed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@EFragment(R.layout.fragment_server_list)
public class ServerListFragment extends Fragment {
@Bean
protected DiscoveryService discoveryService;
@Bean
protected ServerListAdapter adapter;
@ViewById(R.id.server_list)
protected GridView serverGridView;
@AfterViews
protected void afterViews() {
serverGridView.setAdapter(adapter);
discoveryService.start();
}
@Override
public void onDetach() {
super.onDetach();
discoveryService.stop();
}
}

As you can see, in the ServerListFragment, the ServerListAdapter instance injected into bean is actually the instance of ServerListAdapter_. And due to polymorphic, the instance just behaves like a ServerListAdapter instance.

On the other hand, according to the description on Otto’s Home Page:

In order to receive events, a class instance needs to register with the bus.

Registering will only find methods on the immediate class type. Unlike the Guava event bus, Otto will not traverse the class hierarchy and add methods from base classes or interfaces that are annotated. This is an explicit design decision to improve performance of the library as well as keep your code simple and unambiguous.

Otto only search annotations in direct class, which is ServerListAdapter_ in instance, and there isn’t any annotation included. As a consequence, all the @Subscribe annotated methods are ignored by com.squareup.otto.AnnotatedHandlerFinder. So the posted events become dead event due to no subscriber found.

Comments

There is rumor that this issue will be fixed in Otto 2.0. But according to the comment from Jake Wharton, Otto’s developer, Otto 2.0 will take forever to release.

In fact Otto 2.0 Repo has been not touched for 2 years already. Although we could check out the code and build Otto 2.0 by ourselves, but it takes efforts. Especially when some bug is found.

Conclusion

Luckily, although Otto turns into “maintenance mode”, the compatibility issue takes forever to resolve. I found a great Otto alternative, EventBus from GreenRobot. A boring name, but great library.

According to EventBus‘s document, it provides richer feature and better performance than Otto.
The most important EventBus is friendly to AndroidAnnoations. They two works well together.

EventBus and Otto Feature Comparison

EventBus Otto
Declare event handling methods Name conventions Annotations
Event inheritance Yes Yes
Subscriber inheritance Yes No
Cache most recent events Yes, sticky events No
Event producers (e.g. for coding cached events) No Yes
Event delivery in posting thread Yes (Default) Yes
Event delivery in main thread Yes No
Event delivery in background thread Yes No
Asynchronous event delivery Yes No

EventBus and Otto Performance Comparison

EventBus over Otto
Posting 1000 events, Android 2.3 emulator ~70% faster
Posting 1000 events, S3 Android 4.0 ~110% faster
Register 1000 subscribers, Android 2.3 emulator ~10% faster
Register 1000 subscribers, S3 Android 4.0 ~70% faster
Register subscribers cold start, Android 2.3 emulator ~350% faster
Register subscribers cold start, S3 Android 4.0 About the same

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.