原文地址:http://blog.joanzapata.com/robust-and-readable-part-2-introducing-async-service/
健壮且可读的安卓架构(二)
注意:
阅读这篇文章之前请阅读第一部分
我收到很多关于我的架构设计的评论和反馈,特别感谢每周安卓社区(Android weekly),你们中的一切人注意到我的架构中的一些弱点,或者一些替代方案,还有的人问我要源码。也有许多人问我在结尾处所说的第二部分什么时候写。
在我写完第一部分的几周后,关于我如何开始开发CandyShopApp是一个模糊描述,我们在Redmill实验室中,从CandyShop转到Midpic。在这过程中,我得到了一个绝佳的机会去将这个架构去转为一个开源库:AsyncService
一、基础用法
这是一个基本的AsyncService的用法,通过用户名称去检索用户
@AsyncService
public class UserService {
public User getUser(String name) {
return ...;
}
}
当你在activity里面注入这个UserService的时候,你可以在任何线程里面回调getUser()这个方法,它会立即返回null并开始异步执行,结果会以一个消息的形式返回,你可以通过构造一个带有合适的参数方法拿到这个结果。
public class MyActivity extends Activity {
@InjectService public UserService userService;
public void onCreate(Bundle savedInstanceState){
// Runs injections and detect @OnMessage callback methods
AsyncService.inject(this);
// Starts method asynchronously (never catch the result here, would be null)
userService.getUser("Joan");
}
// Callback method for getUser
@OnMessage void onUser(User e) {
// Runs on UI thread.
}
}
注意:
AsyncService是在编译的时候生成代码,一个MyActivity类会在编译的时候创建,当你调用inject(this)的时候,系统会通过反射来实例化这个注入器,这点非常重要,因为在android上反射是非常慢的。
正如你所见,这个机制非常的像我在第一文章第一部分中所提到的eventBus,只是这次你不需要注册和注销事件总线,它会全部处理好。在Service里面,你也不用通过事件总线去传递User这个对象,它仅仅是一个返回值,所以代码会更加的简洁。
那么,现在回有人注意到使用事件总线会有一个巨大的缺点:
如果你有多个调用者在同一时间调用了getUser这个方法,onUser这个方法会在意想不到的时候回调很多次。而AsyncService可以解决这个问题,在AsyncService.inject注入的时候,它会绑定好回调者,这意味着你只会通过你自己注入的UserService实例来获得你回调的消息。
当然,如果你需要接受来自整个应用程序任何地方的消息,这是一个非常有用的管理消息通知机制,你可以在回调函数上加上OnMessage(from=ALL)来实现
调用缓存:
当我在写第一篇文章的时候,我所关心的就是不要让用户等待,所以我描述了如何直接展示一些东西给用户,如图所示:
快速提示:服务通过事件总线直接发送缓存数据,然后通过相关API调用并发送更新后的数据。通过使用一个特殊的线程(serial=CACHE)去处理新的请求,我确定缓存会直接发送,然后在通过一个特殊的线程(serial=NETWORK)去处理网络请求,这样我更容易处理get-after-post上的麻烦。
通过使用AndroidAnnomations@serial的注释、 EventBus和snappyDB,代码看起来是这样的
@Background(serial = CACHE)
public void getUser() {
postIfPresent(KEY_USER, UserFetchedEvent.class);
getUserAsync();
}
@Background(serial = NETWORK)
private void getUserAsync() {
cacheThenPost(KEY_USER, new UserFetchedEvent(candyshopApi.fetchUser()));
}
这样,依旧需要很多个引用代码去写每个请求,AsyncService有这样一个注释:
@CacheThenCall
public User getUser(){
return ...;
}
这样很简洁,对吗?它依旧以相同的方式在后台工作。
如果getUser()方法带参数,比如getUser(String name),你可以使用一个特殊的键值来标记这个缓存,但是这个键值必须在整个应用程序中唯一。
一般来说,缓存键值的默认值形式为"<Class.name>.<MethodName>({arg1},{arg2},...)",所以在这个地方我们实际上并不需要去标记它。
@CacheThenCall(key="UserService.getUser({name})")
public User getUser(String name){
return ...;
}
一般来说,缓存键值的默认值形式为"<Class.name>.<MethodName>({arg1},{arg2},...)",所以在这个地方我们实际上并不需要去标记它。
错误管理
在上一篇文章中,我没有写到错误管理,在AsyncService中有这样的错误管理机制:
@AsyncService(errorMapper = StatusCodeMapper.class)
@ErrorManagement({
@Mapping(on = 0, send = NoNetworkError.class),
@Mapping(on = 500, send = ServerInternalError.class),
@Mapping(on = 503, send = ServerMaintenanceError.class),
...})
public class UserService {
@ErrorManagement({
@Mapping(on = 404, send = UserNotFoundError.class),
...})
public User getUser(String username){
return ...;
}
}
如果一个错误在getUser()中发生了,那么ErrorManager将会把这个错误转换成一个int数据,然后去和@Mappingannotations里的值匹配,如果匹配成功,那么匹配成功的类会被实例化并当做消息发送。
一个基础的ErrorMapper接口可以是这样:
public class StatusCodeMapper implements ErrorMapper {
@Override
public int mapError(Throwable throwable) {
if (throwable instanceof HttpStatusCodeException)
return ((HttpStatusCodeException) throwable).getStatusCode().value();
if (isConnectivityError(throwable))
return 0;
return SKIP;
}
}
如果返回的是SKIP,那么意味着这个错误没有捕获,那么它会发送到UncaughtExceptionHandler(这个类是全局异常捕获类,之前写的文章里有说明),如果没听过这个类,这个类就是所有你未捕获的错误所去的地方。一些错误报告工具例如:ACRA\Crashlytic等,捕获错误并报告。
也许第一次写这些很反感,但你只需要这么写一次就好。之后就可以定义什么错误发生在每个方法中。在Midpic这个项目中,我的ErrorManager有一点点大,因为我们的服务端响应头里面有这些无意义代码{code:"1002",message:"blah blah blah"},所以我通过匹配1002条错误代码去读取错误。这样使得我的代码完全镜像服务器API。
关于ErrorManager的最后一点,如果在getUser(String name)上注释了错误标签,404映射到了UserNotFoundError,那么在activity中你可以这样捕获这个错误:
@OnMessage
void onError(UserNotFoundError e){
Toast.makeText(this, "User does not exist.", LENGTH_LONG).show();
}
当错误发生在getUser(String name)方法时,你可以更进一步的捕获到用户名。这样,你可以定义一个参数在错误信息中:
public class UserNotFoundError {
public UserNotFound(@ThrowerParam("username") String username){
this.username = username;
}
...
那么现在你可以更加准确的展示信息:
@OnMessage
void onError(UserNotFoundError e){
Toast.makeText(this, String.format("User %s does not exist.", e.getUsername()), LENGTH_LONG).show();
}
结论:
在这篇文章中,我介绍了一些AsyncService的优点,例如缓存和错误管理机制,如果想得到更完整的功能列表,请阅读
github项目。正如我们所看到的,这是一个对AndroidAnnotations,eventBus,snappyDB组合的提升,这个框架已经用在Midpic项目上,目前为止还没有发现任何bug。
如今AsyncService已经实现了它的价值在Midpic项目上,但是我希望它可以帮助到更多的人并且我已经将它发布到社区上,我很乐意接受相关意见和反馈!
终于翻译完了。。