谷歌官方Android APP 架构指南

2017年的Google IO 上,Google 官方推出了一套架构组件类库,并给出了一份 APP 架构指南。



一、引言

本指南使用于具有构建应用程序基础并且想了解构建强大、优质的应用程序的最佳实践和推荐架构的开发人员。

注:本指南假定读者熟悉 Android Framework。如果你是一个应用程序开发的新手,请参阅 入门指南 系列培训,其中包含了本指南先决条件的相关主题。

二、移动应用用户体验(Mobile app user experiences)

在大多数情况下,APP 只有一个来自桌面或程序启动器的入口点并作为独立进程运行。另一方面,Android 应用程序具有更复杂的结构。典型的 Android 应用程序包含多个应用程序组件,包括 activities, fragments, services, content providers, broadcast receivers等

在 APP manifest 中声明了大部分这些应用组件。Android 操作系统使用此文件来决定如何将您的应用程序集成到设备的整体用户体验中。鉴于正确编写的 Android 应用程序包含多个组件,并且用户经常在短时间内与多个应用程序进行交互,因此应用程序需要适应不同类型的用户驱动的工作流程和任务。

例如,当您考虑在自己喜欢的社交网络应用中分享照片时会发生什么:

  • 该应用程序触发相机意图。Android 操作系统启动相机应用程序来处理请求。此时,用户已离开社交网络应用程序,但他们的体验仍然是无缝的。

  • 相机应用程序可能会触发其他意图,例如启动文件选择器,这可能会启动另一个应用程序。

  • 最终,用户返回社交网络应用程序并共享照片。

在此过程中的任何时候,用户都可能被电话或通知中断。在对此中断采取行动后,用户希望能够返回并恢复此照片共享过程。此应用程序跳跃行为在移动设备上很常见,因此您的应用必须正确处理这些问题。记住,移动设备也受资源限制,因此在任何时候,操作系统都可能会杀死某些应用程序进程以为新的进程腾出空间。

鉴于此环境的条件,您的应用程序组件可能会被单独启动和无序的启动,操作系统或用户可以随时销毁它们。因为应用程序组件是短暂的,并且其声明周期(什么时候被创建和销毁)不受你控制,因此 不应该在应用程序组件中存储任何应用程序数据或状态,同时应用程序组件不应相互依赖。

三、常见的架构原则(Common architectural principles)

如果不应该使用应用程序组件来存储应用程序数据和状态,那么应该如何构建应用程序?

3.1 关注点分离(Separation of concerns)

最重要的原则是 分离关注点,在一个 Activity 或一个 Fragment 中编写所有代码是一个常见的错误。这些基于UI的类应该只包含处理 UI 和 APP 交互的逻辑。通过保持这些类的精简可以避免许多与生命周期相关的问题发生。

请记住,你没有自己的实现 Activity 和 Fragment; 相反,这些只是表示 Android 操作系统和应用程序之间协议的粘合类。操作系统可以根据用户交互或低内存等系统条件随时销毁它们。为了提供令人满意的用户体验和更易于管理的应用程序维护体验,最好尽量减少对它们的依赖。

3.2 从模型中驱动 UI(Drive UI from a model)

另一个重要原则是您应该从模型驱动 UI,最好是持久模型。模型是负责处理应用程序数据的组件。它们独立于 View 应用中的 对象和应用组件,因此它们不受应用生命周期和相关问题的影响。持久性是理想的,原因如下:

  • 如果 Android 操作系统销毁应用以释放资源,用户不用担心丢失数据。
  • 如果网络连接不稳定或无法使用,应用仍可继续使用。

通过将应用程序基于具有明确定义的数据管理职责的模型类,您的应用程序更具可测性和一致性。

四、推荐的应用程序架构(Recommended app architecture)

在本文中,将演示如何使用 官方Android应用架构库(Architecture Components) 构建应用程序。

注意:不可能有一种应用程序的编写方式对于每种情况都是完美的。话虽这么说,这个推荐的架构是大多数情况和工作流程的良好起点。如果您已经有一种编写遵循通用架构原则的 Android 应用程序的好方法,则无需更改它。

假设我们正在构建一个显示用户配置文件的 UI。我们使用私有后端和 REST API 来获取给定配置文件的数据。

4.1 概述

首先,请考虑下图,该图显示了在设计应用程序后所有模块应如何相互交互:

请注意,每个组件仅取决于其下一级的组件。例如,Activity 和 Fragment 仅依赖于视图模型。Repository 是唯一依赖于其他多个类的类; 在此示例中,Repository 依赖于持久数据模型和远程后端数据源。

这种设计创造了一种一致和愉快的用户体验。无论用户在上次关闭应用程序几分钟后还是几天后都回到应用程序,他们会立即看到应用程序在本地持续存在的用户信息。如果此数据过时,应用程序的存储库模块将开始从后台更新数据。

4.2 构建用户界面

UI 由 Fragment 文件 UserProfileFragment.java 和相应的布局文件组成 user_profile_layout.xml 。

要驱动 UI,数据模型需要包含以下数据元素:

  • User ID:用户的标识符。最好使用 fragment 的参数将用户 ID 传到 fragment 中。如果 Android 操作系统破坏了我们的流程,则会保留此信息,因此下次重新启动应用时 ID 就可用。
  • User object:包含用户详细信息的数据类。

我们使用基于 ViewModel 的架构组件 UserProfileViewModel 来保存此信息。

一个 ViewModel 对象提供针对特定 UI 组件中的数据,如一个 fragment 或 activity,并包含数据处理的业务逻辑与 model 进行通信。例如,ViewModel 可以调用其他组件来加载数据,它可以转发用户请求来修改数据。ViewModel 不知道UI组件,因此它不会受到配置更改的影响,例如旋转设备时,重新创建的 activity 。

现在定义了以下文件:

  • user_profile.xml:屏幕的UI布局定义。
  • UserProfileFragment.java:显示数据的UI控制器。
  • UserProfileViewModel.java:准备数据以供查看 UserProfileFragment 并对用户交互作出反应的类。

以下代码段显示了这些文件的起始内容。(为简单起见,省略了布局文件。)

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

UserProfileFragment.java

public class UserProfileFragment extends Fragment {
    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_layout, container, false);
    }
}

现在有了这些代码模块,如何连接它们?毕竟,当 user 在 UserProfileViewModel 类中设置字段时,我们需要一种方法来通知 UI。这就是 LiveData 架构组件的用武之地。

LiveData 是一个可观察的数据持有者。应用程序中的其他组件可以使用此监视对象的更改,而无需在它们之间创建明确且严格的依赖关系路径。LiveData 组件还尊重应用程序组件的生命周期状态(如activities, fragments, and services),并包括清除逻辑以防止对象泄漏和过多的内存消耗。

注意:如果您已经使用了像 RxJava 或 Agera 这样的库 ,则可以继续使用它们而不是 LiveData。但是,当您使用这些库和方法时,请确保正确处理应用程序的生命周期。特别是,确保在相关 LifecycleOwner 内容停止时暂停数据流,并在相关内容 LifecycleOwner 被销毁时销毁这些流。您还可以添加 android.arch.lifecycle:reactivestreams 组件以将 LiveData 与另一个反应流库(如 RxJava2)一起使用。

要将 LiveData 组件合并到我们的应用程序中,我们更改 UserProfileViewModel 中的字段类型变成 LiveData。现在,在UserProfileFragment 更新数据时通知。此外,由于此 LiveData 字段可识别生命周期,因此在不再需要引用后会自动清除引用。

UserProfileViewModel.java

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

现在我们修改 UserProfileFragment 观察数据并更新 UI:

UserProfileFragment.java

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

每次更新用户配置文件数据时, onChanged() 都会调用回调,并刷新 UI。

如果您熟悉使用可观察回调的其他库,可能已经意识到我们没有重写 Fragment 的 onStop() 方法来停止观察数据。LiveData 不需要此步骤,因为它可识别生命周期,这意味着 onChanged() 除非 Fragment 处于活动状态,否则它不会调用回调。也就是说,它已收到 onStart() 但尚未收到onStop()。调用 Fragment 的 onDestroy() 方法时,LiveData 也会自动删除观察者。

我们也没有添加任何逻辑来处理配置更改,比如用户旋转设备的屏幕。当配置发生变化时,UserProfileViewModel 会自动恢复,因此一旦创建新的 Fragment,它就会接收到相同的 ViewModel 实例,并且使用当前数据立即调用回调。鉴于 ViewModel 对象的目的是超越它们更新的相应视图对象,您不应该在 ViewModel 的实现中包含对视图对象的直接引用。有关 ViewModel 生命周期的更多信息对应于UI组件的生命周期,请参阅 ViewModel 的生命周期。

4.3 请求数据

现在我们已经使用 LiveData 连接 UserProfileViewModel 到了 UserProfileFragment,我们如何获取用户配置文件数据?

对于此示例,假设我们的后端提供 REST API。我们使用 Retrofit 库来访问我们的后端,尽管您可以自由地使用不同的库来实现相同的目的。

以下是我们 Webservice 与后端通信的定义:

Webservice.java

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 获取数据并将此数据分配给我们的 LiveData 对象。这种设计有效,但通过使用它,我们的应用程序随着它的发展变得越来越难以维护。它给 UserProfileViewModel 类带来了太多的责任 ,这违反了 关注点分离 原则。此外,ViewModel 的范围与 Activity or Fragment 生命周期联系在一起,这意味着当关联的UI对象的生命周期结束时,来自 Webservice 的数据就会丢失。这种行为会产生不良的用户体验。

相反,我们的 ViewModel 将数据抓取过程委托给一个新的模块,即 Repository 。

Repository 模块处理数据操作。它们提供了一个干净的 API,以便应用程序的其余部分可以轻松地检索这些数据。他们知道从何处获取数据以及在更新数据时要进行的 API 调用。可以将 Repository 视为不同数据源之间的调解器,例如持久性 models,web services 和 caches。

我们的 UserRepository 类(如以下代码段所示)使用一个实例 WebService 来获取用户的数据:

UserRepository.java

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // 这不是一个最优的实现。我们稍后再解决。
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }

            // 为了简便起见,省略了错误案例。
        });
        return data;
    }
}

即使 Repository 模块看起来不必要,它也有一个重要的目的:它从应用程序的其余部分抽象出数据源。现在,我们 UserProfileViewModel 不知道如何获取数据,因此我们可以为视图模型提供从几个不同的数据获取实现获得的数据。

注意:为简单起见,我们省略了网络错误情况。有关公开错误和加载状态的替代实现,请参阅 附录:公开网络状态。

4.4 管理组件之间的依赖关系

UserRepository 上面的类需要一个 Webservice 获取用户数据的实例。它可以简单地创建实例,但要做到这一点,它还需要知道 Webservice 类的依赖关系。另外, UserRepository 可能不是唯一需要的 Webservice 的类。这种情况要求我们复制代码,因为需要引用的每个类都需要 Webservice 知道如何构造它及其依赖项。如果每个类创建一个新的 WebService,我们的应用程序可能会变得非常消耗资源。

您可以使用以下设计模式来解决此问题:

  • 依赖注入(DI):依赖注入允许类在不构造它们的情况下定义它们的依赖关系。在运行时,另一个类负责提供这些依赖项。我们建议使用 Dagger2 库在 Android 应用程序中实现依赖注入。Dagger2 通过遍历依赖树自动构造对象,并为依赖关系提供编译时保证。
  • 服务定位器:服务定位器模式提供了一个注册表,其中类可以获取它们的依赖关系而不是构造它们。

实现服务注册表比使用 依赖注入 更容易,因此如果不熟悉 DI,请改用服务定位器模式。

这些模式允许您扩展代码,因为它们提供了清晰的模式来管理依赖项,而无需复制代码或增加复杂性。此外,这些模式允许您在测试和生产数据获取实现之间快速切换。
我们的示例应用程序使用 Dagger2 来管理 Webservice 对象的依赖项。

4.5 连接ViewModel和存储库

现在,修改 UserProfileViewModel 使用 UserRepository 对象:

UserProfileViewModel.java

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

    // 命令 Dagger 2 提供 UserRepository 参数。
    @Inject
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(int userId) {
        if (this.user != null) {
            // ViewModel is created on a per-Fragment basis, so the userId
            // doesn't change.
            return;
        }
        user = userRepo.getUser(userId);
    }

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

4.6 缓存数据

UserRepository 实现将对 Webservice 对象的调用抽象出来,但因为它只依赖于一个数据源,不是很灵活。

UserRepository 实现的关键问题是,在它从我们的后端获取数据之后,它不会在任何地方存储这些数据。因此,如果用户离开 UserProfileFragment,然后返回到它,我们的应用程序必须重新取回数据,即使它没有改变。

由于以下原因,此设计不是最理想的:

  • 它浪费了宝贵的网络带宽。
  • 它强制用户等待新查询完成。

为了解决这些缺点,我们向 UserRepository 添加了一个新的数据源,它在内存中缓存 User 对象:

UserRepository.java

// 告诉Dagger2 这个类只应该构造一次。
@Singleton
public class UserRepository {
    private Webservice webservice;

    // 简单的内存缓存。为了简洁起见,省略了细节。
    private UserCache userCache;

    public LiveData<User> getUser(int userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);

        // 这个实现仍然是次优的,但是比以前更好了。
        // 一个完整的实现也需要对应的处理错误案例,此处忽略
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}

4.7 持久化数据

使用我们当前的实现,如果用户旋转设备或离开并立即返回应用程序,现有的 UI 将立即可见,因为 Repository 从内存缓存中检索数据。但是,如果用户离开应用程序并在Android 操作系统杀死进程后几小时后回来会发生什么?在这种情况下依靠我们当前的实现,我们需要从网络再次获取数据。这种重新获取过程不仅仅是糟糕的用户体验; 这也是浪费,因为它消耗了宝贵的移动数据。

您可以通过缓存 Web 请求来解决此问题,但这会产生一个关键的新问题:如果相同的用户数据显示来自其他类型的请求,例如获取朋友列表,会发生什么?该应用程序将显示不一致的数据,这充其量令人困惑。例如,如果用户在不同时间发出好友列表请求和单用户请求,我们的应用可能会显示同一用户数据的两个不同版本。我们的应用程序需要弄清楚如何合并这些不一致的数据。

处理这种情况的正确方法是使用持久模型。这是 Room persistence library 来救援的地方。

Room 是一个对象映射库,它通过最少的样板代码提供本地数据持久化。在编译时,它会根据您的数据模式验证每个查询,因此损坏的 SQL 查询会导致编译时错误而不是运行时故障。房间抽象出了使用原始SQL表和查询的一些底层实现细节。它还允许您观察对数据库数据的更改,包括收藏品和连接查询,使用 LiveData 对象公开这些更改。它甚至显式地定义了处理常见线程问题的执行约束,比如访问主线程上的存储器。

注意:如果您的应用已经使用了其他持久性解决方案,例如 SQLite 对象关系映射(ORM),则无需使用 Room 替换现有解决方案。但是,如果您正在编写新应用或重构现有应用,我们建议您使用 Room 来保留应用的数据。这样,您就可以利用库的抽象和查询验证功能。

要使用 Room,我们需要定义本地模式。首先,我们将 @Entity 注释添加到 User 数据模型类中,并将 @PrimaryKey 注释添加到类的 id 字段中。这些注释标记 User 为数据库 id 中的表和表的主键:

User.java

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;

  // Getters and setters for fields.
}

然后,我们通过实现 RoomDatabase 我们的应用程序来创建数据库类 :

UserDatabase.java

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

注意这 UserDatabase 是抽象的。Room 自动提供它的实现。有关详细信息,请参阅 Room 文档。

我们现在需要一种将用户数据插入数据库的方法。对于此任务,我们创建了一个数据访问对象(DAO)

