应用程序架构指南(本文已过时,请点击原文链接查看最新官方文档)

原文链接

目录

移动应用程序用户体验

一般架构原则

关注点分离

由 model(模型)驱动 UI

概览

构建用户界面

获取数据

连接 ViewModel 和 repository

缓存数据

持久化数据

展示正在进行中的操作

测试每个组件

最佳实践

附录:暴露网络状态


本指南面向那些已经掌握了构建应用程序的基础知识,现在想了解构建健壮、高质量应用程序的最佳实践和推荐架构的开发人员。

本页假设您熟悉 Android 框架。如果您是 Android 应用程序开发新手,请查看我们的 开发指南,其中包含本指南的必要主题。

移动应用程序用户体验


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

大多数这些应用程序组件是在你的应用程序清单中声明的。然后Android操作系统使用这个文件来决定如何将你的应用程序集成到设备的整体用户体验中。考虑到一个正确编写的Android应用程序包含多个组件,并且用户经常在短时间内与多个应用程序交互,应用程序需要适应不同类型的用户驱动的工作流和任务。

例如,思考当你在喜欢的社交网络应用程序中分享照片时会发生什么:

  1. 应用程序触发相机 intent。然后 Android 操作系统启动摄像头应用程序来处理这个请求。此时,用户已经离开了社交网络应用程序,但他们的体验仍然是无缝的。
  2. 相机应用程序可能触发其他 intent,比如启动文件选择器,而文件选择器也可能启动另一个应用程序。
  3. 最后,用户返回社交网络应用程序并分享照片。

在这个过程中的任何时候,用户都可能被来电或通知打断。在中断过后,用户希望能够返回并恢复分享该照片的过程。这种应用切换行为在移动设备上司空见惯,所以你的应用程序必须正确处理这些流程。

请牢记移动设备也是资源受限的,所以在任何时候,操作系统都可能杀死一些应用程序进程来为新的应用程序腾出空间。

考虑到这种情景,您的应用程序组件可能单独启动,并且处于无序状态,操作系统或用户可以在任何时候销毁它们。因为这些事件不在您的控制之下,所以你不应该在应用程序组件中存储任何应用程序数据或状态,并且应用程序组件不应该相互依赖。

一般架构原则


如果你不该使用应用程序组件来存储应用程序数据和状态,你又该如何设计应用程序呢?

关注点分离

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

请记住,您并不拥有 Activity 和 Fragment 的实现;相反,这些只是代表 Android 操作系统与您的应用程序之间的契约的粘合关联类。基于用户交互或由于低内存等系统条件,操作系统可以随时销毁它们。为了提供令人满意的用户体验和更易于管理的应用程序维护体验,最好尽量减少对它们的依赖。

由 model(模型)驱动 UI

另一个重要原则是你应该 由模型来驱动你的 UI,最好是持久化的模型。models 是负责处理应用程序数据的组件。它们与你应用程序中的 View 和应用程序组件无关,因此它们不受应用程序生命周期和相关问题的影响。

持久化操作是理想的,原因如下:

  • 如果 Android 操作系统为了释放资源而销毁了你的应用程序,你的用户不会丢失数据。
  • 在网络连接不稳定或不可用的情况下,你的应用程序仍能继续工作。

通过让你的应用程序基于具有管理数据的明确职责的模型类,你的应用程序将更有可测试性和连续性。


在本节中,我们将演示如何通过使用端到端用例来构造使用 Architecture Components(架构组件)的应用程序。

注意:不存在一种对每种情况都适用的编写应用程序的方法。这就是说,这个推荐的架构对于大多数情况和工作流来说都是很好的起点。如果你已经有了较好的方法来编写遵循 通用架构原则 的 Android 应用程序,那你就无需更改它。

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

概览

首先,思考下列示意图,它展示了在设计应用程序之后,所有模块应该如何相互交互:

请留意每个组件只依赖于它下面的一个组件。例如,activites 和 fragments 仅依赖于 view model(视图模型)。repository(存储库)是唯一依赖于多个其他类的类;在本例中,repository 依赖于持久数据 model 和远程后端数据源。

这种设计创造了连贯、愉快的用户体验。无论用户是在上次关闭应用程序几分钟后还是几天后返回应用程序,他们都会立即看到应用程序在本地保存的用户信息。如果这些数据已过时,应用程序的 repository 模块就开始在后台更新数据。

构建用户界面

  • 用户ID:用户的标识符。最好使用 fragment arguments(参数)将此信息传递到片段中。如果 Android 操作系统销毁了我们的进程,但信息被保存了下来,所以在下次重启我们的应用程序时,ID 仍旧可用。
  • 用户对象:一个保存用户详细信息的数据类。

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

ViewModel 对象为特定的UI组件(如 fragment 或 activity)提供数据,它还包含用于与 model 通信的数据处理业务逻辑。例如,ViewModel 可以调用其他组件来加载数据,还可以发送用户请求来修改数据。ViewModel 并不知道 UI 组件的细节,因此它不受配置更改的影响,比如在旋转设备时重新创建 activity。

现在我们已经定义好了以下文件:

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

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

UserProfileViewModel

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

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, container, false);
    }
}

既然我们有了这些代码模块,我们如何连接它们呢?毕竟,当 UserProfileViewModel 类中的变量 user 被设置时 ,我们需要一种方式来通知 UI。这就是 LiveData 架构组件的所在。

LiveData 是一个可观察的数据持有者。应用程序中的其他组件可以使用该持有者监视对象的更改,而不需要在对象之间创建显式且严格的依赖路径。LiveData 组件还尊重应用程序组件的生命周期状态——例如活动、片段和服务——并且包括清理逻辑以防止对象泄漏和过度的内存消耗。

注意:如果您已经使用了像 RxJava 或 Agera 这样的库,您可以继续使用它们而不是 LiveData。但是,当你使用像这样的库和方法时,请确保你能正确地处理应用程序的生命周期。尤其是要确保当相关的  LifecycleOwner(生命周期持有者) 停止时要暂停数据流,并且当相关的 LifecycleOwner销毁时,同时销毁这些流。你还可以添加 android.arch.lifecycle:reactivestreams 工件,以便将LiveData与另一个响应流库(如RxJava2)一起使用。

为了将 LiveData 组件加入到我们的应用程序中,我们将 UserProfileViewModel 中的变量类型改为 LiveData<User>。现在,当数据被更新时,将通知 UserProfileFragment 。此外,由于此 LiveData 变量是可以感知生命周期的,所以在不再需要这些引用之后,它将自动清理引用。

UserProfileViewModel

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

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

UserProfileFragment

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

每次更新用户配置文件数据时, onChanged() 回调将被调用,同时刷新UI。

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

获取数据

既然我们已经使用 LiveData 将 UserProfileViewModel 连接到了  UserProfileFragment,那么我们如何获取用户配置文件数据?

下面是与我们的后端进行通信的 Webservice 的定义:

Webservice

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 或 Fragment 的生命周期相关联,这意味着当关联的 UI 对象的生命周期结束时,来自 Webservice 的数据将丢失。这种行为会产生糟糕的用户体验。

下面的代码片段展示的是我们的 UserRepository 类,使用 WebService 实例来获取用户的数据:

UserRepository

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

            // 简单起见,Error 情况被排除在外。
        });
        return data;
    }
}

尽管 repository 模块看起来没有必要,但它有一个重要的用途:它从应用程序的其余部分抽象出数据源。现在,我们的 UserProfileViewModel 不知道如何获取数据,因此我们可以为 view 模块提供从几个不同的 数据获取实现 中获得的数据。

注意:简单起见,我们忽略了网络错误的情况。对于暴露错误和加载状态的另一种实现,请参阅 附录:暴露网络状态。

管理组件之间的依赖关系

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

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

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

实现服务注册表比使用DI更容易,所以如果不熟悉DI,可以使用服务定位器模式。

这些模式允许您扩展代码,因为它们提供了清晰的模式来管理依赖性,而无需重复代码或增加复杂性。此外,这些模式允许您在用于测试的和生产的获取数据的实现之间快速切换。

我们的示例应用程序使用 Dagger 2 来管理 Webservice 对象的依赖关系。

连接 ViewModel 和 repository

现在,我们修改我们的 UserProfileViewModel 来使用 UserRepository 对象:

UserProfileViewModel

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

    // 通知 Dagger 2 to 提供 UserRepository 参数.
    @Inject
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(int userId) {
        if (this.user != null) {
            // ViewModel 是基于每个 fragment 创建的,因此 userId 不会改变。
            return;
        }
        user = userRepo.getUser(userId);
    }

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

缓存数据

 UserRepository 实现抽象了对 Webservice 对象的调用,但是因为它仅依赖于一个数据源,所以不是很灵活。

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

这并不是最佳设计,原因如下:

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

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

UserRepository

// 告诉 Dagger,这个类应该只构造一次。
@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;
    }
}

持久化数据

使用我们当前的实现,如果用户旋转设备或离开并立即返回应用程序,则现有的UI立即变得可见,因为 repository 从内存缓存中检索数据。

然而,如果用户离开应用程序并在几小时后回来,Android 操作系统杀死进程后会发生什么?在这种情况下,基于我们当前的实现,我们需要从网络中再次获取数据。这个再次获取的过程不但是糟糕的用户体验,而且是浪费的,因为它消耗了宝贵的移动数据。

您可以通过缓存 web 请求来解决这个问题,但是这会产生一个关键的新问题:如果从其他类型的请求中显示相同的用户数据,比如获取好友列表,会发生什么呢?应用程序将显示不一致的数据,这是最混乱的。例如,如果用户在不同时间发出好友列表请求和单用户请求,我们的应用程序可能会显示同一用户数据的两个不同版本。我们的应用程序需要知道如何合并这些不一致的数据。

处理这种情况的正确方法是使用持久化的 model。这就是 Room 持久化库发挥重要作用的地方。

Room 是一个对象映射库,它以最小的模板代码提供本地数据的持久化。在编译时,它将根据你的数据方案验证每次查询,因此失败的 SQL 查询会导致编译时错误,而不是运行时失败。Room 抽象出了一些与原始 SQL表 和查询一起工作的底层实现细节。它还允许您观察数据库数据的更改,包括集合和联合查询,并使用 LiveData 对象公开这些更改。它甚至显式地定义了解决常见线程问题的执行约束,例如访问主线程上的存储。

注意:如果您的应用程序已经使用了另一个持久化解决方案,例如SQLite对象-关系映射(ORM),则不需要用 Room 替换现有的解决方案。但是,如果你正在编写一个新的应用程序或者重构现有的应用程序,我们建议使用 Room 来保存你的应用程序的数据。这样,您就可以充分利用该库的抽象和查询验证功能。

要使用 Room,我们需要定义我们的本地模型。首先,我们将 @Entity 注解添加到我们的 User 数据 model 类,并添加 @PrimaryKey 注解到类的变量 id 上。这些注解标识  User 作为我们数据库中的表, id 作为表的主键:

User

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

  // 变量的 Getters 和 setters。
}

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

UserDatabase

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

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

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

UserDao

@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<User> 对象。Room 知晓数据库被修改的时机,并自动通知所有处于活动状态的观察者。因为 Room 使用 LiveData,该操作是高效的;只有当至少有一个活动的观察者时,它才更新数据。

注意:Room 会检查基于数据表的无效改动,这意味着它也许会发送假的修改通知。

使用我们定义好的 UserDao 类,我们就可以从数据库类中引用DAO:

UserDatabase

@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 。

单一数据源

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

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

展示正在进行中的操作

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

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

  • 更改 getUser() 来返回 LiveData 类型的对象。此对象将包括网络操作的状态。

    例如,请参阅 android-architecture-components (Android架构组件)GitHub 项目中的 NetworkBoundResource 实现。

  • 在 UserRepository 类中提供另一个公共函数,该方法可以返回 User 的刷新状态。如果你希望仅当数据获取过程源自显式的用户操作(例如 pull-to-refresh )时才在 UI 中显示网络状态,则此选项更好。

测试每个组件

关注点分离小节中,我们提到遵循这一原则的一个主要优点是可测试性。

下面的列表显示了如何从扩展示例中测试每个代码模块:

  • 用户界面和交互:使用 Android UI instrumentation test。创建这个测试的最佳方法是使用 Espresso 库。您可以创建 fragment 并为其提供模拟的 UserProfileViewModel。因为 fragment 只与 UserProfileViewModel 通信,所以模拟这个类就足以完全测试你的应用程序的 UI。

  • ViewModel: 您可以使用 JUnit test 来测试 UserProfileViewModel 类。你只需要模拟一个类, 即 UserRepository

  • UserRepository: 您也可以使用 JUnit test 来测试 UserRepository。你需要模拟 Webservice 和 UserDao。在这些测试中,验证以下行为:

    因为 Webservice 和 UserDao 都是接口,所以您可以对它们进行模拟或为更复杂的测试用例创建虚假实现。

    • repository 创建正确的网络服务调用。
    • repository 将结果保存到了数据库中。
    • 若果数据被缓存了并且是最新的,那么 repository 不发起不必要的请求。
  • UserDao: 使用 instrumentation tests 测试 DAO 类。因为这些测试不需要任何 UI 组件,所以它们得以快速运行。 

       对于每个测试,创建一个内存数据库,以确保测试没有任何副作用,例如更改磁盘上的数据库文件。

注意:Room允许指定数据库实现,因此可以通过提供 SupportSQLiteOpenHelper 的JUnit实现来测试DAO。但是,不推荐使用这种方法,因为在设备上运行的 SQLite 版本可能与开发机器上的 SQLite 版本不同。

  • Webservice: 在这些测试中,避免对后端进行网络调用。对于所有的测试,尤其是基于网络的测试,独立于外界是很重要的。

       一些库,例如 MockWebServer,可以帮助您为这些测试创建一个虚假本地服务器。 

  • 测试工件: Architecture Components 提供一个 Maven工件 来控制其后台线程。android.arch.core:core-testing 包含以下 JUnit 规则:

    • InstantTaskExecutorRule(即时任务执行器规则): 使用此规则正在调用的线程上立即执行任何后台操作。
    • CountingTaskExecutorRule(任务执行器计数规则): 使用此规则等待 Architecture Components 的后台操作。您还可以将此规则与 Espresso 相关联,作为一项 闲置资源

最佳实践


编程是个创造性领域,构建 Android 应用程序也不例外。解决问题的方法有很多,无论是在多个 activities 或 fragments 之间通信数据,检索远程数据并为脱机模式把这些数据保存到本地,还是特定应用程序遇到的任何其他常见场景。

尽管以下建议不是强制性的,但我们的经验是,遵循这些建议可以使您的代码库从长远来看更加健壮、可测试和可维护:

避免指定应用程序的入口点—如 activities、services 和 broadcast receivers —作为数据源。

相反,它们只应与其他组件协调以检索与该入口点相关的数据子集。每个应用程序组件的存活时间都相当短暂,这取决于用户与设备的交互以及系统当前总体的健康状况。

在你的应用程序的各个模块之间创建明确的职责范围。

例如,不要将从网络加载数据的代码分散到你的代码库中的多个类或包中。同样,不要定义多个无关的职责—例如数据缓存和数据绑定—到同一个类中。

尽可能少地暴露每个模块。

不要试图创建“就是那个”快捷方式,这个快捷方式会从一个模块中公开内部实现细节。短期内你可能会节省一点时间,但是在你的代码库进化的过程中,你会多次承担技术债务。

考虑如何使每个模块在隔离状态下可测试。

例如,使用明确定义的 API 从网络获取数据,可以更容易地测试将数据持久化在本地数据库中的模块。相反,如果在一个地方混合来自这两个模块的逻辑,或者将网络代码分布在整个代码库中,那么测试就变得非常困难(如果不是不可能的话)。

专注于你的应用程序的独特核心,让它从其他应用程序中脱颖而出。

不要反复编写相同的模板代码来重新发明轮子。相反,把时间和精力集中在你的应用程序的独特之处,让 Android Architecture Components(Android 架构组件) 和其他推荐的库处理重复的样板。

尽可能多地保存相关和新鲜的数据。

这样,即使设备处于离线模式下,用户也可以享受应用程序的功能。记住,不是所有的用户都享有持续、高速的连接。

指定一个数据源作为单一数据源。

无论你的应用程序何时访问一条数据,该数据应当总是源于 单一数据源

附录:暴露网络状态


在上面 推荐的应用程序架构 小节中,我们忽略了网络错误和加载状态来保持代码段的简洁。

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

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

// 包含数据和加载此数据的状态的泛型类。
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 }
}

在显示数据磁盘副本的同时从网络加载数据的情况是很常见的,所以最好创建一个可以在多个地方重用的 helper 类。对于这个例子,我们创建了一个名为 NetworkBoundResource 的类。

下图显示了NETWorkDealEdvices的决策树:

它是由观察资源的数据库开始的。当第一次从数据库加载该条目时,NetworkBoundResource 资源检查该结果是否满足被调度的条件,或者是否应该从网络中重新获取。请注意,这两种情况都可能同时发生,因为您可能希望在从网络更新缓存数据的同时显示缓存数据。

如果网络调用成功完成,则将响应保存到数据库中并重新初始化流。如果网络请求失败,则 NetworkBoundResource 直接发送失败的信号。

注意:在将新数据保存到磁盘之后,我们从数据库重新初始化流。然而,我们通常不需要这样做,因为数据库本身恰好调度了更改。

请记住,依赖数据库来调度更改涉及依赖关联的副作用,这并不好,因为如果数据库由于数据没有更改而不调度更改,则可能会发生来自这些副作用的未定义行为。

另外,不要将来自网络的结果发送出去,因为这违背了 单一数据源 原则。毕竟,数据库可能包含在“保存”操作期间更改数据值的触发器。同样,不要在没有新数据的情况下发送 SUCCESS ,因为那样客户端将接收到错误的数据版本。 

下面的代码片段显示了 NetworkBoundResource 资源类为其子类提供的公共 API:

// ResultType: 资源数据类型。
// RequestType: API 响应数据类型。
public abstract class NetworkBoundResource<ResultType, RequestType> {

    // 调用该方法将 API 响应的结果保存到数据库中。
    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    // 调用数据库中的数据来决定是否获取
    // 来自网络的潜在地被更新的数据。
    @MainThread
    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // 调用该方法从数据库获取被缓存的数据。
    @NonNull @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    // 调用该方法来创建 API 请求。
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // 当获取失败时调用。子类可能需要重置类似速率限制器这样的组件。
    @MainThread
    protected void onFetchFailed();

    // 返回一个代表在基类中被实现的资源的 LiveData 对象。
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

注意这些关于该类定义的重要细节:

  • 它定义了两种类型的参数,即 ResultType 和 RequestType,因为从API返回的数据类型可能与本地使用的数据类型不匹配。
  • 它使用名为 ApiResponse 的类来处理网络请求。ApiResponse 是 Retrofit2.Call 类的一个简单包装,用于将响应转换为 LiveData 实例。

NetworkBoundResource 类的完整实现作为 android-architecture-components GitHub 项目的一部分出现。

在创建 NetworkBoundResource 之后,我们可以使用它在 UserRepository 类中编写我们的用户磁盘和网络范畴的实现:

UserRepository

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

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值