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

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

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

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

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

Ключевых преимуществ класса ViewModel по существу два:

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

Упорство

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

Объем

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

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

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

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

Дескриптор сохраненного состояния

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

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

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

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

Реактивный ранец

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

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

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

Для получения дополнительной информации см. руководство по поднятию состояния для Jetpack Compose.

Реализация модели представления

Ниже приведен пример реализации 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
        });
    }
}

Реактивный ранец

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:

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

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

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

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

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

Для получения дополнительной информации о классе ViewModel обратитесь к следующим ресурсам.

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

Образцы

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