Что такое ViewModel

Введение

При создании приложений многие разработчики наверняка сталкивались с проблемой потери данных приложения при повороте устройства. Задумывались ли вы, почему это происходит? Дело в том, что при изменении конфигурации ваша активность создается заново. При этом ваши данные или состояние теряются. Но не стоит беспокоиться: решить эту проблему поможет ViewModel!

ViewModel не является исключительным явлением для Android-приложений, но в Android они занимают особое место.

Что нужно знать

  • Основы разработки Android.
  • Язык Kotlin.
  • LiveData и DataBinding.
  • Жизненный цикл активностей (Activity Lifecycle).

Что такое архитектура приложения?

Прежде чем перейти к рассмотрению ViewModels, немного освежим знания об архитектуре приложений.

Разработка приложения предполагает создание последовательной, масштабируемой, поддерживаемой и понятной кодовой базы. Для этого обычно используются архитектурные шаблоны проектирования. К числу популярных шаблонов относятся MVVM, MVI и MVC. Они определяют способ организации структуры приложения.

Хорошо продуманная архитектура приложения позволяет масштабировать и расширять его новыми возможностями в будущем. Кроме того, она облегчает командную работу. Наиболее популярными архитектурными идеями являются разделение задач и управление пользовательским интерфейсом с помощью модели.

В Android одним из самых популярных шаблонов проектирования является MVVM (Model View View Model). Ключевой частью этого шаблона является ViewModel. Хотя в данный момент нет необходимости подробно рассматривать шаблон MVVM, важно понимать его значение в процессе разработки.

Что такое ViewModel?

Говоря техническим языком, класс, называемый ViewModel, отвечает за управление и хранение данных для Activity или Fragment. Он управляет данными приложения и позволяет ему придерживаться архитектурного принципа, согласно которому пользовательский интерфейс (UI) создается на основе модели.

Говоря простым языком, ViewModel  —  это место, содержащее бизнес-логику приложения, которая представляет собой реальную программную логику, а не просто обновление элемента пользовательского интерфейса, например фильтрацию списка или проверку вводимых пользователем данных.

Почему стоит использовать ViewModel?

Без ViewModel

Теперь взгляните на GIF-изображение несколько раз и обратите внимание на то, что там происходит. Вы наверняка заметили, что цвет текста меняется при нажатии на кнопку, но при повороте экрана все происходит по-другому (потому что активность пересоздается при изменении конфигурации, а цвет текста не сохраняется). Но не волнуйтесь, ViewModel может спасти положение!

При изменении конфигурация ViewModel автоматически сохраняется, так что содержащиеся в ней данные сразу же становятся доступными для следующего экземпляра Activity или Fragment. Это также означает, что при переключении между активностями или настройке конфигурации, например повороте экрана, пользовательскому интерфейсу не нужно будет снова запрашивать данные.

ViewModel также содержит состояние конкретного экрана. Состояние  —  это то, как выглядит пользовательский интерфейс в данный момент. Это значение, которое может изменяться с течением времени. Например, если цвет фона приложения меняется с течением времени, то это и есть состояние.

С ViewModel

После применения ViewModel к приложению цвет текста настолько закрепился на экране, что сохраняется даже при повороте.

Теперь у вас может возникнуть вопрос: можно ли другим способом сохранить состояние или изменения конфигурации? Можно, если применить коллбэк onSavedInstanceState(). Но у этого способа есть свои недостатки:

  • Он требует написания дополнительного кода для сохранения состояния в бандле.
  • Коллбэк onSavedInstanceState() вызывается только при воссоздании активности.
  • Объем хранящихся в бандле данных минимален.
  • Необходимо реализовать логику для извлечения этого состояния.

По сравнению с другими методами сохранения состояния, ViewModel обладает рядом преимуществ, помогая в написании более модульного, удобного и эффективного кода. Одно из их главных преимуществ заключается в том, что ViewModel учитывает жизненный цикл.

Как использовать ViewModel?

Чтобы понять, как работает ViewModel, разработаем приложение с изменяющимся по цвету текстом.

Для начала создадим новый проект в Android Studio. Выберем пустую активность (Empty Activity) и кликнем “Далее” (Next).

Выбираем “Новый проект” (“New Project”)
Выбираем “Пустая активность” (“Empty Activity”)

После нажатия “Next” дадим приложению имя ViewModelExample (или любое другое). Выберем в качестве языка Kotlin и кликнем “Завершение” (“Finish”).

К этому моменту сборка Gradle должна быть завершена, а вы должны находиться в файле MainActivity.kt.

Откроем файл build.gradle для нашего модуля приложения и добавим следующий код в блок android:

buildFeatures{
dataBinding = true
}

Кроме того, добавим в модуль приложения следующие зависимости:

dependencies{
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.fragment:fragment-ktx:1.6.0'
}

Эти зависимости необходимы для использования ViewModels и LiveData в приложении. После добавления вышеуказанных изменений можно синхронизировать проект.

Добавим следующий XML-код в файл activity_main.xml в каталоге res/layout:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>

<variable
name="viewModel"
type="com.shanks.viewmodelexample.MainViewModel" />

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp"
tools:context=".MainActivity">

<TextView
android:id="@+id/tvMyName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Hi, I Am Shashank"
android:textColor="@{viewModel.currentColor}"
android:textSize="40sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/btnChange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:padding="10dp"
android:text="Change"
android:textAlignment="center"
android:textSize="25sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Теперь щелкнем правой кнопкой мыши на пакете com.example.viewmodelexample и выберем New > Kotlin Class. Назовем класс MainViewModel и кликнем “OK”.

Изменим MainViewModel так, чтобы он был подклассом для ViewModel. ViewModel  —  это абстрактный класс, поэтому для его использования в приложении необходимо его расширить.

class MainViewModel : ViewModel() {
//код...
}

Чтобы связать MainViewModel с контроллером пользовательского интерфейса, т.е. с Activity или Fragment, создайте ссылку (объект) на ViewModel внутри MainActivity.

  • В верхней части класса MainActivity добавьте свойство типа MainViewModel.
  • Инициализируйте MainViewModel с помощью делегата свойств by viewModels() Kotlin. В двух словах, делегирование свойств в Kotlin  —  это что-то вроде найма личного помощника, готового взять на себя все ваши обязанности геттера-сеттера. Вы можете сосредоточиться на других вещах, например на написании кода, в то время как помощник будет заниматься утомительными деталями.
private val viewModel : MainViewModel by viewModels()

Допустим, вы инициализируете в приложении View Model с помощью стандартного конструктора MainViewModel, как показано ниже:

private val viewModel = MainViewModel()

Тогда при изменении конфигурации устройства приложение будет терять состояние ссылки на viewModel. Например, при повороте устройства активность будет “уничтожена” и создана заново, и у вас снова будет новый экземпляр View Model с начальным состоянием. Именно поэтому следует использовать подход с использованием делегата свойства.

Теперь добавим в файл MainViewModel.kt следующий код:

// Список цветов
private val colorList = listOf(
Color.rgb(255, 0, 0),
Color.rgb(0, 0, 255),
Color.rgb(0, 255, 0),
Color.rgb(255, 255, 0),
Color.rgb(0, 0, 0),
Color.MAGENTA,
Color.CYAN
)

// Объявляем частный изменяемый объект LiveData, который может быть изменен
// только в пределах объявленного класса.
private val _currentColor = MutableLiveData<Int>()

// Объявляем другой публичный неизменяемый LiveData и переопределяем его метод getter.
// Возвращает значение частного свойства в методе getter.
// Таким образом, к нему можно обращаться за пределами текущего класса.
val currentColor : LiveData<Int>
get() = _currentColor

// Логика приложения
init {
getColor()
Log.d("viewModel", "in-it")
}

// Функция getcolor() устанавливает значение _currentColor в случайный цвет.
fun getColor() {
_currentColor.value = colorList[getRandomValue(0, colorList.size)]
}

// Эта функция возвращает случайное число в диапазоне от 0 до размера списка.
private fun getRandomValue(start : Int, end: Int): Int {
return Random.nextInt(start, end);
}

В приведенном выше коде используется концепция LiveData и свойство backing.

Теперь добавим логику в приложение, чтобы при нажатии на кнопку цвет менялся. Для этого добавим следующий код в файл MainActivity.kt. Настроим также привязку данных в файле MainActivity, чтобы можно было взаимодействовать с XML-файлом.

// Ваш файл MainActivity.kt должен выглядеть следующим образом

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.databinding.DataBindingUtil
import com.shanks.viewmodelexample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

private val viewModel : MainViewModel by viewModels()
private lateinit var binding : ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

binding.viewModel = viewModel

// Использование объекта viewModel для вызова функции getColor()
binding.btnChange.setOnClickListener {
viewModel.getColor()
}

// Указываем активность в качестве владельца жизненного цикла привязки.
// Это используется для того, чтобы привязка могла наблюдать за обновлениями LiveData.
binding.lifecycleOwner = this
}
}

MainViewModel.kt должен выглядеть следующим образом.

import android.graphics.Color
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import kotlin.random.Random

class MainViewModel : ViewModel() {

private val colorList = listOf(
Color.rgb(255, 0, 0),
Color.rgb(0, 0, 255),
Color.rgb(0, 255, 0),
Color.rgb(255, 255, 0),
Color.rgb(0, 0, 0),
Color.MAGENTA,
Color.CYAN
)

private val _currentColor = MutableLiveData<Int>()

val currentColor : LiveData<Int>
get() = _currentColor

init {
getColor()
Log.d("viewModel", "in-it")
}

fun getColor() {
_currentColor.value = colorList[getRandomValue(0, colorList.size)]
}

private fun getRandomValue(start : Int, end: Int): Int {
return Random.nextInt(start, end);
}


}

Работа по созданию приложения с использованием ViewModel завершена. Теперь запустим его на любом виртуальном или физическом устройстве. Вы увидите, что цвет текста остается неизменным даже при повороте устройства.

Заключение

Мы разобрали все нюансы работы с ViewModels. Итак, теперь вы знаете ответы на следующие вопросы:

  • Что такое ViewModels?
  • Зачем нужно использовать ViewModels?
  • Как эффективно использовать ViewModels?

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Shashank Yadav: A Simple View of ViewModels

Предыдущая статьяРазделение пользовательского интерфейса и логики в React: чистый код с безголовыми компонентами
Следующая статьяКак создать свой Twitter или управляемое данными приложение с Golang и Kafka