Android App 架构设计

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

简介

本文是对谷歌原生文档的翻译,仅供学习参照。

原文链接

此文档写给希望学习最优编程实践和架构以开发健壮、高质量APP的开发者。

开发者常遇到的问题

传统的桌面程序大多数使用场景是有一个启动入口,作为一个独立进程运行。Android app结构要复杂很多,一个典型的Android app由很多组件构成,包括activities,fragment,services,content providers 和broadcast receivers。

App四大组件在Androidmanifest.xml文件里面声明,它们被安卓系统用来决定如何构建App在设备上的交互体验。之前提到,桌面App一般运行在一个独立进程里面,安卓App则不同。安卓设备交互场景经常会遇到多个App之间切换任务,因此安卓App设计上需要灵活一些以满足需求。

举个例子:使用社交App分享一张照片。首先,社交App发intent启动相机App,此时用户已经离开了社交App,但是用户可能并未感知到这个状态。相机App又可能发送intent启动其他的App,比如图片选择器,最终用户返回社交App完成分享照片的动作。在此过程还可能被其他事件中断,比如来电话,用户需要等通话结束以后才可以继续操作分享照片的动作。

Android app组件可以被单独启动,也可以无序启动,并且可能会随时被用户手动或系统销毁。用户无法掌控Android app组件的生命周期,因此不应该在组件里面存储app的数据和状态,组件之间也不应相互依赖耦合。

通用的架构原则

  1. 关注点分离一个常见的错误是将所有代码都写到Activity或者Fragment,这么做不仅会让代码看起来很臃肿,难以阅读和维护,而且容易导致生命周期相关的问题产生。按照谷歌官方的开发推荐,任何不是处理UI和系统的代码都不应该写到这两个类里面。Activity或者Fragment可能会因为一些原因被系统销毁,比如低内存的时候,用户无法掌控。为了使得App更加的稳定可靠,我们应该在开发中最小化对它们的依赖。
  2. Mode驱动UI更新:优选持久化模型。持久化模型有两个好处:(1)当app被系统回收的时候用户不用再担心丢失数据,即使网络不通,app仍然可以继续运行。Modes是一种组件,它用来持有app的数据,它独立于views和app的其他组件。因此,它与app四大组件存在的生命周期问题是隔离的。保持UI代码和逻辑之间的隔离可以使得代码更加容易管理和维护。通过引入Modes类,并给予每一个mode定义好明确的数据映射关系,可以使得app更加方便测试和维护。

推荐的app架构

本节通过一个案例介绍如何使用Architecture Components 来构建App。

说明:理论上,不存在一种万能架构使得app在所有场景下都是最优。因此,本文推荐的架构适用范围有限,如果你已经有不错的架构方式,可以不用更换。

下面我们开始案例,假设需要构建UI来显示用户的简历,简历数据需要通过REST API从后台获取。

构建接口

UI 对应的类UserProfileFragment.java 布局文件是 user_profile_layout.xml.

为了驱动UI,model需要持有两个数据元素

  • The User ID: 用户ID。传递这个数值最好的方式是在fragment的argument里面。因为如果app进程被系统回收,这个数值会被持久化,当app重启的时候还可以获取到这个数据。
  • The User object: A POJO 持有用户数据.

给予ViewModel类构建UserProfileViewModel

ViewModel 用来为UI组件(activity或者fragment)提供数据,并且负责UI与业务数据处理之间的通信。ViewMode不关心UI的变化,例如activity旋转或者重建。

Now we have 3 files.

  • user_profile.xml: UI定义
  • UserProfileViewModel.java: 为UI提供数据
  • UserProfileFragment.java: UI控制器,用来显示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;
    }
}
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);
    }
}

Note: 上面的例子使用 LifecycleFragment 代替 Fragment 。等lifecycles API稳定以后,Support Library里面的Fragment将会更新实现 LifecycleOwner

现在已经有三个模块,如何连接他们?当ViewModel里面用户数据更新以后,需要通知UI来同步显示。这时LiveData登场了。

LiveData 可观察的数据持有者。它允许app组件在不创建与它之间显示和刚性依赖的前提下观察LiveData对象的改变。LiveData遵守app组件的生命周期原则,可以避免内存泄漏。

Note: 如果你正在使用其他的库,例如RxJava 或者 Agera,可以不用替换成LiveData。但是如果你准备使用LiveData,你务必要正确处置生命周期,这样当LifecycleOwner stopped的时候你的数据流也暂停,当LifecycleOwner destroyed的时候你的数据流也destroyed。如果你要在使用LiveData的时候搭配RxJava2等库,可以通过引入 android.arch.lifecycle:reactivestreams

现在我们使用LiveData来替换 UserProfileViewModel里面User的属性,这样当这个值有变化会通知fragment同步更新。LiveData遵守lifecycle原则,当它不在被需要的时候会自动清理引用。

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

Now we modify UserProfileFragment to observe the data and update the UI.

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

每当用户数据更新,onChanged回调方法会被执行用来刷新UI。

如果你熟悉其它类似LiveData功能的库,你会发现我们没有重写fragment的onStop()方法来停止观察数据。使用LiveData无需做这个处理,因为它被设计自动感知Lifecycle,当fragment执行onDestroy()的时候,LiveData会自动删除观察。

We also didn’t do anything special to handle configuration changes (for example, user rotating the screen). The ViewModel is automatically restored when the configuration changes, so as soon as the new fragment comes to life, it will receive the same instance of ViewModel and the callback will be called instantly with the current data. This is the reason why ViewModels should not reference Views directly; they can outlive the View’s lifecycle. SeeThe lifecycle of a ViewModel.

不必针对configuration 的改变做特殊处理(例如activity旋转)。当configuration 变化时,ViewModel 会自动恢复数据。因此,当fragment重新启动,将获取到与configuration 变化之前相同的ViewModels ,并且callback会被马上调用,数据也和之前保持一致。因此,ViewModes不必直接引用Views,他们可以超越View的生命周期。参考The lifecycle of a ViewModel.

获取数据

至此,ViewModel和fragment之间已经建立了联系,那么ViewModel如何获取用户数据的?在本例中,我们假设后台提供的是REST API,我们使用Retrofit 来封装http请求。

下图展示了使用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);
}

ViewMode可以直接调用webservice来从后台获取数据并传递给用户对象,这是最简单的一种实现方式。不过这不是最佳方案,因为随着业务增加,这种架构会比较难扩展和维护。这种架构给予ViewMode过多的责任,因此违背了上文提到的关注分离原则。另外,ViewModel已经跟Activity或者Fragment的生命周期绑定,当UI的生命周期结束时数据丢失是非常不好的体验,因此我们引入了Repository模块,将ViewModel获取数据的工作交于它。

Repository 模块用来处理数据,包括:从哪儿获取数据,当数据变化时调用什么API来更新。它可以被看做是不同数据源之间的调解人,数据来源大致有:持久化数据,webservice,缓存等。

UserRepository 使用 WebService 来获取用户数据

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

respository看起来不是必须的,但是它有一个很重要的优点:抽象了app获取数据的通道,比如在上例中ViewModel并不知道数据源来自Webservice,因此当我们业务需要变更时可以方便修改数据源。

Note: 为了简单起见,我们已经排除了网络错误案例。 暴露错误和加载状态的替代实现请参阅Addendum: exposing network status.

管理组件之间的依赖:

UserRepository获取数据的时候需要一个webservice实例,创建webservice实例并不麻烦,但是需要知道构造webservice时的依赖。这样会稍显复杂并产生冗余代码,因为并不是只有UserrRepository需要webservice的实例,其他类在使用webservice实例的时候都需要知道构建webservice时的依赖。

有两个模式可以用来解决上述问题:

  • 依赖注入: 依赖注入框架允许你定义一个类的依赖,而不必自己去构建这个依赖对象。代码执行期,有专门的类来负责提供依赖对象。我们推荐Android app使用谷歌Dagger 2 框架来实现依赖注入。Dagger 2通过遍历依赖关系树自动构建对象,并在依赖关系上提供编译时保证。
  • 服务定位: 服务定位器提供了一个注册表,其中类可以获取它们的依赖关系而不是构造它们. 它的实现比依赖注入简单很多,如果你对依赖注入不熟悉,可以考虑使用服务定位。

