Android Architecture Component App架构指南

版权声明:本文为博主原创文章,欢迎大家转载!

但是转载请标明出处: https://blog.csdn.net/t000818/article/details/84312601 ,本文出自:【唐宏宇的博客】


本指南适用于过去开发过应用有一定基础知识的开发人员,现在希望了解开发强大的,可生产的高质量应用的最佳实践和建议的体系结构。

手机App用户体验

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

在AndroidMainifest.xml中声明了大部分这些应用组件。 Android操作系统然后使用此文件来决定如何将我们的应用程序集成到设备中,给到用户优质的体验。鉴于编写的Android应用包含多个组件,并且用户经常在短时间内会和多个应用程序进行交互,应用程序需要适应不同类型的用户操作流程和任务。


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

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

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

  3. 最终,用户返回社交应用并共享照片。

在此过程中的任何时候,用户都可能被电话或通知中断。在对此中断采取行动后,用户希望能够返回并恢复此照片共享过程。
该应用程序跳跃行为在移动设备上很常见,因此我们的应用必须正确处理这些流程。


请记住,移动设备也受资源限制,因此在任何时候,操作系统都可能会杀死某些应用程序进程,以为新的进程腾出内存空间。

鉴于这样的环境条件,我们的应用程序组件可能会单独启动并无序启动,操作系统或用户可能随时销毁它们。由于这些事件不在控制之下,因此不应在应用程序组件中存储任何应用程序数据或状态,并且应用程序组件之间不应相互依赖。

 

共同的架构原则

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

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

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

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

持久性是理想的选择的原因如下:

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

通过将应用程序中模型类,明确定义它数据的管理职责,我们应用程序更具可测试性和一致性。

 

推荐的App架构


在本节中,我们将使用端到端的用例演示,如何使用Architecture Components构建应用程序。

注意:编写最适合每种情况的应用程序是不可能的。话虽这么说,这个推荐的架构是大多数情况和工作流程的良好起点。如果您已经有一种编写遵循通用架构原则的Android应用程序的好方法,则无需更改它。
想象一下,我们正在构建一个显示用户配置文件的UI。我们使用私有后端和REST API来获取配置文件的数据。

预览

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

请注意,每个组件仅取决于其下一级的组件。例如,activities 和fragments 仅依赖于 view model。数据仓库是只依赖于其他几个类;在此示例中,数据仓库依赖于持久数据model和远程后端数据源。

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

构建用户界面

UI由Fragment,UserProfileFragment及其对应的布局文件user_profile_layout.xml构成。


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

  • User ID: 用户的标识符。最好使用Fragment参数将此信息传递到Fragment中。如果Android操作系统销毁了我们的进程,该数据仍然会被保留,下次重新启动应用时ID仍然可用。
  • User object: 包含用户详细信息的数据类

我们使用基于ViewModel体系结构组件的UserProfileViewModel来保存信息。

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


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

  • 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类中设置用户字段时,我们需要一种方法来通知UI。这就是LiveData架构组件的用处了。

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

注意:如果您已经使用了像RxJavaAgera这样的库,则可以继续使用它们而不是LiveData。但是,当使用这些库和方法时,请确保正确处理应用程序的生命周期。特别是,确保在相关的生命周期持有对象(LifecycleOwner)stop时暂停数据流,并在销毁相关的生命周期持有对象(LifecycleOwner)时销毁这些数据流。我们还可以添加android.arch.lifecycle:reactivestreams工件以将LiveData与另一个链式库(如RxJava2)一起使用。
要将LiveData组件集成到我们的应用程序中,我们将UserProfileViewModel中的字段类型更改为LiveData <User>。现在,在更新数据时通知UserProfileFragment。此外,由于此LiveData字段可识别生命周期,因此在不再需要引用后会自动清除引用。

UserProfileViewModel

public class UserProfileViewModel extends ViewModel {
    ...

    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 -> {
      // Update UI.
    });
}

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

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

我们也没有添加任何逻辑来处理配置更改,例如用户旋转设备的屏幕。当配置发生更改时,UserProfileViewModel会自动恢复,因此只要创建新fragment,它就会收到相同的ViewModel实例,并使用当前数据立即调用回调。鉴于ViewModel对象比它们需更新的相应View对象,更持久,我们不应该在ViewModel的实现中包含对View对象的直接引用。有关ViewModel的生命周期的更多信息对应于UI组件的生命周期,请参阅The lifecycle of a ViewModel.。

获取数据

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

对于该示例,我们假设我们的后台提供REST API。我们使用Retrofit 库来访问我们的后台,当然我们可以自由地使用其他不同的库来实现相同的目的。

这是我们与后台通信的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类负有太多责任,这违反了关注点分离(separation of concerns)原则。此外,ViewModel的范围与Activity或Fragment生命周期相关联,这意味着当关联的UI对象的生命周期结束时,来自Webservice的数据将丢失。这样会有不好的用户体验。

下面我们的ViewModel将数据获取过程委托给新模块,即数据仓库。

Repositor模块处理数据操作。它们提供了一个干净的API,以便应用程序可以轻松地获取到这些数据。他们知道从何处获取数据以及在更新数据时要调用什么API。我们可以将数据仓库视为不同数据源之间的调解器,例如持久性模型,Web服务和缓存。

如以下代码段所示,我们可以定义UserRepository类,使用WebService实例来获取用户的数据:

UserRepository

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This isn't an optimal implementation. We'll fix it later.
        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 case is left out for brevity.
        });
        return data;
    }
}

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

注意:为简单起见,我们已经省略了网络错误情况。对于公开错误和加载状态的替代实现,请参阅附录:Addendum: exposing network status

