文档章节

Android Architecture Components

dj_归去来兮
 dj_归去来兮
发布于 2017/08/13 14:39
字数 6063
阅读 39
收藏 0

Guide to App Architecture

这篇指南面向有过构建APP经验,现在想要了解一些Android框架搭建的最佳实践和推荐的架构来构建稳定的,高质量的APP的开发者

 

Common problems faced by app developers

不像那些传统的单入口,单进程的桌面应用,Android 应用拥有更复杂的架构。

一个典型的Android应用由多个组件构造,包括activities(活动),fragments(碎片),services(服务),content providers(内容提供者),broadcast receivers(广播接收器)。

APP大部分的组件声明在清单文件中,告诉Android OS 决定如何集成到用户设备中。

然而,正如之前提到的,APP以传统的方式运行在单进程中,一个妥善编写的APP需要运行在复杂的环境中,特别是用户在设备的不同APP间切换流程和任务的时候。

 

例如,当你向网络社交APP分享一张图片的时候发生了什么?

APP会发起一个让Android OS启动相机程序的请求,这时候,用户无感知的离开了社交APP,并且进入了拍照界面;而相机程序,接下来可能还会发起其他请求,像启动文件管理器选择文件。

最终,用户回到了社交APP,分享了选择的照片;另外,在这个过程中,任何时候都应该被打进来的电话中断,并且在通话结束之后返回到这个过程中。

 

在Android中,应用间的交互是最寻常不过了。所以,APP需要正确的处理这些流程。记住,移动设备的资源有限,所以任何时候 ,操作系统都会为新的应用腾出空间。

 

所有着一切都表明,APP 组件可以被单独的启动,并且无序的销毁,可以被用户销毁,也可以被系统销毁。

因为APP组件的声明周期很短暂,并且不在我们的控制之中,所以,不应该存储任何数据或者状态到这些组件中,并且这些组件不应该相互依赖。

 

 

Common architectural principles

如果不能使用APP组件存储数据和状态,那么APP架构应该怎么搭建呢?

1 在APP中,最重要的关注点应该是分离;一个通常会犯的错误就是将所有的业务代码写在Activity或者Fragment中。

没有持有UI元素的或者与系统有交互的任何代码都不要出现在这些组件中。将他们保持的越瘦越好,这会避免很多生命周期相关的问题。

不要忘记,这些class不属于APP,它们只是实现OS与APP之间的逻辑的粘合剂。

Android OS可以在任何时候销毁它们,由于用户的操作或者其他的因素(低内存)。

最好,在对它们最小程度的依赖下,提供给用户稳定的体验。

2 第二重要的原则是应该用模型驱动UI,宁可是持久化的模型。

持久化原因有两点:① 在操作系统销毁APP的时候,用户不会丢失数据;② 在弱网环境或者网络未连接的情况下,你的APP可以正常工作

模型是负责持有APP数据的组件。它独立于视图和APP中的组件,因此,它就脱离了那些组件的声明周期的问题了。

保持UI代码简单和APP逻辑清晰,易于管理。应用基于良好定义的管理数据的模型有助于APP的测试和保持一致性。

 

Recommended app architecture

这部分内容,我们将通过一个用例来展示如何用架构组件搭建一个APP。

想象一下,我们正在构建一个用户显示用户信息的UI。用户信息从后端的REST API获取

 

Building the user interface

UI由 UserProfileFragment.java 表示,fragment对应的布局文件为 user_profile_layout.xml

为了驱动UI,我们的数据模型需要持有两个数据元素。

1 userID

    user的标识,最好通过fragment的argument方式传递userID,如果Android OS销毁了APP进程,这些信息会被保存起来,当应用重启的时候这些数据还是可用的。

2 User对象

    一个持有user数据的POJO(简单Java对象)

