鲁棒性、可读的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 cleaner syntax possible.

This blog post won‘t give a precise implementation, but one possible way to structure an app with a good balance between flexibility, readability and robustness.

Some existing solutions

At the beginning of Android, most people were relying on AsyncTasks for long running processes. Basically: it sucked, there‘s already a lot of articles on this subject. Later on, Honeycomb introduced Loaders which were better for supporting configuration changes. In 2012, Robospicecame out, based on an Android Service running in the background. Thisinfographic shows how it works.

It‘s great compared to AsyncTask, but I still have some problems with it. Here‘s the average code to make a request with Robospice, in an Activity. No need to read it precisely, it‘s just to give you an idea:

FollowersRequest request = new FollowersRequest(user);
lastRequestCacheKey = request.createCacheKey();
spiceManager.execute(request, lastRequestCacheKey,
    DurationInMillis.ONE_MINUTE,
    new RequestListener<FollowerList> {
      @Override
      public void onRequestSuccess(FollowerList listFollowers) {
        // On failure
      }

      @Override
      public void onRequestFailure(SpiceException e) {
          // On success
      }
    });

And the Request that goes with it:

public class FollowersRequest extends SpringAndroidSpiceRequest<FollowerList> {
  private String user;

  public FollowersRequest(String user) {
    super(FollowerList.class);
    this.user = user;
  }

  @Override
  public FollowerList loadDataFromNetwork() throws Exception {
    String url = format("https://api.github.com/users/%s/followers", user);
    return getRestTemplate().getForObject(url, FollowerList.class);
  }

  public String createCacheKey() {
      return "followers." + user;
  }
}

Problems

  1. This code looks awful and you have to do it for each request!
  2. You need to create a SpiceRequest subclass for each type of request
  3. You need to create a RequestListener for each request
  4. If the cache expires shortly, the user will have to wait at each call
  5. If the cache expires after a long time, the user could see obsolete data
  6. The RequestListener keeps an implicit reference to the activity, what about memory leaks?

Not so good…

Concise and robust in five steps

When I started working on Candyshop, I tried something else. I mixed different libraries with very interesting features and tried to make something concise yet robust.

Here‘s a global schema of what I‘ll explain in the next parts.

Step 1 — An easy to use cache system

You‘ll need a persistent cache system. Keep it simple.

@EBean
public class Cache {
    public static enum CacheKey { USER, CONTACTS, ... }

    public <T> T get(CacheKey key, Class<T> returnType) { ... }
    public void put(CacheKey key, Object value) { ... }
}

Step 2 — A REST client

I give this as an example, just make sure the logic of the REST API you‘re using stays in one place.

@Rest(rootUrl = "http://anything.com")
public interface CandyshopApi {

    @Get("/api/contacts/")
    ContactsWrapper fetchContacts();

    @Get("/api/user/")
    User fetchUser();

}

Step 3 — An application-wide event bus

Instantiate it in a strategic place, accessible from anywhere in the app, the Application object is a good candidate for that.

public class CandyshopApplication extends Application {
    public final static EventBus BUS = new EventBus();
    ...
}

Step 4 — An Activity which needs some data!

My solution is, like Robospice, based on a service, but not an Android one. A regular singleton object, shared accross the app. We‘ll see the code of that service in step 5. But right now, let‘s see how the Activity code looks like, because this is what I wanted to simplify the most in the first place!

@EActivity(R.layout.activity_main)
public class MainActivity extends Activity {

    // Inject the service
    @Bean protected AppService appService;

    // Once everything is loaded…
    @AfterViews public void afterViews() {
        // … request the user and his contacts (returns immediately)
        appService.getUser();
        appService.getContacts();
    }

    /*
        The result of the previous calls will
        come as events through the EventBus.
        We‘ll probably update the UI, so we
        need to use @UiThread.
    */

    @UiThread public void onEvent(UserFetchedEvent e) {
        ...
    }

    @UiThread public void onEvent(ContactsFetchedEvent e) {
        ...
    }

    // Register the activity in the event bus when it starts
    @Override protected void onStart() {
        super.onStart();
        BUS.register(this);
    }

    // Unregister it when it stops
    @Override protected void onStop() {
        super.onStop();
        BUS.unregister(this);
    }

}

One line to request the user, one line to express the fact we‘ll receive an answer for that request. Same thing for contacts. Sounds really good!

Step 5 — A singleton service

As I said in step 4, the service I‘m using is not an Android service. I actually started with one, but I changed my mind. The reason issimplicity. Services are meant to be used when you need to have something running while no activity is displayed, or when you want to make some code available to other apps. That‘s not exactly what I wanted. Using a simple singleton allows me to avoid using ServiceConnectionBinder, etc...

There‘s many things to say here. Let‘s start with a schema to show you what happens when we called getUser() and getContacts() from the Activity. Then I‘ll explain the code.

You can imagine each serial is a thread.

What you see here is what I really like about this model; the view is immediately filled with cached data, so most of the time the user doesn‘t have to wait. Then, when the up-to-date result arrives from the server, the displayed information is replaced. The counterpart of this is that you need to ensure the activity can receive the same type of response multiple times. Keep it in mind while creating the activity and you‘ll be okay.

Okay, let‘s see some code!

// As I said, a simple class, with a singleton scope
@EBean(scope = EBean.Scope.Singleton)
public class AppService {

    // (Explained later)
    public static final String NETWORK = "NETWORK";
    public static final String CACHE = "CACHE";

    // Inject the cache (step 1)
    @Bean protected Cache cache;

    // Inject the rest client (step 2)
    @RestService protected CandyshopApi candyshopApi;

    // This is what the activity calls, it‘s public
    @Background(serial = CACHE)
    public void getContacts() {

        // Try to load the existing cache
        ContactsFetchedEvent cachedResult =
            cache.get(KEY_CONTACTS, ContactsFetchedEvent.class);

        // If there‘s something in cache, send the event
        if (cachedResult != null) BUS.post(cachedResult);

        // Then load from server, asynchronously
        getContactsAsync();
    }

    @Background(serial = NETWORK)
    private void getContactsAsync() {

        // Fetch the contacts (network access)
        ContactsWrapper contacts = candyshopApi.fetchContacts();

        // Create the resulting event
        ContactsFetchedEvent event = new ContactsFetchedEvent(contacts);

        // Store the event in cache (replace existing if any)
        cache.put(KEY_CONTACTS, event);

        // Post the event
        BUS.post(event);

    }

}

That‘s a lot of code for a single request! Actually, I exploded it to make it more explanatory, but it‘s always the same pattern so you can easily create helpers to make single-lined methods. For example getUser()would look 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()));
    }

So what about the serial thing? Here‘s what the doc says:

By default, all @Background annotated methods are run in parallel. Two methods using the same serial are guaranteed to be run on the same thread, sequentially (ie. one after the other).

Running network calls one after the other may have performance impacts, but it‘s so much easier to deal with GET-after-POST kind of things with it, that I‘m ready to sacrifice a little performance. Moreover, you can easily tune the serials afterward to improve performance if you notice anything. Currently in Candyshop, I use four different serials.

To conclude

The solution I described here is a draft, it‘s the basic idea I started with, a few months ago. As of today, I‘ve been able to solve all particular cases I encountered, and I really enjoy working with it so far. There‘s a few other awesome things I‘d like to share about this model, like error managementcache expirationPOST requestscancelling of useless ops, but I‘m really grateful you read so far, so I won‘t push it!

What about you? Did you find the design of your dreams, one that you enjoy working with on a daily basis?

转自:http://blog.joanzapata.com/robust-architecture-for-an-android-app/

http://blog.jobbole.com/66606/

时间: 2024-12-15 12:13:37

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

鲁棒性、可读的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

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与底层硬件整合的关键技术和必修技术: