7

I have a JSON file in the assets folder and DataManager(repository) class needs it so assetManager(and context) should have access to the assets.

The problem is that based on Best practice, Android context or android specific code should not be passed into the data layer(ViewModel-Repo-Model) because of writing unit tests or etc easily and also view should not interact with the data layer directly.

I ended up providing the list using and injecting it to the repository.

Is this the right thing to do?

-Thanks

P.S: my Module class which provides the list

@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));
    }
}
M-Razavi
  • 3,327
  • 2
  • 34
  • 46
Hessam-Emami
  • 347
  • 2
  • 4
  • 15

2 Answers2

8

It's not a violation of MVVM for a ViewModel and/or Repository to access the Application context directly, which is all you need to access the AssetsManager. Calling Application.getAssets() is OK because the ViewModel doesn't use any particular Activity's context.

For example, you can use the Google-provided AndroidViewModel subclass instead of the superclass ViewModel. AndroidViewModel takes an Application in its constructor (ViewModelProviders will inject it for you). You could pass your Application to your Repository in its constructor.

Alternately, you could use Dagger dependency injection to inject an Application directly into your Repository. (Injecting the Application context is a bit tricky. See Dagger 2 injecting Android Context and this issue filed on the Danger github repo.) If you want to make it really slick, you could configure a provider for AssetManager and inject it directly into your Repository.

Finally, if you are using Room, and all you want is to pre-populate your Room database with a pre-configured database stored in assets, you can follow instructions here: How to use Room Persistence Library with pre-populated database?

Dan Fabulich
  • 37,506
  • 41
  • 139
  • 175
  • But you simply can't unit test your class if any framework component gets into the game. Android relentlessly violates all the patterns they keep recommending. – Farid Apr 06 '22 at 20:00
0

Since you are using MVVM for the first time, we can try to keep things simple.

[ View Component C] ---- (observes) [ ViewModel Component B ] ---- [ Repository ]

According to the Separation of Concerns rule, the ViewModel should expose LiveData. LiveData uses Observers to observe data changes. The purpose of the ViewModel is to separate the data layer from UI. ViewModel should not know about Android framework classes.

In MVVM Architecture, the ViewModel's role is to fetch data from a Repository. You can consider either storing your json file as a local data source using Room, or keeping the Json API as a remote data source. Either way, the general implementation is as follows:

Component A - Entity (implements your getters & setters)

Method 1: Using Room

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

Method 2: Using Remote Data Source

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

    private String content;
}

Component B - ViewModel (Presentation Layer)

Method 1: Using Room

As you asked about how android context can be passed, you can do so by extending AndroidViewModel like below to include an application reference. This is if your database requires an application context, but the general rule is that Activity & Fragments should not be stored in the ViewModel.

Supposing you have "files" as a member variable defined for your list of objects, say in this case, "FileEntry" objects:

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());

Method 2: Using Remote Data Source

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

In this case, getFileEntries method contains MutableLiveData:

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

If you are implementing using Retrofit client, you can do something similar to below code using asynchronous callbacks. The code was taken from Retrofit 2 Guide at Future Studio with some modifications for this discussion example.

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

Component C - View (UI Controller)

Whether you are using Method 1 or 2, you can do:

FileViewModel fileViewModel = ViewModelProviders.of(this).get(FileViewModel.class);

fileViewModel.getFileEntries(content).observe(this, fileObserver);

Hope this is helpful.

Impacts on Performance

In my opinion, deciding whether to use which method may hinge on how many data calls you are implementing. If multiple, Retrofit may be a better idea to simplify the API calls. If you implement it using Retrofit client, you may have something similar to below code taken as provided from this reference 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;
}

The above implementation may have threading performance benefits, as Retrofit allows you to make asynchronous network calls using enqueue & return the onResponse method on a background thread. By using method 2, you can leverage Retrofit's callback pattern for network calls on concurrent background threads, without interfering with the main UI thread.

Another benefit of the implementation above is that if you are making multiple api data calls, you can cleanly get the response through an interface webservice above, for your LiveData. This allows us to mediate responses between different data sources. Then, calling data.setValue sets the MutableLiveData value & then dispatches it to active observers on the main thread, as per Android documentation.

If you are already familiar with SQL & only implementing 1 database, opting for the Room Persistence Library may be a good option. It also uses the ViewModel, which brings performance benefits since chances of memory leaks are reduced, as ViewModel maintains fewer strong references between your UI & data classes.

One point of concern may be, is your db repository (example, FilesDatabase implemented as a singleton, to provide a single global point of access, using a public static method to create the class instance so that only 1 same instance of the db is opened at any one time? If yes, the singleton might be scoped to the application scope, & if the user is still running the app, the ViewModel might be leaked. Thus make sure your ViewModel is using LiveData to reference to Views. Also, it might be helpful to use lazy initialization so that a new instance of the FilesDatabase singleton class is created using getInstance method if there are no previous instances created yet:

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)

Another thing is, no matter your choice of Activity or Fragment for your UI, you will be using ViewModelProviders.of to retain your ViewModel while a scope of your Activity or Fragment is alive. If you are implementing different Activities/Fragments, you will have different instances of ViewModel in your application.

If for example, you are implementing your database using Room & you want to allow your user to update your database while using your application, your application may now need the same instance of the ViewModel across your main activity and the updating activity. Though an anti-pattern, ViewModel provides a simple factory with an empty constructor. You can implement it in Room using public class UpdateFileViewModelFactory extends ViewModelProvider.NewInstanceFactory{:

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

Above, T is a type parameter of create. In the factory method above, the class T extends ViewModel. The member variable sDb is for FilesDatabase, and sFileId is for the int id that represents each FileEntry.

This article on Persist Data section by Android may be more useful than my comments if you would like to find out more, on performance costs.

B4eight
  • 92
  • 3
  • Thank you, So which one is more costly and memory consuming?1- having a json file as asset 2-having all the data in sqlite – Hessam-Emami Feb 05 '19 at 09:46
  • I have edited my response to answer to your question, under Impacts on Performance section. Hope this is helpful. – B4eight Feb 08 '19 at 04:10
  • 3
    This is such a long answer, but it's just rehashing the documentation for MVVM, and isn't directly addressing the question of how to deal with _assets_ in MVVM. (Note that the answer doesn't even _mention_ the word "assets" anywhere.) – Dan Fabulich Apr 20 '19 at 23:11
  • There are always many things to improve on. It is good to receive suggestions. Thanks for your input. – B4eight Apr 22 '19 at 05:53