UserDao.java

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

请注意,该 load 方法返回一个类型的对象 LiveData。Room 知道数据库何时被修改,并在数据发生变化时自动通知所有活动观察者。由于 Room 使用 LiveData,因此该操作非常有效; 它仅在至少有一个活动观察者时才更新数据。

注意:Room 根据表格修改检查失效,这意味着它可能会发送误报通知。

在 UserDao 定义了我们的类之后,我们从数据库类中引用 DAO:

UserDatabase.java

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

现在我们可以修改 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);
        // 直接从数据库返回LiveData对象。
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        // 在后台线程中运行。
        executor.execute(() -> {
            // 检查最近是否获取用户数据。
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // 刷新数据。
                Response<User> response = webservice.getUser(userId).execute();

                // 在这里检查错误。

                // 更新数据库。LiveData对象是自动的
                // 刷新,所以我们不需要做任何其他事情。
                userDao.save(response.body());
            }
        });
    }
}

请注意,即使我们更改了数据的来源 UserRepository,我们也不需要更改我们的 UserProfileViewModel 或 UserProfileFragment。这个小范围的更新展示了我们的应用程序架构提供的灵活性。它也非常适合测试,因为我们可以提供假数据 UserRepository 并同时测试我们的产品 UserProfileViewModel。

如果用户在返回使用此体系结构的应用程序之前等待几天,那么在存储库可以获取更新信息之前,他们可能会看到过时的信息。根据您的使用情况,您可能不希望显示此过时信息。相反,您可以显示占位符数据,该数据显示虚拟值并指示您的应用当前正在获取并加载最新信息。

4.8 单一的真实来源

不同的 REST API 端点返回相同的数据是很常见的。例如,如果我们的后端有另一个端点,它返回一个朋友列表,同一个用户对象可能来自两个不同的 API 端点,甚至可能使用不同的粒度级别。如果 UserRepository 原样返回来自 Webservice 请求的响应,而不检查一致性,我们的 UI 可能会显示令人困惑的信息,因为 Repository 中数据的版本和格式将取决于最近调用的端点。

因此,我们的 UserRepository 实现将 web 服务响应保存到数据库中。对数据库的更改会触发对活动 LiveData 对象的回调。使用这个模型,数据库可作为单一真实来源,应用程序的其他部分可使用 UserRepository 访问它。无论您是否使用磁盘缓存,我们建议您的存储库指定一个数据源作为应用程序其余部分的唯一真实来源。

4.9 显示正在进行的操作

在某些用例中,例如 pull-to-refresh,UI 向用户显示当前正在进行网络操作非常重要。将 UI 操作与实际数据分开是一种很好的做法,因为数据可能会因各种原因而更新。例如,如果我们获取了一个好友列表,可能会再次以编程方式获取相同的用户,从而触发 LiveData 更新。从 UI 的角度来看,有请求在运行这一事实只是另一个数据点,类似于 User 对象本身的任何其他数据。

我们可以使用以下策略之一在 UI 中显示一致的数据更新状态,无论更新数据的请求来自何处:

  • 更改 getUser() 以返回类型的对象 LiveData。该对象将包括网络操作的状态。有关示例,请参阅 NetworkBoundResource 中 android-architecture-components GitHub 项目中的实现。
  • 在 UserRepository 类中提供另一个可以返回刷新状态的公共函数 User。如果要仅在数据获取过程源自显式用户操作(例如,下拉刷新 pull-to-refresh)时才在 UI 中显示网络状态,则此选项会更好。

4.10 测试每个组件

