美文网首页
Android官方 App 架构指南

Android官方 App 架构指南

作者: mao眼 | 来源:发表于2017-05-27 15:50 被阅读699次

    2017年的Google IO 上,Google 官方推出了一套架构组件类库,并给出了一份 App 架构指南。这个组件的主要目的就是帮助开发者方面处理Android应用组件生命周期的问题,并提供了一个SQlite 对象映射类库。利用这些组件,可以构建一个 APP 基本的架构骨架。

    长期以来,Android应用层架构的选择没有约束,MVC、MVP、MVVM等各种架构方式都有开发者使用,Google 官方也没有推荐的架构模型和应用指南。开发者在处理Activity、Fragment、Service的生命周期时,需要处理很多资源回收或者状态、数据持久化的问题,而这些case在项目中也没有通用的解决方式。

    通用架构准则

    第一准则是『关注点隔离(Separation of concerns)』,就是依赖解耦。最常见的错误是把所有代码都写在Activity或者Fragment里。任何与操作UI和系统交互无关的代码,都不应该在Activity或Fragment类里。

    第二条准则:通过Model驱动UI,最好是可持久化的Model。

    持久化有两个原因:

    • 当系统由于资源紧张回收掉你的应用时,或者有网络连接等问题时,用户不会丢失数据。
    • Model 是负责处理应用程序数据的组件,独立于UIView和应用程序组件,因此是和应用程序组件的生命周期隔离的。这样,UI代码保持简洁,逻辑更易管理。

    推荐 APP 架构

    Google 通过新引入的架构组件(Architecture Components)演示构建一个App。

    构建用户界面

    一个显示个人信息的界面,用户数据通过一个 REST API 获取。

    UI 由一个Fragment UserProfileFragment.java 文件和 layout user_profile_layout.xml 组成。

    为了驱动UI,DataModel 需要持有两种数据元素:

    • User ID:用户标识id,通过Fragment的参数传入Fragment,当应用被系统回收时,会被保存,下次应用重启时,id 仍是可用的。
    • User object:持有User数据的POJO。

    为此我们创建一个 UserProfileViewModel(继承自 ViewModel类)。

    ViewModel 为特定的UI组件(Activity、Fragment)提供数据,也会处理业务数据的交互,诸如调用其他组件加载数据,转发用户的修改。ViewModel并不了解View,也不会受到 cofiguration changes的影响(例如Activity 横竖屏切换)。

    3个文件:

    • user_profile_layout.xml
    • UserProfileFragment.java
    • 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;
        }
    }
    
      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 和 Fragment 类,怎么联系在一起呢?当ViewModel中的user属性设置或者修改时,我们需要用一种方式通知UI。此时,LiveData就派上用场了。

    LiveData是一个可被观察的数据持有者(观察者模式)。可以让 Activity, Fragment 等应用程序组件对其进行观察,并且不会在它们之间创建强依赖,LiveData 还能够自动响应各组件的声明周期事件,防止内存泄漏,从而使应用程序不会消耗更多的内存。

    使用LiveData修改ViewModel类:

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

    修改UserProfileFragment类,使其能够观察user和更新UI:

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

    每次数据变化时,onChanged 回调会被调用,更新UI。

    获取数据

    使用Retrofit库,访问后端的User REST API。

      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获取数据,但是这样违反了依赖解耦的第一原则。

    我们引入一个Repository模块管理数据。Repository 模块负责数据处理,为应用的其他部分提供干净可靠的 API。你可以将其考虑为不同数据源(Web,缓存或数据库)和应用之间的中间层。

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

    管理组件之间的依赖

    UserRepository中需要一个WebService实例进行数据获取的工作,不要直接new一个WebService对象,这很容易产生重复代码,并且使复杂度上升。另外,UserRepository可能并不是唯一一个需要WebService的类,如果每个类都创建一个WebService,也会造成资源浪费。

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

    • 依赖注入(Dependency Injection):推荐Dagger2
    • 服务定位期模式(Service Locator):通过注册表的方式获取某个class 的依赖,而不是新构建一个。使用比DI简单。

    这里我们使用Dagger2管理依赖。

    连接ViewModel 和 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不止有一个数据源,我们在此加入一个缓存。

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

    数据持久化

    UserRepository中有一个web数据源,一个内存缓存,当用户杀死进程后,重新启动时,会再次从网络获取数据,这可能不是一个好的体验,因此我们还需要能够持久化。

    我们使用 Room 这个组件来完成持久化的任务。

    Room是一个 SQLite object 映射类库,可以方便进行数据持久化。Room 抽象出了很多常用的数据库操作,并且在编译时会验证每个查询,从而损坏的 SQL 查询只会导致编译时错误,而不是运行时崩溃。还能和LiveData 一起工作,并帮开发者处理了很多线程问题。

    要使用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 类是abstract的,Room会自动添加实现。

    现在,我们需要把数据插入数据库,创建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 会知道什么时候数据库发生了变化,并自动通知所有的观察者。

    修改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 中的数据源,我们也完全不需要修改 ViewModelFragment,这就是抽象的灵活性。同时还非常易于测试,我们可以在测试 UserProfileViewModel 时提供测试用的 UserRepository。

    至此,我们的代码完成了。当用户几天后回到相同的UI页面时,也会马上看到用户信息,因我我们已经把数据持久化了。同时,Repository也会在后台更新数据。当然UI层也会根据需求决定是否显示过期的数据。

    在一些场景下,例如下拉刷新,如果正在有网络请求时UI显示用户信息是很重要的。分离UI操作和实际数据是很有用处的,因为数据可以有各种原因触发更新。

    测试

    • UI:使用 Espresso
    • ViewModel:JUnit,需要mock UserRepository
    • UserRepository:JUnit,需要mock WebService和DAO
    • UserDao:推荐使用instrumenttation测试,Room可以允许指定数据库实现,因此可以提供一个JUnit实现的SupportSQLiteOpenHelper
    • WebServiceMockWebServer

    最终架构

    指导原则

    • 在 manifest中定义的组件(activity、service、broadcast receiver等),都不是数据源。因为每个组件的生命周期都很短,而且取决于当前用户和设备的交互和系统的运行状况。这些都不能作为应用的数据源。
    • 确保 APP 中的模块职责清晰,建立明确的责任边界。简单来说,不要把不相关的功能职责放到同一个类里实现(例如数据缓存和数据binding)。
    • 模块尽量少暴露内部实现。不要为了一时方便而把不应暴露的实现暴露出去,在后期这会导致越来越多的技术债。
    • 在定义模块直接的交互时,考虑如何将每个模块尽量隔离。通过良好设计的API进行交互。例如,把网络和数据库的数据逻辑放在一起也能使代码正常工作,但是,这很难测试。
    • 你应用的核心是那些能脱颖而出的东西,不要浪费时间重复造轮子或一次次编写同样的模板代码。把精力集中在使你的应用独一无二上,把一些重复性的工作交给Android Architecture Components和其他优秀的第三方库。
    • 尽可能的持久化,使应用能够在脱机模式下可用。
    • Repository应该指定一个数据源作为单一的信任数据源。

    原文:https://developer.android.com/topic/libraries/architecture/guide.html

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

    相关文章

      网友评论

          本文标题:Android官方 App 架构指南

          本文链接:https://www.haomeiwen.com/subject/xjyqfxtx.html