我们将创建一个继承了ViewModel 的类型 UserProfileViewModel  来保持信息

    一个ViewModel为特定的UI组件提供数据,比如fragment或者activity,同时也处理数据业务处理部分,比如调用其它组件加载数据或者传递用户对数据的修改。ViewModel对View是无感知的,并且不会受配置变化的影响,诸如由于旋转导致的重建activity。

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的user属性设置后,我们需要一种方法通知UI。

这就是LiveData引进的缘由。

LiveData是可观察的数据持有者。它使得APP中的组件在没有在组件之间创建明确、死板的引用路径 监听数据的变化。LiveData也遵循组件的生命周期,并且正确的处理防止对象内存泄漏,以防应用消耗更多的内存。

现在我们将 UserProfileViewModel 的属性 User 换成 LiveData<User>,为了当数据变化的时候,让fragment能被通知到。

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

我们修改 UserProfileFragment,让其监听数据,更新UI

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

每当数据发生变化的时候,onChanged回调方法将会被调用,接着更新UI

如果你对其他类库的回调调用方式比较熟悉的话,你可能已经意识到了我们没有必要重写fragment的onStop方法去停止监听数据。对应的,LiveData也是这样,因为它可感知组件的生命周期,意味着除非fragment处在活动状态(接收到onStart方法,没有收到onStop方法),否则不会调用回调方法。当fragment执行onDestroy方法的时候,LiveData也会自动的移除监听。

我们也没有针对配置变化做特殊处理,当配置改变时,ViewModel自动的恢复数据,所以,当一个新的fragment生成的时候,它将接收到相同的viewmodel实例,ViewModel将会用当前的数据调用回调方法。

这就是为什么ViewModel不应该直接引用视图的原因;ViewModel比视图拥有更长的生命周期。

 

Fetching data

现在我们已经将fragment和ViewModel连接起来了,那ViewModel怎么获取user数据呢?在这个示例中,我们假设后端提供了一个REST API接口。我们使用Retrofit类库访问后端接口。

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的简单的实现类可以直接访问接口获取数据并且将获取的数据赋值给user对象。

尽管可以这样做,但是随着APP逐渐庞大,你的APP将会变的越来越难维护。这样做,就赋予了ViewModel太多的职责,这样就违背了之前提到的分层思想。另外,ViewModel的生存范围是UI组件的生命周期,所以当UI组件的生命结束的时候,丢失所有的数据是很不好的用户体验。一种替代方案,我们的ViewModel将该工作委托给新的Repository模块。

    Repository模块负责处理数据操作。它们提供一个整齐清洁的API供上层调用。当APP需要数据的时候,它们知道去哪取数据,知道调用什么API。可以把它看做是不同数据之间的中介(持久化模型、服务端接口、缓存等等).

下面的 UserRepository  使用 WebService  获取user数据:

 

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

尽管repository模块看起来不是必须的,它提供了一个重要的用途;它从APP的其余部分抽象了数据。现在,我们的ViewModel不知道数据是从后端接口获取的,意味着我们可以根据需要对数据进行转换。

 

Managing dependencies between components:

UserRepository 实现功能需要 Webservice 的一个实例,创建实例非常简单,但是构造它,UserRepository 需要知道 Webservice 的依赖。这值得深思,因为这会导致代码变的复杂(Repository单例即可解决)。另外,UserRepository 不是唯一需要使用 Webservice 的类,如果每个类都创建一个新的 Webservice 的实例,将会非常臃肿。

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

    依赖注入:DI(IOC容器管理单例对象生命周期),依赖注入允许classes在不去构造实例的情况下定义它们的依赖,在运行时,另外一个class负责提供这些依赖。这里推荐使用Google的Dagger 2框架去实现Android APP的依赖注入。Dagger 2通过依赖树构造对象,提供编译时依赖保障。

    服务定位器模式(缓存列表):

在这个示例中,我们将用Dagger2去管理依赖。服务定位器提供一个注册表,用来获取依赖关系而不是构造它们,相当易于使用,所以,如果你对依赖注入不熟悉的话,可以使用服务定位器模式。

