鲁棒性、可读的android架构(II)

Note: This blog post assume you already read the first part.

I received a lot of comments and feedbacks about it, mostly thanks to theAndroid Weekly community, so thank you all. Some of you noticed some weak spots in the architecture I described, or alternative solutions, others asked for working code, and a lot of people asked for a second part on the topics I evoked at the end.

A few weeks after my first article, which was a vague description of how I started the developement of the Candyhop app, at Redmill Lab wepivoted from Candyshop to Midpic. It gave me the perfect opportunity to turn this architecture into a library: AsyncService

Basics

Here is a basic AsyncService with a single method which retrieves a user from his name.

@AsyncService
public class UserService {
   public User getUser(String name) {
      return ...;
   }
}

When you inject this UserService into your activity, you can call getUser() from any thread, it will immediately return null while the execution starts asynchronously. The result is then returned as a message that you can catch using a 1-arg method with appropriate type:

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.
   }
}

Note: AsyncService is based on compile-time code generation. A MyActivityInjector class is created at compile-time, the only bit of reflection is done to instantiate this injector when you call inject(this). That‘s an important point on Android because reflection is slow.

As you can see, it looks a lot like what I did in the first part of the article, with the event bus. This time you don‘t have to register and unregister from the event bus, it‘s all handled. On the service part you don‘t have to send the User through the event bus either, only to return it. So the code is already a little bit more concise.

Now, some people agreed that this usage of an event bus had a huge drawback: if you have multiple actors calling getUser at the same time, onUser will get called multiple times, at unexpected moments. AsyncService solve the problem: AsyncService.inject binds the injected service with the callbacks. That means you only receive the messages you asked for on your own instance of UserService.

If, however, you need to receive messages emitted from anywhere in the app, which is very useful to manage notifications for example, you can use @OnMessage(from=ALL) on your callback.

Cache then call

My main concern when I wrote my first article was not to make the user wait. So I explained how to immediately display something to the user. The concept was illustrated like this: 

Quick reminder: the service immediately sends the cached value through the event bus, then it makes the API call and sends the updated result. By using a specific thread (serial=CACHE) to deal with new requests, I make sure the cached result is sent immediately. And by using a specific thread (serial=NETWORK) for network requests, I deal more easily with get-after-post troubles.

Using AndroidAnnotations‘ @Serial annotation, EventBus and SnappyDB, the code looked like this:

@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()));
}

That‘s still quite a lot of boilerplate code to write for each request. AsyncService has an annotation dedicated for this:

@CacheThenCall
public User getUser(){
   return ...;
}

Smarter, right? And it still works the same way behind the scenes.

If getUser() has arguments, for example getUser(String name), you can use it to specify the key you want to use for caching. This key must be unique at the application level:

@CacheThenCall(key="UserService.getUser({name})")
public User getUser(String name){
   return ...;
}

The default value of the cache key is "<ClassName>.<MethodName>({arg1}, {arg2},...)", so here we don‘t actually need to specify it.

Error management

One topic I didn‘t talk about in the last post is error management. AsyncService has an error handling mechanism:

@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 ...;
   }

}

As you can see the @AsyncService defines an ErrorMapper. It‘s an interface that transforms a Throwable to an int, for example extracting the HTTP error status code of the exception. We‘ll see this in a minute.

If an exception occurs in getUser(), the ErrorMapper will be used to translate it to an int, then if a match is found in one of the @Mappingannotations, the given class is instantiated and sent as a message.

One basic implementation of the ErrorMapper can be:

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 means that the exception cannot be handled, and will be sent to the UncaughtExceptionHandler. For those who never heard of it, it‘s where all your uncaught exception go. Crash report tools like ACRACrashlytics, etc... replace it to catch and report them.

It‘s quite annoying to write at first, but you only need to do this once. After that, just declare what error can occur on each method. In Midpic, my ErrorMapper is a little bigger, because our server responses contain things like { code: ‘1002‘, message: ‘blah blah blah‘ }, so I read it to map the exception with the 1002 code, this way my code perfectly mirrors the server API.

One last thing about error management. On the getUser(String username) above, 404 is mapped to UserNotFoundError. So, on the Activity side you can catch it this way:

@OnMessage
void onError(UserNotFoundError e){
    Toast.makeText(this, "User does not exist.", LENGTH_LONG).show();
}

But you can actually go further by capturing the username that was used to call getUser(String username) where the exception occured. For this, you can define a constructor param in the error message:

public class UserNotFoundError {
   public UserNotFound(@ThrowerParam("username") String username){
      this.username = username;
   }
   ...

Now your toast can be much more explicit:

@OnMessage
void onError(UserNotFoundError e){
    Toast.makeText(this, String.format("User %s does not exist.", e.getUsername()), LENGTH_LONG).show();
}

Conclusion

In this article I introduced some features of AsyncService, like caching and error management. For the full list of features, please read the wiki. As we‘ve seen, it‘s an improvement in every way compared to the manual combination of AndroidAnnotations, EventBus and SnappyDB. It‘s currently used in production in the Midpic app and it raised no related bug so far.

AsyncService already served its purpose on Midpic, but I hope it‘ll help someone else as I‘m now releasing it to the community. I‘ll be glad to receive comments and feedbacks!

时间: 2024-08-08 04:39:49

鲁棒性、可读的android架构(II)的相关文章

鲁棒性、可读的android架构(I)

Since the early days of Android, I've been looking for a robust way to build Android apps, keep the IO operations out of the UI Thread, avoid duplicated network calls, cache relevant things, update the cIache at the right time, etc... with the cleane

android架构

android基本架构 Android其本质就是在标准的Linux系统上增加了Java虚拟机Dalvik,并在Dalvik虚拟机上搭建了一个JAVA的application framework,所有的应用程序都是基于JAVA的application framework之上. Android主要应用于ARM平台,但不仅限于ARM,通过编译控制,在X86.MAC等体系结构的机器上同样可以运行. android分为四个层,从高层到低层分别是应用程序层.应用程序框架层.系统运行库层和linux核心层.

Android架构设计和软硬整合完整训练:HAL&amp;Framework&amp;Native Service&amp;Android Service&amp;Best Practice

如何理解Android架构设计的初心并开发出搭载Android系统并且具备深度定制和软硬整合能力特色产品,是本课程解决的问题. 课程以Android的五大核心:HAL.Binder.Native Service.Android Service(并以AMS和WMS为例).View System为主轴,一次性彻底掌握Android的精髓. 之所以是开发Android产品的必修课,缘起于: 1, HAL是Android Framework&Application与底层硬件整合的关键技术和必修技术: 2

Android架构设计和软硬整合:HAL&amp;Framework&amp;Native Service&amp;Android Service&amp;Best Practice

如何理解Android架构设计的初心并开发出搭载Android系统并且具备深度定制和软硬整合能力特色产品,是本课程解决的问题. 课程以Android的五大核心:HAL.Binder.Native Service.Android Service(并以AMS和WMS为例).View System为主轴,一次性彻底掌握Android的精髓. 之所以是开发Android产品的必修课,缘起于: 1, HAL是Android Framework&Application与底层硬件整合的关键技术和必修技术: 2

Android架构师之路-架构师的决策

android架构师之路-架构师的决策 内涵+造型:可能大部分人对这个内涵和造型不是很理解,在这里我可以给大家举个生动的例子:相信很多人都有自己的汽车, 我们总结汽车有哪些属性和功能,这些都是内涵,大自然中的每个对象都有自己的内涵(人有手有脚,还可以跑),然后我们 将这些内涵放入指定的造型中,类似模版,比如java语言如果定义一个class的时候,必须在作用域(大括号内部)指定属性和 函数,这个class的定义规范就是一个造型,然后我们将汽车这个内涵按照class的规范定义一个汽车class,那

Android架构分析之Android智能指针(二)

作者:刘昊昱 博客:http://blog.csdn.net/liuhaoyutz Android版本:4.4.2 在上一篇文章中,我们分析了Android智能指针中的强指针sp,本文我们来分析弱指针wp.为什么需要弱指针wp呢?我们来考虑下面一种场景:有两个类CParent和CChild,CParent类中有一个智能指针指向CChild对象,CChild类中有一个智能指针指向CParent对象 class CParent :public LightRefBase<CParent> { --

Android架构设计和软硬整合完整训练:HAL&amp;Framework&amp;Native Service&amp;Android Service&amp;Best Practice

如何理解Android架构设计的初心并开发出搭载Android系统并且具备深度定制和软硬整合能力特色产品,是本课程解决的问题. 课程以Android的五大核心:HAL.Binder.Native Service.Android Service(并以AMS和WMS为例).View System为主轴,一次性彻底掌握Android的精髓. 之所以是开发Android产品的必修课,缘起于: 1,     HAL是Android Framework&Application与底层硬件整合的关键技术和必修技

Android架构设计和软硬整合完整训练

Android架构设计和软硬整合完整训练:HAL&Framework&Native Service&Android Service&Best Practice 如何理解Android架构设计的初心并开发出搭载Android系统并且具备深度定制和软硬整合能力特色产品,是本课程解决的问题. 课程以Android的五大核心:HAL.Binder.Native Service.Android Service(并以AMS和WMS为例).View System为主轴,一次性彻底掌握An

王家林最受欢迎的一站式云计算大数据和移动互联网解决方案课程 V3之Android架构设计和实现完整训练:HAL&amp;Framework&amp;Native Service&amp;Android Service&amp;Best Practice

如何理解Android架构设计的初心并开发出搭载Android系统并且具备深度定制和软硬整合能力特色产品,是本课程解决的问题. 课程以Android的五大核心:HAL.Binder.Native Service.Android Service(并以AMS和WMS为例).View System为主轴,一次性彻底掌握Android的精髓. 之所以是开发Android产品的必修课,缘起于: 1,  HAL是Android Framework&Application与底层硬件整合的关键技术和必修技术: