Этот пост не является учебным пособием по реализации архитектурного шаблона MVVM — это список хороших практик и неправильных представлений о MVVM, обычно используемых разработчиками Android.

ViewModel против AndroidViewModel

tl;dr Не используйте AndroidViewModel, это разрушает всю тестируемость, которую в первую очередь обеспечивает MVVM.

/* Для упрощения примера в этой части предполагается, что мы не используем слой UseCase (который следует использовать) */

AndroidViewModel — это то, о чем говорит его название — это обычная ViewModel с добавлением немного Android — в конструкторе нам нужно передать экземпляр приложения, и мы можем получить к нему доступ из ViewModel. Вот и все из отличий. Только это, одна простая вещь, чтобы вы не могли легко издеваться над своей ViewModel. Ни разу не было сказано, какой бы архитектурный шаблон вы ни использовали, не используйте зависимости Android в Business Logic Layer.

Итак, какая альтернатива у нас есть? - Создание классов-оболочек, которые будут отвечать за то, что вы использовали, либо за приложение, либо за любую другую зависимость от Android. Таким образом, вы обнаружите некоторые определенные функции, которые на самом деле используются, и только они будут имитироваться в ваших модульных тестах — представьте, что вы имитируете Application.getSharedPreferences() или все SharedPreferences.get/putметоды — слишком много работы. .

Предположим, вы использовали Application для получения некоторых данных из SharedPreferences — что нужно сделать, так это создать некоторый класс Settings , который будет отображать определенные SharedPreferences поля, а экземпляр SharedPreferences должен быть одним из параметров конструктора класса Settings.

private const KEY_DARK_MODE = "dark_mode"
class Settings(private val _sharedPreferences: SharedPreferences) {
    var isDarkMode: Boolean
        set (value) {
            _sharedPreferences.edit {
                putBoolean(KEY_DARK_MODE, value)
            }
        }
        get() = _sharedPreferences.getBoolean(KEY_DARK_MODE)
}

Более того, это решение позволяет вам устанавливать и получать данные из ваших настроек с помощью простого синтаксиса kotlin — _settings.isDarkMode = true

Создание экземпляра ViewModel

tl;dr Не создавайте экземпляр ViewModel конструктором непосредственно в вашем представлении, вместо этого используйте ViewModelProvider и ViewModelFactory, иначе он будет привязан к жизненному циклу и будет создаваться каждый раз при вызове onCreate.

Каждый ViewModel должен иметь ViewModelFactory, который будет отвечать за создание экземпляров в провайдере, вот пример:

class LoginViewModelFactory(private val _settings: Settings): ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return LoginViewModel(_settings)
    }
}

вьюмоделпровидер

И чтобы получить его, вы используете ViewModelProvider, первый параметр ViewModelProvider constructor — ViewModelStoreOwner, поэтому в Activity and Fragments мы можем просто передать this, второй параметр это ViewModelProvider.Factory экземпляр. При создании экземпляра ViewModelProvider вы можете вызывать get для получения ViewModel указанного Classтипа в качестве параметра,

viewModel = ViewModelProvider(this, loginViewModelFactory)
    .get(LoginViewModel::class.java)

ViewModelStoreOwner

ViewModelStoreOwner заботится о правильном создании экземпляра ViewModelStore, чтобы отделить его от изменений конфигурации и жизненного цикла приложения, поэтому данные сохраняются, а ViewModels не создаются повторно. ViewModelStore (и ViewModels) очищаются только в onDestroy, когда это не вызвано изменениями конфигурации (например, изменением ориентации) - это магия независимости жизненного цикла MVVM, ниже код из ComponentActivity:

getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            if (!isChangingConfigurations()) {
                getViewModelStore().clear();
            }
        }
    }
});

Итак, у нас есть ViewModelStoreOwner в ViewModelProvider, что дальше?
Короче говоря, метод get берет экземпляр ViewModel из хранилища, если он уже создан, если это не так, он создается и помещается в ViewModelStore, поэтому, когда мы запрашиваем ViewModel этого типа после изменения конфигурации, мы получим тот же экземпляр, поэтому уже загруженные данные будут сохранены и больше не будут извлекаться.

/* Есть способы сделать его более абстрактным, но это зависит от того, какую библиотеку внедрения зависимостей вы используете. Вкратце: ЭТО — это руководство по созданию этого с помощью dagger2, а в коине вы просто предоставляете метод viewModel внутри своего модуля — создание экземпляра конструктором, все фабрики и обеспечение выполняются коином. */

Нет пользовательского интерфейса во ViewModel

tl;dr Не определяйте тексты, изображения и т. д. в ViewModel — определяйте их в View в зависимости от текущего значения LiveData.

Это как-то связано с частью AndroidViewModel — никаких зависимостей от Android в бизнес-логике. Для получения строк или рисунков в ViewModel требуется контекст, который не должен быть включен в вашу ViewModel, поскольку это зависимость от Android. Хорошо, это можно сделать с помощью некоторой оболочки, но в этом случае, если вы установите какую-либо строку или Drawable в качестве значения LiveData, это разрушит тестируемость.

Допустим, у вас есть ImageView, и в зависимости от состояния он должен отображать разное изображение: при неудаче — красный «X», при загрузке — циклический прогресс, при успехе — зеленый ✓. Если вы извлекаете Drawable из какой-либо оболочки, у вас будет зависимость от Android в ViewModel (сама Drawable), и ее будет сложно протестировать, вам нужно будет создать экземпляр Drawable, а это абстрактный класс, поэтому вам нужно будет найти какой-нибудь подкласс, например GradientDrawable, создайте его экземпляр, а затем сравните возвращенный экземпляр с вашим ожидаемым экземпляром, но как бы вы отличили неудачный, загрузочный и успешный рисунок?
Возможно, это не так точно со строками, но есть и другая причина - представление должно быть отделено от бизнес-логики. Бизнес-логика должна сообщать вам, что такое состояние, и представление должно адекватно реагировать на текущее состояние, и эта реакция должна быть определена на уровне представления.

Итак, каково решение? — Классы перечисления. Будет намного лучше просто создать enum с тремя состояниями FAILED, IN_PROGRESS, SUCCESS, а затем легко установить свое изображение в обозревателе. Таким образом, вы строго разделяете слой просмотра и логики.

viewModel.currentState.observe(this, Observer { state ->
    val drawableId = when(state) {
        DataState.FAILED -> R.drawable.failedIcon
        DataState.IN_PROGRESS -> R.drawable.circularProgress
        DataState.SUCCESS -> R.drawable.successIcon
    }
    stateImageView.setDrawable(drawableId)
}

Конец