Connecting ViewModel and the repository

现在我们修改 UserProfileViewModel 类。

 

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

Caching data

上面的仓库是访问网络的抽象实现,但是它只是依赖于数据源。其实它并不实用。

问题是 UserRepository 实现是:在获取数据之后,并没有在任何地方持有数据。如果用户离开了 UserProfileFragment ,然后又回到这个界面,APP又会重新去拉取数据。有两点值得改善:

它浪费了带宽资源,强制用户等待一个新的请求数据完成。为了解决这些问题,我们将添加一个新的数据源到 UserRepository 用来缓存数据到内存中。

 

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

Persisting data

我们的当前实现方案中,如果用户旋转屏幕或者离开了,然后又回到APP中,已经存在的UI会立刻显示,因为仓库类会在内存缓存中获取数据。但是在Android OS杀掉APP进程之后,会发生什么呢?

当前实现方案,我们需要重新访问接口拉取数据,这真是个不好的用户体验,而且使用移动设备重新拉取相同的数据也是浪费资源的。你可以简单的使用缓存网络请求的数据解决之前的问题,但是这种方案又会引起新的问题。如果另一个请求返回相同的用户数据,会发生什么?(不同的用户,返回好友列表)这时候APP大概就会显示前后矛盾的数据了,这充其量导致误导用户的效果。你的APP需要合并这些数据避免显示前后矛盾的数据。

解决这个问题的一个合适的方案就是引入持久化模型。这也是引入Room持久化框架的原因。

Room是一个ORM框架,使用最少的样板代码实现本地数据的持久化。编译期间,它将会根据schema验证每个查询的被打碎的SQL语句的查询结果,这样就不会在运行时出错了。Room将原生的SQL语句抽象成下面的实现方案。它也允许通过LiveData监听数据库的变化。另外,它明确规定了线程模型来解决常见的问题,如在主线程访问存储的数据。

使用Room,首先定义我们的schema,在 User 类加上@Entity注解,标识它是一个数据库中的table。

@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 是抽象的,Room会自动的提供一个它的实现类。

现在,我们需要一个方式插入数据到DB中,这里,我们使用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知道数据库什么时候有变化,当数据改变的时候,它将自动通知所有激活的观察者。由于使用的是LiveData,只要还有激活的观察者,当数据改变的时候,通知功能就是有效的。

现在我们可以修改 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中的数据,我们没有必要改变 UserProfileViewModel 或者UserProfileFragment。这就是抽象的灵活性。这也极大的便于测试,因为你可以提供一个假的UserRepository,当你测试 UserProfileViewModel的时候。

现在,我们的代码已经完成了。如果用户几天之后访问UI的话,他们将会即刻就看到用户信息,因为我们已经将数据持久化到DB中了。同时,如果数据陈旧了,我们会在后台默默的更新仓库。当然了,根据实际情况,不要显示持久化数据,如果数据太旧的话。

在某些情况下,例如下拉刷新,如果有网路操作的话,有必要显示进度条之类的东东。由于数据可能会因为各种原因更新,所以将UI层与数据层分离是一种好的实践。从UI层的角度来看,只是有一个请求在进行中,请求一个数据点,与其他数据无异。

有两种方案处理这个情况:

    1 将方法 getUser 返回LiveData数据类型,包含了网络操作状态。目录中有一种实现就是提供网络状态的。

    2 在repository类中提供另外一个public方法,返回user刷新状态。如果你想要在UI层显示网络状态以明确响应用户操作的话,这是一种更好的选择(下拉刷新状态显示)。

 

Single source of truth(单一数据源的原理)

不同的REST API返回相同类型的数据是在正常不过的了。例如,如果后端有另外一个接口返回好友列表,相同的user对象会从两个不同的API创建出来。如果 UserRepository 使用接口返回的数据,由于服务端数据在这些请求中可以改变,这将会导致UI显示混乱的数据。这也是 UserRepository 为什么这样实现:服务端返回的数据仅仅是存入数据库,然后,改变数据库引起LiveData的回调==>更新UI。

在这个模型当中,数据库服务充当单一数据源,区别于APP的其他部分,而且其他部分通过数据库访问数据。不管有没有使用磁盘缓存,我们建议repository设计成单一的数据源模式。

The final architecture

下面的这个图标展示了我们推荐的架构中的所有模块,以及它们之间如何相互通信。

 

Guiding principles

编程是很有创造性的领域,构建Android APP也不例外。有很多方法解决一个问题,可以是在多个activities或者fragments之间交换数据,获取远程数据并为了离线模式持久化到本地,或者其他APP遭遇的任何场景。

虽然下面的建议不是强制的,但是这是我们的能让代码健壮、可测试、可维护的,APP长期稳定运行的经验。

    1 这些定义在清单文件中的切入点(activities,services,broadcast receivers等等)不是数据的来源。相反,它们应该协调相关的数据子集。因为每个APP组件的存活时间参考用户与设备交互的活跃的时间相比是很短暂的,所以不要想把这些切入点作为数据的来源。

    2 给APP的各个模块设置责任区分是不分你我的。例如,不要在你的代码库中或者多个类中传递获取数据的代码;同样的,不要在同一个类中编写不相关的责任逻辑代码-例如,在同一个类中做数据缓存和数据绑定。

    3 每个模块对外暴露的接口越少越好。不要创建所有功能都在那个类里面的类,这样会暴露内部实现细节。这样做虽然短期内,你可能会缩短开发时间,但是随着代码的拓展,你将会花更多的时间在技术的维护上。

    4 当你设计模块间的交互的时候,考虑一下如何使每个模块独立可测试。例如,有一个良好设计的获取网络数据的模块更容易测试持久化数据到本地数据库。相反,如果你将这两个逻辑混在一起,或者将你的网络层代码撒在代码库中,这将难以测试。

    5 APP的核心是如何区别于其他的APP,别出心裁。不要在重复造轮子或者一遍又一遍的写同样的重复的样板代码上花费你的时间。反而,把精力放在如何让你的APP做成独一无二的,并且让Android Architecture Components 和其他推荐的类库处理样板代码。

    6 尽量持久化相关的和新鲜的数据,这样你的APP就可以在设备断网的情况下使用了。虽然你可能喜爱恒定的、高速的连接,但是你的用户可能并不这样想。

    7 你的repository应该设计成单数据源。不论APP什么时候需要访问数据,都应该从这个单一的数据源中获取数据。

Addendum: exposing network status

在上面的推荐APP架构部分,我们故意忽略网络层的错误和加载状态的处理,保持例子简单。在这个部分,我们使用一个 Resource class包装数据和它的状态。

下面是示例的代码实现:

 

//a generic class that describes a data with a status
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<>(SUCCESS, data, null);
    }

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

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

因为从网络加载数据的时候,同时展示本地数据是一个常见的方案。我们创建一个helper类 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 it should be
    // fetched 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 that represents the resource
    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

请注意,上面的类中定义了两种类型参数(ResultType,RequestType)因为API返回的数据类型可能不匹配在本地使用的数据类型。

还要注意,上面的代码使用 ApiResponse 网络请求。ApiResponse是对 Retrofit2.Call 一个简单的包装,将其响应转换成一个LiveData。

下面的是 NetworkBoundResource 类剩余部分的实现。

 

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        LiveData<ApiResponse<RequestType>> apiResponse = createCall();
        // we re-attach dbSource as a new source,
        // it will dispatch its latest value quickly
        result.addSource(dbSource,
                newData -> result.setValue(Resource.loading(newData)));
        result.addSource(apiResponse, response -> {
            result.removeSource(apiResponse);
            result.removeSource(dbSource);
            //noinspection ConstantConditions
            if (response.isSuccessful()) {
                saveResultAndReInit(response);
            } else {
                onFetchFailed();
                result.addSource(dbSource,
                        newData -> result.setValue(
                                Resource.error(response.errorMessage, newData)));
            }
        });
    }

    @MainThread
    private void saveResultAndReInit(ApiResponse<RequestType> response) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                saveCallResult(response.body);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                // we specially request a new live data,
                // otherwise we will get immediately last cached value,
                // which may not be updated with latest results received from network.
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }
}