管理组件之间的依赖关系
上面的UserRepository类需要一个Webservice实例来获取用户的数据。它可以简单地创建实例,但要做到这一点,它还需要知道Webservice类的依赖关系。此外,UserRepository可能不是唯一需要Web服务的类。这种情况要求我们复制代码,因为每个需要引用Webservice的类都需要知道怎么依赖并如何构造它。
如果每个类都创建一个新的WebService,我们的应用程序可能会变得非常耗费资源。


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

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

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

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

下面的示例应用程序使用Dagger 2来管理Webservice对象的依赖项。

连接ViewModel和数据仓库

下面我们使用 UserRepository 对象,修改 UserProfileViewModel类。:

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

    // Instructs Dagger 2 to provide the UserRepository parameter.
    @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;
    }
}

缓存数据
UserRepository实现抽象地对Webservice对象的调用,但由于它只依赖于一个数据源,因此它不是很灵活。

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

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

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

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

UserRepository

// Informs Dagger that this class should be constructed only once.
@Singleton
public class UserRepository {
    private Webservice webservice;

    // Simple in-memory cache. Details omitted for brevity.
    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);

        // This implementation is still suboptimal but better than before.
        // A complete implementation also handles 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;
    }
}

 

数据持久化

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

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

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

处理这种情况的正确方法是使用持久模型。
这就是Room persistence library存在的意义。

Room是一个对象映射库,提供本地数据持久化和最少的样板代码。在编译时,它根据您的数据模型验证每个查询,因此错误的SQL查询会导致编译时错误而不是运行时失败。 Room抽象了使用原始SQL表和查询的一些底层实现细节。使用LiveData对象,它可以让我们观察数据库中数据的更改,包括集合和连接查询。
它甚至明确定义了解决常见线程问题的执行约束,例如访问主线程上的存储。

注意:如果您的应用已经使用了其他持久性解决方案,例如SQLite对象关系映射(ORM),则无需使用Room替换现有解决方案。但是,如果您正在编写新应用或重构现有应用,我们建议您使用Room来保留应用的数据。这样,您就可以利用库中抽象和查询验证功能。
要使用Room,我们需要定义本地数据模型。首先,我们添加@Entity 注解到我们的用户数据模型类和@PrimaryKey注解到类的id字段。这些注释将User标记为数据库中的表,并将id标记为表的主键:

User

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

  // Getters and setters for fields.
}

接下来,我们新建一个数据库类实现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使用LiveData,因此该操作非常有效;它仅在至少有一个活动观察者时才更新数据。

注意:Room同时也会校验修改表数据是否有效,这意味着它可能会发送误报通知。
在定义了UserDao类之后,我们从数据库类中引用DAO:

UserDatabase

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

接下来我们集成Room到 UserRepository 

@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);
        // Returns a LiveData object directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        // Runs in a background thread.
        executor.execute(() -> {
            // Check if user data was fetched recently.
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // Refreshes the data.
                Response<User> response = webservice.getUser(userId).execute();

                // Check for errors here.

                // Updates the database. The LiveData object automatically
                // refreshes, so we don't need to do anything else here.
                userDao.save(response.body());
            }
        });
    }
}

请注意,即使我们更改了UserRepository中数据的来源,我们也不需要更改UserProfileViewModel或UserProfileFragment。这个小范围的更新展示了我们的应用程序架构提供的灵活性。它也非常适合测试,这样我们可以虚拟创建一个UserRepository测试UserProfileViewModel。

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

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

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

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

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

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

有关示例,请参阅android-architecture-components GitHub项目中的NetworkBoundResource实现。

在UserRepository类中提供另一个可以返回User的当前刷新状态的函数。如果当用户显式操作(例如下拉刷新)时,数据获取过程中,在UI中显示网络状态,则此会更好。

测试每个组件
在关注点分离部分,我们提到遵循这一原则的一个关键好处是可测试性。

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

  • 用户界面和交互:使用Android UI工具测试。创建此测试的最佳方法是使用Espresso库。您可以创建fragment并为其提供模拟UserProfileViewModel。因为fragment只与UserProfileViewModel通信,所以模拟这一个类就足以完全测试应用程序的UI。
  • ViewModel:您可以使用JUnit测试来测试UserProfileViewModel类。您只需要模拟一个类UserRepository。
  • UserRepository:可以使用JUnit测试来测试UserRepository。需要模拟Webservice和UserDao。在这些测试中,验证以下行为:

数据仓库正确地调用了Web服务。

数据仓库将结果保存到数据库中。

如果数据被缓存并且是最新的,则数据仓库不会发出非必要的请求。

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

  • UserDao:使用instrumentation tests来测试DAO类。因为这些instrumentation tests不需要任何UI组件,所以它们运行得很快。

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

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

  • Webservice:在这些测试中,请避免对后端进行网络调用。

对于所有测试,尤其是基于Web的测试,独立于外部世界非常重要。


包括MockWebServer在内的几个库可以为我们这些测试创建虚拟的本地服务器。

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

InstantTaskExecutorRule:使用此规则立即执行调用线程上的任何后台操作。

CountingTaskExecutorRule:使用此规则等待Architecture Components的后台操作。我们还可以将此规则与Espresso关联为空闲资源

 

 

最佳实践

编程是一个创造性的领域,构建Android应用程序并不是一个例外。有许多方法可以解决问题,无论是在多个activity或fragment之间传递数据,检索远程数据并在本地持久化以用于脱机模式,或者其他类似场景,App都会碰到。

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

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

 

附录:披露网络状态


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

本节演示如何使用封装数据及其状态的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 }
}

由于显示从网络加载数据,并拷贝该数据到磁盘时是很常见的,所以创建一个可以在多个地方重用的helper类是很好的。在本例中,我们创建了一个名为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之后,我们可以使用它在UserRepository类中编写User的磁盘和网络绑定实现:

 

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

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值