在关注点分离部分,提到遵循这一原则的一个关键好处是可测试性。以下列表显示了如何从扩展示例中测试每个代码模块:

  • 用户界面和交互:使用 Android UI 工具测试。创建此测试的最佳方法是使用 Espresso 库。可以创建片段并为其提供模拟 UserProfileViewModel。因为 Fragment 只与 ViewModel 交互,所以 UserProfileViewModel 模拟这个类就足以完全测试应用的UI。
  • ViewModel:可以 UserProfileViewModel 使用 JUnit 测试来测试该类。只需要模拟一个类,UserRepository 来测试它。
  • UserRepository:也可以使用 JUnit test 来测试 UserRepository。需要模拟 Webservice 和 UserDao。可以测试 UserRepository 是否进行了正确的Web服务调用。将结构保存到数据库,如果数据被缓存且是最新的,则不会发起任何不必要的请求。因为 WebService 和 UserDao 都是接口,所以可以模拟它们或者为更复杂的测试用例创建假的实现。
  • UserDao:使用检测测试来测试 DAO 类。由于这些检测测试不需要任何 UI 组件,因此它们可以快速运行。对于每个测试,创建一个内存数据库以确保测试没有任何副作用,例如更改磁盘上的数据库文件。

Room 允许指定数据库实现,因此可以通过提供 SupportSQLiteOpenHelper 的 JUnit 实现来测试 DAO 。但是,不建议使用此方法,因为设备上运行的 SQLite 版本可能与开发计算机上的 SQLite 版本不同。

  • Web 服务:在这些测试中,避免对后端进行网络调用。对于所有测试,尤其是基于Web的测试,独立于外部世界非常重要。包括 MockWebServer 在内的几个库 可以帮助您为这些测试创建虚假的本地服务器。
  • 测试工件:Architecture Components提供了一个maven工件来控制其后台线程。该 android.arch.core:core-testing 工件包含以下JUnit的规则:InstantTaskExecutorRule:使用此规则立即执行调用线程上的任何后台操作。CountingTaskExecutorRule:使用此规则等待架构组件的后台操作。您还可以将此规则与 Espresso 关联为空闲资源。

五、最佳做法(Best practices)

编程是一个创造性的领域,构建 Android 应用程序也不例外。有许多方法可以解决问题,无论是在多个 fragment 或 activity 之间传递数据,检索远程数据并在本地持久保存以用于脱机模式,还是任何其他非常重要的应用程序遇到的常见场景。

虽然以下建议不是强制性的,但我们的经验是,遵循它们可以使您的代码库在长期运行中更加健壮,可测试和可维护:

  • 避免将应用的入口点(如活动,服务和广播接收器)指定为数据源。相反,它们应该只与其他组件进行协调,以检索与该入口点相关的数据子集。每个应用程序组件都是相当短暂的,这取决于用户与设备的交互以及系统的整体当前健康状况。
  • 在应用的各个模块之间创建明确定义的责任范围。例如,不要将代码库中的数据加载到代码库中的多个类或包中。同样,不要将多个不相关的职责(例如数据缓存和数据绑定)定义到同一个类中。
  • 从每个模块尽可能少地暴露。不要试图创建 “只是那个” 的快捷方式,从一个模块公开内部实现细节。您可能会在短期内获得一些时间,但随着代码库的发展,您会多次承担技术债务。
  • 考虑如何使每个模块独立可测试。例如,具有用于从网络获取数据的定义良好的 API 使得更容易测试将该数据保存在本地数据库中的模块。相反,如果您将这两个模块的逻辑混合在一个地方,或者在整个代码库中分发网络代码,那么测试就变得更加困难 - 如果不是不可能的话。
  • 专注于您应用的独特核心,以便从其他应用中脱颖而出。不要一次又一次地编写相同的样板代码来重新发明轮子。相反,请将时间和精力集中在使应用程序独一无二的地方,并让 Android 架构组件和其他推荐的库处理重复的样板。
  • 保持尽可能多的相关和新鲜数据。这样,即使设备处于离线模式,用户也可以享受应用的功能。请记住,并非所有用户都享受恒定的高速连接。
  • 将一个数据源指定为单一事实来源。每当您的应用需要访问此数据时,它应始终源于此单一事实来源。

六、附录:暴露网络状态(Addendum: exposing network status)

在上面 推荐的应用程序架构 部分中,省略了网络错误和加载状态以保持代码片段的简单性。

本节演示如何使用 Resource 封装数据及其状态的类来公开网络状态。

