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

时间:2019-02-03 13:35:15

标签: android android-assets android-mvvm

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

问题是,根据最佳实践,由于编写单元测试或其他内容不容易,因此不应将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));
    }
}

2 个答案:

答案 0 :(得分: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?

答案 1 :(得分:0)

由于您是第一次使用MVVM,因此我们可以尝试简化操作。

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

根据“关注分离”规则,ViewModel应该公开LiveData。 LiveData使用观察者观察数据更改。 ViewModel的目的是将数据层与UI分开。 ViewModel不应该了解Android框架类。

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

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

方法1:使用会议室

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

方法2:使用远程数据源

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

    private String content;
}

组件B-ViewModel (演示层)

方法1:使用会议室

当您询问如何传递android上下文时,可以通过扩展如下所示的AndroidViewModel来包含应用程序引用来实现。这是如果您的数据库需要应用程序上下文,但是一般规则是“活动和片段”不应存储在ViewModel中。

假设您有“文件”作为为对象列表定义的成员变量,在这种情况下,例如“ 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());

方法2:使用远程数据源

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控制器)

无论您使用的是方法1还是方法2,都可以这样做:

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干净地获取响应。这使我们能够调解不同数据源之间的响应。然后,根据Android文档,调用data.setValue设置MutableLiveData值,然后将其分派给主线程上的活动观察者。

如果您已经熟悉SQL且仅实现1个数据库,则选择Room Persistence Library是一个不错的选择。它还使用ViewModel,因为ViewModel维护了UI和数据类之间较少的强引用,因此减少了内存泄漏的机会,从而带来了性能优势。

一个令人关注的问题是,您的数据库存储库(例如FilesDatabase实现为单例,以提供单个全局访问点,使用公共静态方法创建类实例,因此仅1可以一次打开同一个数据库实例吗?如果是,则单例可能会作用于应用程序范围;如果用户仍在运行该应用程序,则可能会泄漏ViewModel。因此,请确保您的ViewModel使用LiveData来另外,使用惰性初始化可能会有所帮助,如果还没有创建以前的实例,则可以使用FilesDatabase方法创建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,同时您的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上关于持久数据的article部分可能比我的评论有用。