爱收集资源网

Android开发者的简单产品级app建构最佳实践

网络 2023-06-29 17:05

该手册针的目标人群是早已晓得怎样建构简单的app,而且希望了解建立强壮的产品级app的最佳实践和推荐构架。

app开发者面临的困局

不同于大部份的传统桌面应用只有一个入口,但是作为一个整体的进程运行,Androidapp有愈加复杂的结构。一个典型的app由多种组件构成,包括activity,fragment,service,contentprovider和broadcastreceiver。

几乎所有的组件都在app清单上面进行申明,这样Android系统就才能决定怎样将该app整合到整体的用户体验中来。一个优良的app须要在用户的工作流或则任务切换中应对自如。

当你想要在社交网路app中分享一张合照时会发生哪些?app触发了一个拍照机intent,之后系统启动单反app来响应。这个时侯用户离开了社交网路app,但是体验没有被打断。单反app也可能触发其他的intent,例如打开了一个文件选择器。最终用户返回社交app完成了相片分享。其实分享的中途可能会进来一个电话打断了这个过程,但是在通话结束后还是继续。

Android中应用跳转是很常见的,因此你的app要能正确应对。仍然要记住的是,联通设备是资源有限的,因此操作系统可能在任何时侯杀害一些app便于给其他app挪位子。

这儿的要点是app中的组件可能被单独启动而且没有固定次序,还可能在任何时侯被用户或则系统销毁。由于app组件是朝生暮死的,它们的生命周期不受你的控制,为此你不应当把数据或则状态存到app组件上面而且组件之间不应当互相依赖。

构架的通常原则

假如app组件不能拿来储存数据和状态,这么app应当怎样组织?

首要的一点就是要做到分离关注点(separationofconcerns)。一个常见的错误做法是将所有的代码都讲到Activity或则Fragment中。任何与UI无关的或则不与系统交互的代码都不应当放在这种类上面。尽量保持这一层代码很薄能防止好多跟生命周期相关的问题。请记住你并不拥有这种类,它们仅仅是系统和你的app沟通的胶带类(glueclass)。系统按照用户交互或则低显存的情况在任意时刻销毁它们。因而坚实的用户体验绝不应当依赖于此。

其次你应当使用model来驱动UI,最好是持久化的model。首推持久化的缘由有二,app在任何时侯都不会遗失用户数据虽然是在系统销毁app释放资源的时侯,但是app在网路状况不佳的时侯也能工作正常。model是app上面负责处理数据的组件,与app的view和其他组件独立,因此不受这种组件生命周期的影响。保持UI的代码简单而且不包含应用逻辑促使代码愈发容易管理。基于定义良好的model来创建app,这样促使app更具有可测性和一致性。

推荐的app构架

接出来我们将通过一个用例来展示怎样使用构架组件来组织一个app。

假定我们要创建一个UI拿来展示用户信息,用户信息通过RESTAPI从后台获取。

创建用户界面

UI由UserProfileFragment.java和布局文件user_profile.xml构成。

要驱动UI,我们的数据model要持有两个数据元素。

-用户ID:用户的标示符。最好是使用fragment参数传递该信息。假如系统销毁了进程,该信息会被保留,当app重启后再度可用。

-用户对象:一个持有用户数据的POJO。

我们将基于ViewModel类创建一个类UserProfileViewModel用于保持这种信息。

一个ViewModel用于给特定的UI组件提供数据,比如fragment或则activity,而且负责和数据处理的业务逻辑部份沟通,如通知其它组件加载数据或则转发用户更改。ViewModel并不晓得特定的View而且也不受配置修改的影响,例如旋转设备造成的activity重启。

如今我们有三个文件。

-user_profile.xml

-UserProfileViewModel.java

-UserProfileFragment.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  );
    }
}  

注意:上例中继承了LifecycleFragment而不是Fragment类,当构架组件中的lifecycleAPI稳定后,Android支持库中的Fragment将会实现LifecycleOwner。

怎么联接三个代码模块呢?虽然当ViewModel的user域的数据打算妥当后,我们须要一种方法来通知UI。这就轮到LiveData类出场了。

LiveData是可观测数据(observabledata)的持有者。app中的组件订阅LiveData的状态变化,不须要显示的定义依赖。LiveData同样能应付app组件(activity,fragment,service)的生命周期因而杜绝显存泄露。

注意:假如你已然使用了RxJava或则Agera这样的库,可以继续作为LiveData的取代使用。假如使用这种取代库,请确保在相关LifecycleOwner停止的时侯暂停数据流但是在LifecycleOwner被销毁时销毁数据流。你同样可以通过添加android.arch.lifecycle:reactivestreams依赖将LiveData和其他的响应式流库配合使用(如RxJava2)。

