Android官方 App 架构指南

2017年的Google IO 上,Google 官方推出了一套架构组件类库,并给出了一份 App 架构指南。这个组件的主要目的就是帮助开发者方面处理Android应用组件生命周期的问题,并提供了一个SQlite 对象映射类库。利用这些组件,可以构建一个 APP 基本的架构骨架。

长期以来,Android应用层架构的选择没有约束,MVC、MVP、MVVM等各种架构方式都有开发者使用,Google 官方也没有推荐的架构模型和应用指南。开发者在处理Activity、Fragment、Service的生命周期时,需要处理很多资源回收或者状态、数据持久化的问题,而这些case在项目中也没有通用的解决方式。

通用架构准则

第一准则是『关注点隔离(Separation of concerns)』,就是依赖解耦。最常见的错误是把所有代码都写在Activity或者Fragment里。任何与操作UI和系统交互无关的代码,都不应该在Activity或Fragment类里。

第二条准则:通过Model驱动UI,最好是可持久化的Model。

持久化有两个原因:

  • 当系统由于资源紧张回收掉你的应用时,或者有网络连接等问题时,用户不会丢失数据。
  • Model 是负责处理应用程序数据的组件,独立于UIView和应用程序组件,因此是和应用程序组件的生命周期隔离的。这样,UI代码保持简洁,逻辑更易管理。

推荐 APP 架构

Google 通过新引入的架构组件(Architecture Components)演示构建一个App。

构建用户界面

一个显示个人信息的界面,用户数据通过一个 REST API 获取。

UI 由一个Fragment UserProfileFragment.java 文件和 layout user_profile_layout.xml 组成。

为了驱动UI,DataModel 需要持有两种数据元素:

  • User ID:用户标识id,通过Fragment的参数传入Fragment,当应用被系统回收时,会被保存,下次应用重启时,id 仍是可用的。
  • User object:持有User数据的POJO。

为此我们创建一个 UserProfileViewModel(继承自 ViewModel类)。

ViewModel 为特定的UI组件(Activity、Fragment)提供数据,也会处理业务数据的交互,诸如调用其他组件加载数据,转发用户的修改。ViewModel并不了解View,也不会受到 cofiguration changes的影响(例如Activity 横竖屏切换)。

3个文件:

  • user_profile_layout.xml
  • UserProfileFragment.java
  • UserProfileViewModel.java
  public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
  public class UserProfileFragment extends LifecycleFragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }
}

有了ViewModel 和 Fragment 类,怎么联系在一起呢?当ViewModel中的user属性设置或者修改时,我们需要用一种方式通知UI。此时,LiveData就派上用场了。

LiveData是一个可被观察的数据持有者(观察者模式)。可以让 Activity, Fragment 等应用程序组件对其进行观察,并且不会在它们之间创建强依赖,LiveData 还能够自动响应各组件的声明周期事件,防止内存泄漏,从而使应用程序不会消耗更多的内存。

使用LiveData修改ViewModel类:

  public class UserProfileViewModel extends ViewModel {
    ...
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}

修改UserProfileFragment类,使其能够观察user和更新UI:

  @Override
  public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}

每次数据变化时,onChanged 回调会被调用,更新UI。

获取数据

使用Retrofit库,访问后端的User REST API。

  public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}

小白的方式是直接在ViewModel中调用WebService获取数据,但是这样违反了依赖解耦的第一原则。

我们引入一个Repository模块管理数据。Repository 模块负责数据处理,为应用的其他部分提供干净可靠的 API。你可以将其考虑为不同数据源(Web,缓存或数据库)和应用之间的中间层。

  public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}

管理组件之间的依赖

UserRepository中需要一个WebService实例进行数据获取的工作,不要直接new一个WebService对象,这很容易产生重复代码,并且使复杂度上升。另外,UserRepository可能并不是唯一一个需要WebService的类,如果每个类都创建一个WebService,也会造成资源浪费。

有两种方式解决这个问题:

  • 依赖注入(Dependency Injection):推荐Dagger2
  • 服务定位期模式(Service Locator):通过注册表的方式获取某个class 的依赖,而不是新构建一个。使用比DI简单。

这里我们使用Dagger2管理依赖。

连接ViewModel 和 Repository

  public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}

缓存数据

实际项目中,Repository不止有一个数据源,我们在此加入一个缓存。

  @Singleton  // informs Dagger that this class should be constructed once
  public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}

数据持久化

UserRepository中有一个web数据源,一个内存缓存,当用户杀死进程后,重新启动时,会再次从网络获取数据,这可能不是一个好的体验,因此我们还需要能够持久化。

我们使用 Room 这个组件来完成持久化的任务。

Room是一个 SQLite object 映射类库,可以方便进行数据持久化。Room 抽象出了很多常用的数据库操作,并且在编译时会验证每个查询,从而损坏的 SQL 查询只会导致编译时错误,而不是运行时崩溃。还能和LiveData 一起工作,并帮开发者处理了很多线程问题。

要使用Room,我们用@Entity注解把User类标识成数据库中的一张表。

  @Entity
  class User {
    @PrimaryKey
    private int id;
    private String name;
    private String lastName;
    // getters and setters for fields
  }

继承roomDatabase类,创建数据库类

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}

MyDatabase 类是abstract的,Room会自动添加实现。

现在,我们需要把数据插入数据库,创建DAO 对象:

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}

在数据库类中添加DAO

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

load方法会返回LiveData<User>Room 会知道什么时候数据库发生了变化,并自动通知所有的观察者。

修改UserRepository类,引入Room 数据源:

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread
            // check if user was fetched recently
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database
                userDao.save(response.body());
            }
        });
    }
}

可以看到,即便我们修改了 UserRepository 中的数据源,我们也完全不需要修改 ViewModelFragment,这就是抽象的灵活性。同时还非常易于测试,我们可以在测试 UserProfileViewModel 时提供测试用的 UserRepository。

至此,我们的代码完成了。当用户几天后回到相同的UI页面时,也会马上看到用户信息,因我我们已经把数据持久化了。同时,Repository也会在后台更新数据。当然UI层也会根据需求决定是否显示过期的数据。

在一些场景下,例如下拉刷新,如果正在有网络请求时UI显示用户信息是很重要的。分离UI操作和实际数据是很有用处的,因为数据可以有各种原因触发更新。

测试

  • UI:使用 Espresso
  • ViewModel:JUnit,需要mock UserRepository
  • UserRepository:JUnit,需要mock WebService和DAO
  • UserDao:推荐使用instrumenttation测试,Room可以允许指定数据库实现,因此可以提供一个JUnit实现的SupportSQLiteOpenHelper
  • WebServiceMockWebServer

最终架构

指导原则

  • 在 manifest中定义的组件(activity、service、broadcast receiver等),都不是数据源。因为每个组件的生命周期都很短,而且取决于当前用户和设备的交互和系统的运行状况。这些都不能作为应用的数据源。
  • 确保 APP 中的模块职责清晰,建立明确的责任边界。简单来说,不要把不相关的功能职责放到同一个类里实现(例如数据缓存和数据binding)。
  • 模块尽量少暴露内部实现。不要为了一时方便而把不应暴露的实现暴露出去,在后期这会导致越来越多的技术债。
  • 在定义模块直接的交互时,考虑如何将每个模块尽量隔离。通过良好设计的API进行交互。例如,把网络和数据库的数据逻辑放在一起也能使代码正常工作,但是,这很难测试。
  • 你应用的核心是那些能脱颖而出的东西,不要浪费时间重复造轮子或一次次编写同样的模板代码。把精力集中在使你的应用独一无二上,把一些重复性的工作交给Android Architecture Components和其他优秀的第三方库。
  • 尽可能的持久化,使应用能够在脱机模式下可用。
  • Repository应该指定一个数据源作为单一的信任数据源。

原文:https://developer.android.com/topic/libraries/architecture/guide.html

GitHub: https://github.com/googlesamples/android-architecture-components

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容