这些模式允许您扩展代码,因为它们提供明确的模式来管理依赖关系,而不会重复代码或增加复杂性。 两者都允许交换实现进行测试; 这是使用它们的主要好处之一。

在本示例中,我们继续使用Dagger 2 来管理依赖关系。

连接 ViewModel 与 repository

我们通过修改 UserProfileViewModel 来使用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对于抽象webservice的请求非常奏效,但是上文示例只有一个数据源,所以可能感觉不是很明显。

UserRepository也有自身的缺陷,如果用户离开了UserProfileFragment,app会重新加载数据。这有两个弊端:

  1. 浪费了网络流量
  2. 重新请求网络数据耗费时间,用户需要等待

为此,我们在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;
    }
}

持久化数据

在当前示例中,如果旋转设备,UI会立即重新显示之前的数据,这是因为我们使用了内存缓存。但是当用户离开app,进程被杀死,然后再次返回app,此时会出现什么情况?

在当前架构中,遇到这种情况需要重新从后台读取数据。这个体验不太好,既耽误时间也浪费流量。为了解决这个问题,可以缓存web请求。但是 如果相同的用户数据从另一种类型的请求显示(例如,获取一个朋友列表)会发生什么情况? 那么您的应用程序可能会显示不一致的数据,这是最令人困惑的用户体验。 例如,相同的用户的数据可能会不同,因为朋友列表请求和用户请求可以在不同的时间执行。 您的应用需要合并,以避免显示不一致的数据。

解决上面问题最好的方法是使用持久化模型。再次谷歌推荐使用Room。

Room 是一个对象映射库,提供本地数据持久性和最小的样板代码。 在编译时,它根据模式验证每个查询,损坏的SQL查询会导致编译时错误,而不是运行时失败。 Room摘录了使用原始SQL表和查询的一些基本实现细节。 它还允许观察数据库数据(包括集合和连接查询)的更改,通过LiveData对象公开这些更改。 另外,它明确地定义了线程约束,解决常见问题,如访问主线程上的存储。

Note: 如果您熟悉SQLite ORM或Realm等不同数据库的其他持久性解决方案,则无需将其替换为Room,除非Room的功能集与您的用例更相关。

要使用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是一个抽象类,Room自动提供一个它的实现类。参考文档Room

现在我们需要通过一种方式来向数据库插入用户数据,为此我们先新建一个data access object (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。 Room知道数据库何时被修改,当数据发生变化时,它会自动通知所有的主动观察者。使用LiveData,只会在至少有一个主动观察者时更新数据。

Note: 从alpha 1版本开始,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中数据来源的位置,我们也不需要更改UserProfileViewModel或UserProfileFragment。 这是抽象提供的灵活性。 这也非常适合测试,因为您可以在测试UserProfileViewModel时提供假的UserRepository。

现在我们的代码实现已经比较完整了。 如果用户日后再回到同一个用户界面,他们会立即看到用户信息,因为我们已经实现了持久化。 同时,如果数据过期,我们的存储库将在后台更新数据。 当然,根据您的用例,如果持久数据太旧,您可能不希望显示持久化的数据。

在一些使用情况下,例如下拉刷新,当有网络操作的时候UI也应该照常显示用户数据。UI与数据分离是很好的做法,因为改变UI的原因可能有很多。

有两种方法来解决这种情况遇到的问题:

  • 改变getUser的实现,返回一个LiveData数据,包含网络操作的状态。这里有一个参考示例Addendum: exposing network status
  • 在repository类中新增一个public方法,返回用户对象最新的状态。如果希望通过在UI上显示网络状态来响应用户动作(例如下拉刷新),那么此方法更好。
唯一的可靠数据源

不同REST API返回相同数据也很正常,例如:如果后台有另外一个请求接口返回一个朋友列表,同样的用户对象可能会来自两个不同的请求接口。通过webservice获取数据,当后台数据在在多次请求之间发生变化时,用户得到的数据可能会出现不一致的现象。因此,在UserRepository实现中web service的回调只是将数据存储到数据库,然后数据库发生改变会触发生成一个激活的LiveData对象。

在这个模型中,数据库是唯一可靠的数据来源,app其他组件通过repository访问数据库。无论是否使用磁盘缓存,我们推荐repository来为app设计唯一一个可靠的数据源。

测试

上面提到,关注分离带来的一个好处是方便测试。来看下如何测试每一个模块

  • User Interface & Interactions: 这是唯一需要Android UI Instrumentation测试的。 测试UI代码的最好方法是创建一个Espresso测试。 您可以创建该fragment并为其提供一个模拟的ViewModel。 由于fragment只与ViewModel进行通信,所以模拟它将足以完全测试UI。

  • ViewModel: ViewModel 可以使用 JUnit test测试.

  • UserRepository: 您也可以使用JUnit测试来测试UserRepository。 您需要模拟Webservice和DAO。 您可以测试它是否进行正确的Web服务调用,将结果保存到数据库中,如果数据被缓存并且是最新的,则不会发生任何不必要的请求。 既然Webservice和UserDao都是接口,那么你可以模拟它们,或为更复杂的测试用例伪造一个实现。

  • UserDao: 测试DAO类的推荐方法是使用仪器测试。 由于这些仪器测试不需要任何UI,因此它们可以快速运行。 对于每个测试,可以创建一个内存数据库,以确保测试没有任何副作用(如更改磁盘上的数据库文件)。

    Room 还允许指定数据库实现,以便您可以通过向其提供支持SQLiteOpenHelper的JUnit实现来测试它。 通常不推荐使用此方法,因为在设备上运行的SQLite版本可能与主机上的SQLite版本不同。

  • Webservice: 保证测试与外界的独立性很重要,即使是webservice测试也应该避免向后台发送网络请求。有很多的库可以帮助来实现这个需求,例如MockWebServer 可以伪造一个本地服务器来用于测试。

  • Testing Artifacts架构组件提供了一个maven工件来控制其后台线程。 在android.arch.core中:核心测试工件,有2个JUnit规则:

    • InstantTaskExecutorRule: 此规则可用于强制架构组件立即执行调用线程上的任何后台操作。
    • CountingTaskExecutorRule: 该规则可用于仪器测试,以等待架构组件的后台操作或将其连接到Espresso作为闲置资源。

最终的架构

下图展示了谷歌推荐的架构包含的所有模块,以及模块之间如何交互。

img

指导原则


编程是一项创造性工作,开发Android应用程序也不例外。 无论是在多个activity或fragment之间传递数据,检索远程数据并将其在本地保持离线模式,还是任何其他场景,都有多种方法来解决问题,

谷歌推荐,遵循这些建议将使您的代码库从长远来看更加强大,可测试和可维护。

  • 安卓四大组件不应当被用作数据源
  • 关注分离,为应用程序的各个模块之间创建明确的责任界限
  • 模块内部高内聚,尽量少的暴露每个模块的是实现细节
  • 模块之间低耦合
  • 不重复造轮子,将开发精力聚焦在自己app独一无二的特性上
  • 持久化数据,这样用户离线状态也可以使用
  • 为repository设计使用唯一的数据源 Single source of truth.

附录:暴露网络状态


在上面推荐的应用程序体系结构部分,我们故意省略网络错误和加载状态,以保持样本简单。 在本节中,我们演示了一种使用Resource类公开网络状态来封装数据及其状态的方法。

以下是一个示例实现:

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

因为在从磁盘中显示它的时候加载数据是一个常见的用例,所以我们要创建一个帮助类,可以在多个地方重复使用NetworkBoundResourcethat。 以下是NetworkBoundResource的决策树:

img

请求从监听数据库开始,当第一次从数据库加载数据,NetworkBoundResource会检查数据是否有效,如果有效则分发出去,否则开始从网络获取数据。注意,这两个动作可以同时发生,例如你在发送网络请求的时候可能想先展示数据库中的缓存数据,等网络请求完成再用来更新数据内容。

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

以下是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将我们的磁盘和网络绑定用户实现写入存储库。

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();
    }
}
你们的魔鬼又来咯!Android-App的设计架构:MVC,MVP,MVVM与架构经验谈,看完泪流满面 !最后放上一个大概的Android学习方向及思路(详细的内容太多了~),提供给大家:对于程序员来说,要学习的知识内容、技术有太多太多,这里就先放上一部分,其他的内容有机会在后面的文章向大家呈现出来,不过我自己所有的学习资料都整理成了一个文档,一直在不断学习,希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!Android架构师之路很漫长,一起共勉吧! 阅读详情

相关推荐

手把手教你写Android项目文档,专题解析

去年无疑是 Flutter 技术如火如荼发展的一年。 每一个移动开发者都在为 Flutter 带来的“快速开发、富有表现力和灵活的 UI、原生性能”的特色和理念而痴狂,从超级 App 到独立应用,从纯 Flutter 到混合栈,开发者们在不同的场景下乐此不疲的探索和应用着 Flutter 技术,也在面临着各种各样不同的挑战。 Alibaba集团内也有越来越多的业务和团队开始尝试 Flutter 技术栈,从闲鱼的一支独秀引领潮流,到如今淘宝特价版、盒马、优酷、飞猪等BU业务相继入局,Flutter的业务应用在

505

Android App架构设计

一、 Android系统设计概要图Android端采用MVP架构模式,通过Http方式与服务器进行通信,采用json作为数据传输格式。如下图所示MVP架构优点:业务跟界面完全分离,代码变得简洁、移植性高、方便单元测试、避免内存泄漏Model层又细分为:网络层、蓝牙层、数据层、、UI层、定位层、日志层、更新层。

chengzhenjia的博客 1056

Android架构设计——MVC

对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的最后,互联网不存在所谓的寒冬,只是你没有努力罢了!《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!我们!**当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

2401_84009993的博客 901

Android App 架构设计相关资料汇总

只要有1,2年工作经验的程序员,多多少少都会接触到架构东西。可能平时工作中不一定会有机会从0到1完完全全自己去设计一套架构出来,但是如果想成为高级工程师,技术专家,架构师……尽早接触架构方面的知识是有利无害的。我收集了很多材料,现在汇总在这里,方便查阅。

Fantasy 2563

Android app架构设计

Peter的专栏 1086

浅谈Android应用开发中的app架构设计

  作为2017年的首篇博客,定然要来点全局性的料,不过作为一名从事Android开发稍有经验却又不敢称大的开发者来讲,又必须要说出个子丑寅卯来,所以定了这个标题。本篇简单介绍一下个人对app架构的理解,如有纰漏,望留言指正交流。   首先,APP进行架构设计的目的何在?   在我刚开始写APP时,只是注重界面设计和逻辑跳转,至于如何节省时间的敲代码没有太多的了解。(当然这也练就了一身的手速)...

weixin_34129696的博客 171

android webview,app架构设计

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

m0_61331407的博客 1035

【架构篇】Android移动app架构设计浅谈

前言 架构,又名软件架构,是有关软件整体结构与组件的抽象描述,用于指导大型软件系统各个方面的设计。 软件架构设计目标: 1.可靠性(Reliable)。软件架构的可靠是产品设计的前提。 2.安全性(Secure)。软件架构的安全性是产品可持续发展的条件。 3.可扩展性(Scalable)。软件架构必须能够不同的功能需求情况下,支持可扩散性。 4.可定制化(Customi

梦想启航者 5091

android app 架构设计_清爽不花哨,这款 Android 应用让记账变得轻松又高效|安卓|bluecoins...

  如今谁还不是手握多张信用卡,一不注意就弄得生活费超支,这个月花下个月的钱。不合理的消费让很多人变成了「负翁」。  为了理性消费,不少朋友选择了记账,手机 app 记账便成为了年轻人的首选。  对 iOS 用户来说,不错的记账 app 比比皆是,但对 Android 用户而言却少有趁手的,毕竟不少好用的记账 app 都属于 iOS 独占。  那么有没有这样一款适用于 Android 系统,且界面...

weixin_39617685的博客 267

android app 架构设计_这些冷门的App,好用到为你打开新世界大门

一些好用的App总被埋没在数以百万计的应用商店中。今天为博应用(BYYJP)大家推荐几款Windows、Android、iOS、macOS平台里略显小众、但足够好用的遗珠App。万彩办公大师(Windows)转换Office文档格式、批量处理PDF、美化编辑图片……日常工作生活中,总有一些需求迫使我们安装一堆工具App。而万彩办公大师则整合了PDF标注编辑、音视频格式转换、图片编辑处理、录屏演示等...

weixin_39605905的博客 193

android app架构设计mvc

对于应用代码数在10万以上的情况,我们需要考虑架构设计架构设计的有点可以使得程序模块化(分工协同开发),模块内部高内聚,模块之间低耦合;提高开发效率,更容易后期测试和定位     Android中mvc架构设计模式:              V:视图层(View)              一般采用xml文件来进行数据的展示,这些xml就可以理解为android app的View

罗春娥的技术园 507

android app 架构设计_你竟然还没用过这些APP

那些小而美的APP们我们每天的日常生活,都离不开各式各样的APP,帮助我们购物,社交,娱乐。除了日常大众普遍使用的APP外,小编精心挑选了7个小众精品APP,爱玩的你一定要试试。工具类一个木涵官方自称为安卓端的瑞士军刀,是一个集成了多种工具的APP。从快递查询,单位换算到instagram中照片的获取,wifi密码查看,手机大文件清理,各类的工具都能找到。同时具有小程序功能,可以直接在...

weixin_39890452的博客 147

Android开源语音助手App开发实战:从架构设计到性能优化

基于火山引擎豆包大模型,从零搭建一个实时语音通话应用。它不是简单的问答,而是需要你亲手打通 ASR(语音识别)→ LLM(大脑思考)→ TTS(语音合成)的完整 WebSocket 链路。对于想要掌握 AI 原生应用架构的同学来说,这是个绝佳的练手项目。架构理解:掌握实时语音应用的完整技术链路(ASR→LLM→TTS)技能提升:学会申请、配置与调用火山引擎AI服务定制能力:通过代码修改自定义角色性格与音色,实现“从使用到创造”从0到1构建生产级别应用,脱离Demo,点击打开。

2600_94959960的博客 616

Android APP架构设计——MVP的使用示例

0. 前言为了更好地进行移动端架构设计,我们最常用的就是MVC、MVP和MVVM,作为三个最耳熟能详的三大架构,应用可谓非常广泛。对于这三种架构设计以及优缺点已经在Android APP架构设计——MVC、MVP和MVVM介绍一文中介绍过了,本文是对前面那篇文章2.3小节的补充,介绍MVP模式在Android中的使用示例,目的在于深化对MVP架构的理解。...

关于Android开发的一些技术点总结 ╮( ̄▽ ̄”)╭ 1万+

Android APP架构设计——MVC、MVP和MVVM介绍

0. 前言为了更好地进行移动端架构设计,我们最常用的就是MVC、MVP和MVVM,作为三个最耳熟能详的三大架构,应用可谓非常广泛。本文原创,转载请注明出处为SEU_Calvin的博客。本篇博客将介绍这三种架构设计的工作原理以及优缺点,以及它们在Android中的表现。1. MVC1.1MVC工作原理MVC是软件架构中最常见的一种框架,三个字母分别代表三个模块:Model、View和C...

关于Android开发的一些技术点总结 ╮( ̄▽ ̄”)╭ 1万+

车载安卓APP开发工程师职位深度解析与面试指南

摘要:赛科工业科技开发(武汉)有限公司上海分公司招聘车载安卓APP开发工程师(技术驻场岗位),工作地点在南京延锋总部。岗位要求精通Java/Kotlin开发,熟悉Android Automotive OS(AAOS)及车机互联协议(CarPlay/HiCar等),具备车载硬件适配及车规级应用开发经验。主要职责包括需求分析、APP架构设计、核心功能开发(导航/娱乐/车控)、性能优化及多屏联动适配。需掌握车载场景下的交互规范,能进行高低温等极端环境测试。要求3年以上车载安卓开发经验,熟悉MVVM架构及协程编程,

郑伟强dev的专栏 176
上一篇: Android图片系列-2.Android App图片压缩、裁剪分析整理
下一篇: HTTP 常见问题总结
rocky-bull
博客等级 码龄17年 70粉丝 79原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值