如今将UserProfileViewModel中的User域替换成LiveData,这样fragment能够在数据变化时收到通知。LiveData的用处是它能应对生命周期的变化,在引用不再使用时手动清除。

                    public    class   UserProfileViewModel   extends   ViewModel  {  ...  private  User user;  private  LiveData user;  public  LiveData  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不应当直接引用View的缘由,它们不受View生命周期的影响。

获取数据

如今早已将ViewModel联接到了fragment,之后ViewModel从那里获取数据呢?本例中,我们假定后台提供一个RESTAPI,我们使用Retrofit库来访问后台。

下边是retrofit拿来和后台沟通的Webservice插口:

                    public    interface   Webservice  {   @GET  (  "/users/{user}"  )
    Call getUser(  @Path  (  "user"  ) String userId);
}  

简单的做法就是ViewModel直接通过Webservice获取数据之后形参给user对象。这样做其实没错,但是app会显得不容易维护,因而这违背了单一职责原则。除此之外,ViewModel的作用域紧密联系到了activity或则fragment生命周期,生命周期结束后数据也丢掉虽然不是好的用户体验。为此,我们的ViewModel会将这项工作委托给一个新的模块Repository。

Repository模块负责处理数据操作。它向app提供了简约的API。它们负责到各类数据源获取数据(持久化数据,web服务,缓存等)。

                    public    class   UserRepository  {   private  Webservice webservice;  // ...   public  LiveData  getUser  (  int  userId) {  // This is not an optimal implementation, we'll fix it below   final  MutableLiveData data =  new  MutableLiveData<>();
        webservice.getUser(userId).enqueue(  new  Callback() {  @Override   public   void   onResponse  (Call call, Response response) {  // error case is left out for brevity  data.setValue(response.body());
            }
        });  return  data;
    }
}  

repository模块看似没有必要,但是它给app提供了各类数据源的具象层。现今ViewModel并不晓得数据是来自Webservice,也就意味着这种实现在必要时可以替换。

管理各组件之间的依赖

UserRepository类须要一个Webservice实例来完成工作。可以直接创建,但是Webservice也有依赖。并且UserRepository可能不是惟一使用Webservice的类,假如每位类都创建一个Webservice,这么就导致了代码重复。

有两种形式可以解决这个问题:

-依赖注入:依赖注入可以促使类申明而不创建依赖。在运行时,另外的一个类负责提供这种以来。我们推荐使用Dagger2来完成依赖注入。Dagger2在编译时审查依赖树因而手动创建依赖。

-服务定位:服务定位提供了一个注册点,须要任何依赖都可以索要。实现上去比依赖注入简单,因此假如不熟悉DI,可以使用servicelocator取代。

这种模式都对代码的扩充提供了便利,既对依赖进行了清晰的管理又没有引入重复代码和额外的复杂度。还有另外一项用处就是易于测试。

在本例中,我们使用Dagger2进行依赖管理。

联接ViewModel和repository

                    public    class   UserProfileViewModel   extends   ViewModel  {   private  LiveData 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  getUser  () {  return   this  .user;
    }
}  

缓存数据

里面的repository实现仅仅具象了webservice的调用,依赖单一的数据源,因此不是很实用。

问题在于UserRepository获取了数据并且没有暂存上去,这样当用户离开UserProfileFragment再回到该界面时,app要重新获取数据。这样做有两个缺点:浪费了网路带宽和用户时间。为了解决这个问题,我们给UserRepository添加了另一个数据源拿来在显存中缓存User对象。

                    @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  getUser  (String userId) {
        LiveData cached = userCache.get(userId);  if  (cached !=  null  ) {  return  cached;
        }  final  MutableLiveData 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() {  @Override   public   void   onResponse  (Call call, Response response) {
                data.setValue(response.body());
            }
        });  return  data;
    }
}  

持久化数据

在当前的实现中,假如用户旋转了屏幕或则暂停并再度步入app,因为有缓存的存在,UI会立刻渲染出数据。但是当用户离开app后几个小时系统杀害了该进程,这个时侯用户返回到app会发生哪些呢?

根据当前的实现,app会再度从网路上获取数据。这不仅仅是糟糕的用户体验,并且也浪费联通数据流量。最合适的做法就是持久化model。如今就轮到Room持久化库上场了。

Room是一个对象映射库,提供本地数据的持久化。可以在编译期检测sql句子。准许将数据库数据的变化通过LiveData对象的方式曝露出去,并且对数据库的访问做了线程限制(不能再主线程访问)。

要使用Room,首先定义schema。将User类通过@Entity注解成数据库中的表

                    @Entity  class User {  @PrimaryKey   private   int  id;  private  String name;  private  String lastName;  // getters and setters for fields  }  

之后创建一个数据库类

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

请注意MyDatabase是具象类,Room会手动提供实现。

如今我们要将用户数据插入到数据库。创建一个dataacessobject(DAO)。

                    @Dao   public    interface   UserDao  {   @Insert  (onConflict = REPLACE)  void  save(User user);  @Query  (  "SELECT * FROM user WHERE id = :userId"  )
    LiveData load(String userId);
}  

之后在数据库类中引用DAO

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

请注意,load方式返回了一个LiveData。在数据库有数据变化时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  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,用户信息也是即刻呈现的,由于我们早已做了持久化。与此同时,repository也会在后台更新数据。其实按照应用场景,太旧的数据你可能不会选择呈现。

在一些应用场景中,如下拉刷新,假如有网路操作正在进行,这么提示用户是很有必要的。将UI操作和实际的数据分开是有用处的,虽然数据可能由于多种缘由更新。

有两种方式处理这些状况:

-修改getUser方式返回一个包含网路操作状态的LiveData,详情见附表。

-在repository中提供一个公共方式返回User的刷新状态。假如仅仅只想在用户的显示动作(如下拉刷新)后显示状态,这么这些方法更好。

真理的惟一持有者(Singlesourceoftruth)

不同的RESTAPI端点返回相同的数据是很常见的。诸如,假如后台有另一个端点返回一个同学列表,这么同一个用户对象可能来自两个不同的API端点,仅仅是细度不同而已。假如UserRepository将Webservice的恳求结果直接返回这么UI上呈现的数据可能没有一致性,虽然来自后台的数据可能在前后两次恳求时不同。这也就是为什么在UserRepository的实现中,webservice的反弹仅仅将数据存到数据库中。之后数据库的变化会反弹LiveData对象的活动订阅者。

在这个模型中,数据库充当了真理的惟一持有者,app的其他部份通过repository访问它。

测试

之前我们提过,分离关注点的一个用处就是可测性。让我们分别瞧瞧怎样测试各个模块。

-用户插口和交互:这是惟一须要AndroidUIInstrumentationtest的地方。测试UI的最好方法就是创建一个Espresso测试。你可以创建一个fragment之后提供一个模拟的ViewModel。虽然fragment仅仅与ViewModel交互,模拟ViewModel才能完整的测试该UI。

-ViewModel:ViewModel可以使用Junit。仅仅须要模拟UserRepository。

-UserRepository:同样使用JUnit进行测试。你须要模拟Webservice和DAO。你可以测试是否进行了正确的webservice恳求,之后将恳求结果存入数据库而且在本地数据有缓存的情况下不进行多余的恳求。由于Webservice和UserDao都是插口,你可以随便模拟它们进行更复杂的测试用例。

-UserDao:推荐使用instrumentation测试。由于这种instrumentation测试不须要UI,所有会运行得很快。对于每位测试你都可以创建一个显存中的数据库。

-Webservice:测试应当与外界独立,因而Webservice的测试也不应当恳求实际的后台。诸如,MockWebServer库可以用于模拟一个本地服务器便于进行测试。

-测试套件:构架组件提供了两个JUnit规则(InstantTaskExecutorRule和CountingTaskExecutorRule),都包含在android.arch.core:core-testing这个mavenartifact中。

最终的构架图示

附表:曝露网路状态

                    //a generic class that describes a data with a status   public   class  Resource {
    @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   Resource  success  (@NonNull T data) {  return   new  Resource<>(SUCCESS, data,  null  );
    }  public   static   Resource  error  (String msg, @Nullable T data) {  return   new  Resource<>(ERROR, data, msg);
    }  public   static   Resource  loading  (@Nullable T data) {  return   new  Resource<>(LOADING, data,  null  );
    }
}  

                    // ResultType: Type for the Resource data   // RequestType: Type for the API response   public   abstract   class  NetworkBoundResource {  private  final MediatorLiveData> result =  new  MediatorLiveData<>();
    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(  null  ));
        LiveData 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 dbSource) {
        LiveData> 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 response) {  new  AsyncTask() {
            @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();
    }  // 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  loadFromDb  ();  // Called to create the API call.  @NonNull @MainThread  protected   abstract  LiveData>  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>  getAsLiveData  () {  return  result;
    }
}  

最终的UserRepository是这样的:

                   class UserRepository {
    Webservice webservice;
    UserDao userDao;  public  LiveData>  loadUser  (  final  String userId) {  return   new  NetworkBoundResource() {  @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  loadFromDb  () {  return  userDao.load(userId);
            }  @NonNull   @Override   protected  LiveData>  createCall  () {  return  webservice.getUser(userId);
            }
        }.getAsLiveData();
    }
}  

android 获取网络
上一篇:新功能上线!企业微信红包定制封面! 下一篇:没有了
相关文章