MVVM в android, доступ к assetsManager без нарушения шаблона

У меня есть файл JSON в папке с ресурсами, и классу DataManager (репозиторий) он нужен, поэтому у менеджера ресурсов (и контекста) должен быть доступ к ресурсам.

Проблема заключается в том, что, исходя из передовой практики, контекст Android или специфичный для Android код не должны передаваться на уровень данных (ViewModel-Repo-Model) из-за написания модульных тестов и т. д., а также представление не должно взаимодействовать с уровнем данных напрямую.

В итоге я предоставил список, используя и внедрив его в репозиторий.

Правильно ли это?

-Спасибо

PS: класс моего модуля, который предоставляет список

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

person Hessam-Emami    schedule 03.02.2019    source источник


Ответы (2)


Доступ ViewModel и/или Repository к Application контекст напрямую, и это все, что вам нужно для доступа к файлу AssetsManager. Вызов Application.getAssets() допустим, потому что ViewModel не использует какой-либо конкретный контекст Activity.

Например, вы можете использовать предоставленный Google подкласс AndroidViewModel вместо суперкласс ViewModel. AndroidViewModel принимает Application в своем конструкторе (ViewModelProviders вставит его за вас). Вы можете передать свой Application своему Repository в его конструкторе.

В качестве альтернативы вы можете использовать Dagger внедрение зависимостей, чтобы внедрить Application непосредственно в ваш Repository. (Вставка контекста Application немного сложна. См. Dagger 2, внедряющий контекст Android и эта проблема размещена в репозитории Danger на github.) Если вы хотите сделать ее действительно гладкой, вы можете настроить поставщика для AssetManager и внедрить его непосредственно в свой Repository.

Наконец, если вы используете Room и все, что вам нужно, это предварительно заполнить базу данных Room предварительно настроенной базой данных, хранящейся в активах, вы можете следовать инструкциям здесь: Как использовать Room Persistence Library с предварительно заполненной базой данных?

person Dan Fabulich    schedule 20.04.2019

Поскольку вы используете MVVM впервые, мы можем попытаться упростить ситуацию.

[ View Компонент C] ---- (наблюдает) [ ViewModel Компонент B ] ---- [ Репозиторий ]

Согласно правилу разделения интересов ViewModel должна предоставлять LiveData. LiveData использует наблюдателей для наблюдения за изменениями данных. Целью ViewModel является отделение уровня данных от пользовательского интерфейса. ViewModel не должен знать о классах фреймворка Android.

В архитектуре MVVM роль ViewModel заключается в извлечении данных из репозитория. Вы можете либо сохранить файл json в качестве локального источника данных с помощью Room, либо сохранить Json API в качестве удаленного источника данных. В любом случае, общая реализация выглядит следующим образом:

Компонент A — Entity (реализует геттеры и сеттеры)

Способ 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, как показано ниже, чтобы включить ссылку на приложение. Это если для вашей базы данных требуется контекст приложения, но общее правило заключается в том, что Activity & Fragments не должны храниться в 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, вы можете сделать что-то похожее на приведенный ниже код, используя асинхронные обратные вызовы. Код взят из Руководства по модернизации 2 в 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 — представление (контроллер пользовательского интерфейса)

Независимо от того, используете ли вы метод 1 или 2, вы можете сделать следующее:

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

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

Надеюсь, это полезно.

Влияние на производительность

На мой взгляд, решение о том, использовать ли какой метод, может зависеть от того, сколько вызовов данных вы реализуете. Если несколько, Retrofit может быть лучшей идеей для упрощения вызовов API. Если вы реализуете его с помощью клиента Retrofit, у вас может быть что-то похожее на приведенный ниже код, взятый из этой ссылки статья о Android-руководстве по архитектуре приложений:

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 для сетевых вызовов в параллельных фоновых потоках, не мешая основному потоку пользовательского интерфейса.

Еще одним преимуществом вышеприведенной реализации является то, что если вы делаете несколько вызовов данных API, вы можете чисто получить ответ через интерфейс webservice выше для ваших LiveData. Это позволяет нам передавать ответы между различными источниками данных. Затем вызов data.setValue устанавливает значение MutableLiveData, а затем отправляет его активным наблюдателям в основном потоке в соответствии с документацией Android.

Если вы уже знакомы с SQL и реализуете только 1 базу данных, выбор библиотеки сохранения состояния комнаты может быть хорошим вариантом. Он также использует ViewModel, что дает преимущества в производительности, поскольку снижается вероятность утечек памяти, поскольку ViewModel поддерживает меньше сильных ссылок между вашим пользовательским интерфейсом и классами данных.

Одной из проблем может быть ваш репозиторий БД (например, 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)

Другое дело, что независимо от вашего выбора Activity или Fragment для вашего пользовательского интерфейса вы будете использовать ViewModelProviders.of для сохранения своей ViewModel, пока область действия вашего Activity или Fragment жива. Если вы реализуете разные действия/фрагменты, в вашем приложении будут разные экземпляры ViewModel.

Если, например, вы реализуете свою базу данных с помощью Room и хотите, чтобы ваш пользователь обновлял вашу базу данных при использовании вашего приложения, вашему приложению теперь может потребоваться один и тот же экземпляр ViewModel для вашей основной деятельности и действия по обновлению. Несмотря на антишаблон, ViewModel предоставляет простую фабрику с пустым конструктором. Вы можете реализовать это в комнате, используя public class UpdateFileViewModelFactory extends ViewModelProvider.NewInstanceFactory{:

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

Выше T — это параметр типа create. В приведенном выше фабричном методе класс T расширяет ViewModel. Переменная-член sDb предназначена для FilesDatabase, а sFileId — для идентификатора int, который представляет каждый FileEntry.

Эта статья о разделе Persist Data от Android может быть полезнее моей комментарии, если вы хотите узнать больше о затратах на производительность.

person B4eight    schedule 04.02.2019
comment
Спасибо. Итак, какой из них более затратный и потребляет больше памяти? 1. Наличие файла json в качестве актива. 2. Наличие всех данных в sqlite. - person Hessam-Emami; 05.02.2019
comment
Я отредактировал свой ответ, чтобы ответить на ваш вопрос, в разделе «Влияние на производительность». Надеюсь, это полезно. - person B4eight; 08.02.2019
comment
Это такой длинный ответ, но он просто перефразирует документацию для MVVM и не затрагивает напрямую вопрос о том, как обращаться с активами в MVVM. (Обратите внимание, что в ответе нигде не упоминается слово «активы».) - person Dan Fabulich; 21.04.2019
comment
Всегда есть много вещей, которые можно улучшить. Приятно получать предложения. Спасибо за ваш вклад. - person B4eight; 22.04.2019