android 中的 MVVM,在不破坏模式的情况下访问 assetManager

MVVM in android,accessing assetManager without breaking the pattern

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

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

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

这样做正确吗?

-谢谢

P.S:我的模块 class 提供列表

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

由于您是第一次使用 MVVM,我们可以尽量让事情变得简单。

[ View 组件 C] ---- (observes) [ ViewModel 组件B ] ---- [ 存储库 ]

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

在 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 和片段不应存储在 ViewModel 中。

假设您为对象列表定义了 "files" 作为成员变量,在这种情况下,例如 "FileEntry" 个对象:

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 & return 和 onResponse 方法进行异步网络调用。通过使用方法 2,您可以利用 Retrofit 的回调模式在并发后台线程上进行网络调用,而不会干扰主 UI 线程。

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

如果您已经熟悉 SQL 并且只实现了 1 个数据库,那么选择 Room Persistence Library 可能是一个不错的选择。它还使用 ViewModel,这带来了性能优势,因为内存泄漏的机会减少了,因为 ViewModel 在您的 UI 和数据 classes.

之间维护较少的强引用

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

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,同时 [=127] 的范围=] 或 Fragment 还活着。如果您要实现不同的 Activities/Fragments,您的应用程序中将有不同的 ViewModel 实例。

例如,如果您正在使用 Room 实现您的数据库并且您希望允许您的用户在使用您的应用程序时更新您的数据库,您的应用程序现在可能需要在您的主应用程序中使用相同的 ViewModel 实例 activity 和更新 activity。虽然是一种反模式,但 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的类型参数。在上面的工厂方法中,class T 扩展了 ViewModel。成员变量sDb为FilesDatabase,sFileId为代表每个FileEntry的int id。

如果您想了解更多有关性能成本的信息,Android 关于持久数据部分的 article 可能比我的评论更有用。

ViewModel and/or Repository 直接访问 Application 上下文并不违反 MVVM,这就是访问 AssetsManager.调用 Application.getAssets() 是可以的,因为 ViewModel 不使用任何特定的 Activity 上下文。

例如,您可以使用Google-提供的AndroidViewModel subclass instead of the superclass ViewModelAndroidViewModel 在其构造函数中采用 ApplicationViewModelProviders 将为您注入)。您可以在其构造函数中将 Application 传递给 Repository

或者,您可以使用 Dagger dependency injection to inject an Application directly into your Repository. (Injecting the Application context is a bit tricky. See and this issue filed on the Danger github repo。)如果您想让它变得非常流畅,您可以为 AssetManager 配置一个提供程序并将其直接注入您的 Repository

最后,如果您正在使用 Room,并且您想要的只是使用存储在资产中的预配置数据库预填充 Room 数据库,您可以按照此处的说明进行操作: