Обзор ViewModel Часть Android Jetpack .

Попробуйте Kotlin Multiplatform
Kotlin Multiplatform позволяет использовать бизнес-логику на других платформах. Узнайте, как настроить ViewModel и работать с ним в KMP.

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

Дополнительную информацию о держателях состояний см. в руководстве по держателям состояний . Аналогично, дополнительную информацию о слое пользовательского интерфейса в целом см. в руководстве по слою пользовательского интерфейса .

Преимущества ViewModel

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

По сути, основных преимуществ класса ViewModel два:

  • Позволяет сохранять состояние пользовательского интерфейса.
  • Обеспечивает доступ к бизнес-логике.

Упорство

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

Объем

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

Ряд классов являются прямыми или косвенными подклассами интерфейса ViewModelStoreOwner . Прямыми подклассами являются ComponentActivity , Fragment и NavBackStackEntry . Полный список косвенных подклассов см. в справочнике ViewModelStoreOwner .

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

Более подробную информацию см. в разделе ниже, посвященном жизненному циклу ViewModel .

SavedStateHandle

SavedStateHandle позволяет сохранять данные не только при изменении конфигурации, но и при повторном запуске процесса. То есть, он позволяет сохранять состояние пользовательского интерфейса неизменным, даже когда пользователь закрывает приложение и открывает его позже.

Доступ к бизнес-логике

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

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

Jetpack Compose

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

Самое важное, что следует помнить при использовании ViewModel с Compose, — это то, что область действия ViewModel не может быть ограничена компонуемым объектом. Это связано с тем, что компонуемый объект не является ViewModelStoreOwner . Два экземпляра одного и того же компонуемого объекта в Composition или два разных компонуемых объекта, обращающихся к одному и тому же типу ViewModel с одним и тем же ViewModelStoreOwner , получат один и тот же экземпляр ViewModel, что часто не соответствует ожидаемому поведению.

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

Более подробную информацию см. в руководстве по подъему состояния для Jetpack Compose.

Реализовать ViewModel

Ниже приведен пример реализации ViewModel для экрана, позволяющего пользователю бросать кости.

Котлин

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Ява

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

Затем вы можете получить доступ к ViewModel из действия следующим образом:

Котлин

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Ява

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

Использование сопрограмм с ViewModel

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

Дополнительные сведения см. в разделе Использование сопрограмм Kotlin с компонентами архитектуры Android .

Жизненный цикл ViewModel

Жизненный цикл ViewModel напрямую связан с областью её действия. ViewModel остаётся в памяти до тех пор, пока не исчезнет ViewModelStoreOwner , к которому она относится. Это может произойти в следующих случаях:

  • В случае деятельности — когда она заканчивается.
  • В случае осколка — когда он отделяется.
  • В случае записи навигации, когда она удаляется из стека переходов.

Это делает ViewModels отличным решением для хранения данных, сохраняющих работоспособность при изменении конфигурации.

На рисунке 1 показаны различные состояния жизненного цикла активности, происходящие в процессе её вращения и завершения. На рисунке также показано время жизни ViewModel рядом с жизненным циклом соответствующей активности. Эта диаграмма иллюстрирует состояния активности. Те же основные состояния применимы и к жизненному циклу фрагмента.

Иллюстрирует жизненный цикл ViewModel по мере изменения состояния активности.

Обычно ViewModel запрашивается при первом вызове системой метода onCreate() объекта действия. Система может вызывать onCreate() несколько раз на протяжении существования действия, например, при повороте экрана устройства. ViewModel существует с момента первого запроса ViewModel до завершения и уничтожения действия.

Очистка зависимостей ViewModel

ViewModel вызывает метод onCleared , когда ViewModelStoreOwner уничтожает его в ходе своего жизненного цикла. Это позволяет очистить всю работу и зависимости, связанные с жизненным циклом ViewModel.

В следующем примере показана альтернатива viewModelScope . viewModelScope — это встроенный CoroutineScope , который автоматически следует жизненному циклу ViewModel. ViewModel использует его для запуска бизнес-операций. Если для упрощения тестирования вместо viewModelScope требуется пользовательская область видимости, ViewModel может получить CoroutineScope в качестве зависимости в своём конструкторе. Когда ViewModelStoreOwner очищает ViewModel в конце его жизненного цикла, ViewModel также отменяет CoroutineScope .

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

Начиная с версии жизненного цикла 2.5 и выше, вы можете передать один или несколько объектов Closeable конструктору ViewModel, который автоматически закрывается при очистке экземпляра ViewModel.

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

Лучшие практики

Ниже приведены несколько ключевых рекомендаций, которым следует следовать при реализации ViewModel:

  • Ввиду их области действия , ViewModel следует использовать в качестве деталей реализации держателя состояния на уровне экрана. Не используйте их в качестве держателей состояний повторно используемых компонентов пользовательского интерфейса, таких как группы микросхем или формы. В противном случае вы получите один и тот же экземпляр ViewModel в разных случаях использования одного и того же компонента пользовательского интерфейса под одним и тем же ViewModelStoreOwner, если только вы не используете явный ключ модели представления для каждого микросхемы.
  • ViewModel не должны знать о деталях реализации пользовательского интерфейса. Старайтесь, чтобы названия методов, предоставляемых API ViewModel, и полей состояния пользовательского интерфейса были максимально общими. Таким образом, ваша ViewModel сможет адаптироваться к любому типу пользовательского интерфейса: мобильному телефону, складному устройству, планшету и даже Chromebook!
  • Поскольку они потенциально могут существовать дольше, чем ViewModelStoreOwner , ViewModels не должны содержать никаких ссылок на API, связанные с жизненным циклом, такие как Context или Resources , чтобы предотвратить утечки памяти.
  • Не передавайте ViewModel другим классам, функциям или другим компонентам пользовательского интерфейса. Поскольку ими управляет платформа, их следует размещать как можно ближе к ней. Рядом с Activity, фрагментом или компонуемой функцией уровня экрана. Это предотвратит доступ компонентов более низкого уровня к большему объёму данных и логики, чем им необходимо.

Дополнительная информация

По мере усложнения данных вы можете решить использовать отдельный класс только для их загрузки. ViewModel предназначен для инкапсуляции данных для контроллера пользовательского интерфейса, чтобы они сохранялись при изменении конфигурации. Сведения о загрузке, сохранении и управлении данными при изменении конфигурации см. в разделе «Сохранённые состояния пользовательского интерфейса» .

В руководстве по архитектуре приложений Android предлагается создать класс репозитория для обработки этих функций.

Дополнительные ресурсы

Дополнительную информацию о классе ViewModel можно найти в следующих ресурсах.

Документация

Образцы

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}