现在,在仓库中,我们可以使用 NetworkBoundResource 类去写我们的磁盘和网络绑定获取user的数据的实现了。

来自

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final String 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();
    }
}

 来自 https://developer.android.google.cn/topic/libraries/architecture/guide.html

分享自 Guide to App Architecture

© 著作权归作者所有

共有 人打赏支持
dj_归去来兮
粉丝 14
博文 113
码字总数 31739
作品 0
合肥
Android工程师
《Android构建MVVM》系列(一) 之 MVVM架构快速入门

前言   本文属于《Android构建MVVM》系列开篇,共六个篇章,详见目录树。   该系列文章旨在为Android的开发者入门MVVM架构,掌握其基本开发模式。   辅以讲解Android Architecture Co...

昕无旁骛
08/18
0
0
如何绑定页面生命周期(二)-基于Android Architecture Components的Lifecycle实现

上篇文章如何绑定页面生命周期(一)-Glide实现介绍了Glide实现生命周期感知的原理,这里我们再介绍基于Android Architecture Components的Lifecycle实现页面生命周期感知。 Lifecycle是And...

宇是我
07/29
0
0
使用Kotlin高效地开发Android App(四)

一. 运算符重载 在Kotlin的世界里,我们可以重载算数运算符,包括一元运算符、二元运算符和复合赋值运算符。 使用operator修饰符来修饰函数名的函数,这些函数可以是成员函数也可以是扩展函数...

fengzhizi715
05/25
0
0
Android架构:第一部分-每个新的开始都很艰难 (译)

本系列文章的目标是概述我们与Android应用程序体系结构(Android体系结构)的斗争。 我意识到,无论Android应用程序架构的实施可能会如此痛苦,事实证明,这是我一直在努力的每一个优秀应用程...

SuShine
08/07
0
0
Dev-Guide_Android Basics_What is Android?

我将从这里开始记录一些技术备份,笔记。先从android的docs资料开始。目前主要是翻译,为了怕误人子弟,附带英文原文。如果若有不幸浏览到的童鞋,欢迎你提出意见。 What is Android? Andro...

丁天又
2011/10/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

js 操作cookie

var cookie = {// 设置cookie方法set:function(key, val, time){// 获取当前时间var date = new Date();// 将date设置为n天以后的时间var expiresDays = time;//...

小丶二
10分钟前
0
0
限制root远程登录 su和sudo命令

9月21日任务 3.7 su命令 3.8 sudo命令 3.9 限制root远程登录 对于Linux而言,权限的重要性毋庸置疑!对于普通用户而言无法执行那些只有root用户才能有效的命令,导致工作无法有效进行; 系统...

robertt15
12分钟前
0
0
MQTT协议的初浅认识之通讯级别和持久会话

背景 这是我最近了解MQTT协议的最后一部分内容了,MQTT协议里面的QOS和Keep Alive是两个比较重要的内容。QOS的设置,直接影响了订阅客户端与中间件之间的消息交互行为。而Keep Alive直接影响...

亚林瓜子
14分钟前
1
0
calc

width: calc(100% - 30px); 特别注意:减号左右空格,均不能去掉。 width: calc(100% - 30px);

柴高八斗之父
22分钟前
0
0
Spring Cloud Gateway全局过滤器GlobalFilter:返回消息和重定向

Spring Cloud Gateway的全局过滤器GlobalFilter,顾名思义,声明后会对所有的请求生效,可以用来做权限控制,这里简单记录一下拦截到非法请求后如何返回自定义信息和将请求重定向到指定URL。...

夜雨寄北09
24分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部