【问题标题】:MVVM in android,accessing assetManager without breaking the patternandroid中的MVVM,在不破坏模式的情况下访问assetManager
【发布时间】:2019-06-27 10:36:18
【问题描述】:

我在资产文件夹中有一个 JSON 文件,DataManager(repository) 类需要它,因此资产管理器(和上下文)应该可以访问资产。

问题在于,基于最佳实践,Android 上下文或 android 特定代码不应传递到数据层(ViewModel-Repo-Model),因为容易编写单元测试等,并且视图不应与数据交互直接分层。

我最终提供了使用列表并将其注入存储库。

这是正确的做法吗?

-谢谢

P.S: 我的 Module 类提供了列表

@Module
public class UtilModule {

    @Provides
    @JsonScope
    JsonUtil provideJsonUtil(AssetManager assetManager){
        return new JsonUtil(assetManager);
    }

    @Provides
    @JsonScope
    String provideJson(JsonUtil util){
        return util.getJson();
    }

    @Provides
    @JsonScope
    Type provideType(){
        return new TypeToken<List<Data>>() {}.getType();
    }
    @Provides
    @JsonScope
    DataManager provideDataManager (Gson gson, Type type,String json) {
        return new DataManager (gson.fromJson(json, type));
    }
}

【问题讨论】:

    标签: android android-assets android-mvvm


    【解决方案1】:

    ViewModel 和/或Repository 直接访问Application 上下文并不违反MVVM,这是访问AssetsManager 所需的全部内容。调用Application.getAssets() 是可以的,因为ViewModel 不使用任何特定的Activity 的上下文。

    例如,您可以使用 Google 提供的 AndroidViewModel 子类,而不是超类 ViewModelAndroidViewModel 在其构造函数中采用 ApplicationViewModelProviders 将为您注入它)。您可以在其构造函数中将您的 Application 传递给您的 Repository

    或者,您可以使用Dagger 依赖注入将Application 直接注入您的Repository。 (注入Application 上下文有点棘手。请参阅Dagger 2 injecting Android Contextthis issue filed on the Danger github repo。)如果你想让它变得非常流畅,你可以为AssetManager 配置一个提供程序并将其直接注入你的Repository

    最后,如果您正在使用 Room,并且您只想使用存储在资产中的预配置数据库预填充 Room 数据库,您可以按照此处的说明进行操作:How to use Room Persistence Library with pre-populated database?

    【讨论】:

      【解决方案2】:

      由于您是第一次使用 MVVM,我们可以尽量保持简单。

      [ View 组件 C] ---- (observes) [ ViewModel 组件 B ] ---- [ Repository ]

      根据关注点分离规则,ViewModel 应该公开 LiveData。 LiveData 使用 Observers 来观察数据变化。 ViewModel 的目的是将数据层与 UI 分离。 ViewModel 不应该知道 Android 框架类。

      在 MVVM 架构中,ViewModel 的作用是从存储库中获取数据。您可以考虑使用 Room 将 json 文件存储为本地数据源,或者将 Json API 保留为远程数据源。无论哪种方式,一般实现如下:

      组件 A - 实体(实现您的 getter 和 setter)

      方法一:使用房间

      @Entity(tableName =  "file")
      public class FileEntry{ 
      @PrimaryKey(autoGenerate = true)
      private int id; 
      private String content; // member variables
      
      public FileEntry(String content){ // constructor
          this.id = id;
          this.content = content; 
      }
      
      public int getId(){ // getter methods
          return id;
      }
      
      public void setId(int id){ // setter methods
          this.id = id;
      }
      
      public String getContent(){
          return content;
      }
      
      public void setContent(String content){
          this.content = content;
       }
      }
      

      方法二:使用远程数据源

      public class FileEntry implements Serializable{
          public String getContent(){
              return content;
          }
      
          private String content;
      }
      

      组件 B - ViewModel(表示层)

      方法一:使用房间

      当您询问如何传递 android 上下文时,您可以通过如下方式扩展 AndroidViewModel 来包含应用程序引用。这是如果您的数据库需要应用程序上下文,但一般规则是 Activity 和 Fragments 不应存储在 ViewModel 中。

      假设您将“文件”作为为对象列表定义的成员变量,例如在本例中为“文件条目”对象:

      public class FileViewModel extends AndroidViewModel{
      
          // Wrap your list of FileEntry objects in LiveData to observe data changes
          private LiveData<List<FileEntry>> files;
      
          public FileViewModel(Application application){
              super(application);
          FilesDatabase db = FilesDatabase.getInstance(this.getApplication());
      

      方法二:使用远程数据源

      public class FileViewModel extends ViewModel{
           public FileViewModel(){}
           public LiveData<List<FileEntry>> getFileEntries(String content){
           Repository repository = new Repository();
           return repository.getFileEntries(content);
         }
       }
      

      在这种情况下,getFileEntries 方法包含 MutableLiveData:

      final MutableLiveData<List<FileEntry>> mutableLiveData = new MutableLiveData<>();
      

      如果您使用 Retrofit 客户端实现,您可以使用异步回调执行类似于以下代码的操作。代码取自Retrofit 2 Guide at Future Studio,对本讨论示例进行了一些修改。

      // asynchronous
      call.enqueue(new Callback<ApiData>() {
      
      @Override
      public void onResponse(Call<ApiData> call, Response<ApiData> response) {
          if (response.isSuccessful()) {
              mutableLiveData.setValue(response.body().getContent());
          } else {
              int statusCode = response.code();
      
              // handle request errors yourself
              ResponseBody errorBody = response.errorBody();
          }
      }
      
      @Override
      public void onFailure(Call<ApiData> call, Throwable t) {
          // handle execution failures like no internet connectivity 
      }
      
      return mutableLiveData;
      

      组件 C - 视图(UI 控制器)

      无论你是使用方法一还是二,你都可以这样做:

      FileViewModel fileViewModel = ViewModelProviders.of(this).get(FileViewModel.class);
      
      fileViewModel.getFileEntries(content).observe(this, fileObserver);
      

      希望这有帮助。

      对性能的影响

      在我看来,决定是否使用哪种方法可能取决于您正在实现多少数据调用。如果有多个,Retrofit 可能是简化 API 调用的更好主意。如果您使用 Retrofit 客户端实现它,您可能会得到类似于以下代码的内容,这些代码来自此参考 article on Android Guide to app architecture

      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);
      
          webservice.getUser(userId).enqueue(new Callback<User>() {
              @Override
              public void onResponse(Call<User> call, Response<User> response) {
                  data.setValue(response.body());
              }
          });
          return data;
      }
      

      上述实现可能具有线程性能优势,因为 Retrofit 允许您使用 enqueue 进行异步网络调用并在后台线程上返回 onResponse 方法。通过使用方法 2,您可以利用 Retrofit 的回调模式在并发后台线程上进行网络调用,而不会干扰主 UI 线程。

      上述实现的另一个好处是,如果您正在进行多个 api 数据调用,您可以通过上面的接口webservice 干净地获取响应,用于您的 LiveData。这使我们能够调解不同数据源之间的响应。然后,调用data.setValue 设置 MutableLiveData 值,然后根据 Android 文档将其分派给主线程上的活动观察者。

      如果您已经熟悉 SQL 并且只实现了 1 个数据库,那么选择 Room Persistence Library 可能是一个不错的选择。它还使用 ViewModel,由于减少了内存泄漏的可能性,因此带来了性能优势,因为 ViewModel 在 UI 和数据类之间维护的强引用更少。

      可能需要关注的一点是,您的数据库存储库(例如,FilesDatabase 实现为单例,以提供单个全局访问点,使用公共静态方法创建类实例,以便只有 1 个相同的实例数据库的任何时候打开?如果是,单例可能会被限制在应用程序范围内,如果用户仍在运行应用程序,则 ViewModel 可能会泄漏。因​​此请确保您的 ViewModel 使用 LiveData 来引用视图。此外,使用延迟初始化可能会有所帮助,以便在尚未创建先前实例的情况下使用 getInstance 方法创建 FilesDatabase 单例类的新实例:

      private static FilesDatabase dbInstance;
      // Synchronized may be an expensive operation but ensures only 1 thread runs at a time 
      public static synchronized FilesDatabase getInstance(Context context) {
          if (dbInstance == null) {
               // Creates the Room persistent database
               dbInstance = Room.databaseBuilder(context.getApplicationContext(), FilesDatabase.class, FilesDatabase.DATABASE_NAME)
      

      另一件事是,无论您为 UI 选择 Activity 还是 Fragment,您都将使用 ViewModelProviders.of 来保留您的 ViewModel,而您的 Activity 或 Fragment 的范围仍然存在。如果您正在实现不同的活动/片段,您的应用程序中将有不同的 ViewModel 实例。

      例如,如果您正在使用 Room 实现您的数据库,并且您希望允许您的用户在使用您的应用程序时更新您的数据库,那么您的应用程序现在可能需要在您的主要活动和更新活动中使用相同的 ViewModel 实例。尽管是一种反模式,但 ViewModel 提供了一个带有空构造函数的简单工厂。您可以使用public class UpdateFileViewModelFactory extends ViewModelProvider.NewInstanceFactory{ 在 Room 中实现它:

      @Override
      public <T extends ViewModel> T create(@NotNull Class<T> modelClass) {
      return (T) new UpdateFileViewModel(sDb, sFileId);
      

      上面,T是create的类型参数。在上面的工厂方法中,类 T 扩展了 ViewModel。成员变量 sDb 用于 FilesDatabase,sFileId 用于表示每个 FileEntry 的 int id。

      如果您想了解更多关于性能成本的信息,Android 的 Persist Data 部分的 article 可能比我的 cmets 更有用。

      【讨论】:

      • 谢谢,那么哪个更昂贵和更消耗内存?1- 将 json 文件作为资产 2- 将所有数据都保存在 sqlite 中
      • 我已在对性能的影响部分编辑了我对您问题的回答。希望这会有所帮助。
      • 这是一个很长的答案,但它只是重新散列了 MVVM 的文档,并没有直接解决如何处理 MVVM 中的 assets 的问题。 (请注意,答案甚至没有在任何地方提及“资产”这个词。)
      • 总是有很多地方需要改进。收到建议是件好事。感谢您的意见。
      猜你喜欢
      • 2011-06-26
      • 2011-12-04
      • 1970-01-01
      • 2014-08-22
      • 2019-10-16
      • 2021-02-25
      • 1970-01-01
      • 2013-08-21
      • 1970-01-01
      相关资源
      最近更新 更多