以下代码段提供了以下示例实现 Resource:

// A generic class that contains data and status about loading this data.
public class Resource<T> {
    @NonNull public final Status status;
    @Nullable public final T data;
    @Nullable public final String message;
    private Resource(@NonNull Status status, @Nullable T data,
            @Nullable String message) {
        this.status = status;
        this.data = data;
        this.message = message;
    }

    public static <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(Status.SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String msg, @Nullable T data) {
        return new Resource<>(Status.ERROR, data, msg);
    }

    public static <T> Resource<T> loading(@Nullable T data) {
        return new Resource<>(Status.LOADING, data, null);
    }

    public enum Status { SUCCESS, ERROR, LOADING }
}

因为在显示该数据的磁盘副本时从网络加载数据是很常见的,所以创建一个可以在多个位置重用的帮助程序类是很好的。在本例中,我们创建了一个名为的类NetworkBoundResource。

下图显示了以下决策树NetworkBoundResource:



它首先观察资源的数据库。第一次从数据库加载条目时,NetworkBoundResource 检查结果是否足以分派或是否应从网络重新获取。请注意,这两种情况都可能同时发生,因为您可能希望在从网络更新缓存数据时显示缓存数据。

如果网络调用成功完成,它会将响应保存到数据库中并重新初始化流。如果网络请求失败,则 NetworkBoundResource 直接发送失败。
注意:将新数据保存到磁盘后,我们从数据库重新初始化流。但是,我们通常不需要这样做,因为数据库本身恰好发送了更改。

请记住,依赖数据库来分派更改涉及依赖于相关的副作用,这是不好的,因为如果数据库因为数据没有更改而最终没有调度更改,则可能会发生这些副作用的未定义行为。
此外,不要发送从网络到达的结果,因为这会违反单一的真实原则。毕竟,数据库可能包含在“保存”操作期间更改数据值的触发器。同样,不要在没有SUCCESS新数据的情况下进行调度,因为那时客户端会收到错误版本的数据。

以下代码段显示了 NetworkBoundResource 类为其子级提供的公共 API :

// ResultType: Type for the Resource data.
// RequestType: Type for the API response.
public abstract class NetworkBoundResource<ResultType, RequestType> {
    // Called to save the result of the API response into the database.
    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    // Called with the data in the database to decide whether to fetch
    // potentially updated data from the network.
    @MainThread
    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // Called to get the cached data from the database.
    @NonNull @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    // Called to create the API call.
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // Called when the fetch fails. The child class may want to reset components
    // like rate limiter.
    @MainThread
    protected void onFetchFailed();

    // Returns a LiveData object that represents the resource that's implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

请注意有关类定义的这些重要细节:

  • 它定义了两个类型的参数,ResultType 并且 RequestType,因为从 API 返回的数据类型可能不符合当地使用的数据类型。
  • 它使用一个 ApiResponse 为网络请求调用的类。ApiResponse 是一个简单的包装 Retrofit2.Call 类,它将响应转换为实例 LiveData。

NetworkBoundResource 该类的完整实现作为 android-architecture-components GitHub 项目的一部分出现 。
创建后 NetworkBoundResource,我们可以用它来写我们的磁盘和网络结合实现 User 的 UserRepository 类:

UserRepository.java

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final int userId) {
        return new NetworkBoundResource<User,User>() {
            @Override
            protected void saveCallResult(@NonNull User item) {
                userDao.insert(item);
            }

            @Override
            protected boolean shouldFetch(@Nullable User data) {
                return rateLimiter.canFetch(userId)
                        && (data == null || !isFresh(data));
            }

            @NonNull @Override
            protected LiveData<User> loadFromDb() {
                return userDao.load(userId);
            }

            @NonNull @Override
            protected LiveData<ApiResponse<User>> createCall() {
                return webservice.getUser(userId);
            }
        }.getAsLiveData();
    }
}

Refer

原文:https://developer.android.com/topic/libraries/architecture/guide.html
https://developer.android.google.cn/jetpack/docs/guide

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