Vector ViewModel¶
The VectorViewModel
class is the place where the UI state is stored. It is also the only place which can perform changes to the UI state. You must extend this class in your own ViewModels. It is generic on a state class implementing the VectorState
interface.
data class UserState(...): VectorState class UserViewModel( initialState: UsersState ): VectorViewModel<UsersState>(initialState) { ... }
Creating a ViewModel¶
A ViewModel
is scoped to the Lifecycle of its owning Fragment or Activity. An Activity can access Activity-scoped ViewModels, whereas Fragments can create both Fragment-scoped and Activity-scoped ViewModels.
The library ships with a few Kotlin property delegates which make it easy to create a VectorViewModel
for whichever scope you need.
- From a Fragment:
val userViewModel: UserViewModel by fragmentViewModel() // Scoped to this fragment // OR val userViewModel: UserViewModel by activityViewModel() // Scoped to the parent activity
- From an Activity:
val userViewModel: UserViewModel by viewModel() // Scoped to this activity
These delegates automatically create the ViewModel for you, as long as they do not have any external dependencies.
ViewModels with additional dependencies¶
If your ViewModel has external dependencies, then you should use an alternative version of these delegates which accepts a trailing lambda that should contain the code to create your ViewModel.
val userViewModel: UserViewModel by fragmentViewModel { initialState, savedStateHandle -> UserViewModel(initialState, UserRepository()) }
Alternatively, you can choose to implement the VectorViewModelFactory
interface in your ViewModel's companion object to provide your own implementation for its create
and initialState
methods.
class UserViewModel(initialState: UserState, val repository: UserRepository) { ... companion object: VectorViewModelFactory<UserViewModel, UserState> { override fun initialState(handle: SavedStateHandle, owner: ViewModelOwner): UserState? { // Create state object directly or restore it using `SavedStateHandle` } override fun create(initialState: UserState, owner: ViewModelOwner, handle: SavedStateHandle): UserViewModel? { // Create and return your ViewModel // the `owner` parameter can be used to access your DI graph } } }
Support for AssistedInject factories¶
If you use Dagger and AssistedInject in your project, then you can create ViewModels in this way:
@Inject val usersViewModelFactory: UsersViewModel.Factory // The AssistedInject factory val userViewModel: UserViewModel by fragmentViewModel { initialState, savedStateHandle -> userViewModelFactory.create(initialState, ...) }
Managing State¶
Mutating State¶
State mutation is done through the setState
function, which accepts regular lambdas as well as suspending lambdas. The supplied lambda is given the current state as the receiver, and it is responsible for creating a new state and returning it.
class UserViewModel(...): VectorViewModel<UserState>(...) { fun greetUser() = setState { val currentState = this // this = current state val newState = UsersState(greeting = "Hello!", user = currentState.user) newState } }
If the state class is a Kotlin Data class, then this can be expressed succintly as:
fun greetUser() = setState { copy(greeting = "Hello!") }
This works because the this
received in the setState
block is the current state, which has a copy
method define for it by virtue of being a data class.
Note
State mutations are processed asynchronously. You should not rely on the state to be updated immediately after you call the setState
function. Every state mutation is enqueued to a Channel
on a background thread, which processes them sequentially to avoid race conditions.
Accessing State¶
If you need to access the current state and perform some action based on it, you should use the withState
function. It receives the current state as a parameter, and can then use it to perform decisions based on it.
fun greetUser() = withState { state -> if (state.isUserPremium()) { setState { copy(greeting = "Hello, premium user!") } } else { setState { copy(greeting = "Hello!")} } }
withState
blocks, just like setState
blocks, are processed on a background thread asynchronously.
The state parameter supplied to withState
is guaranteed to be the latest state at the time of processing the lambda. Any nested setState
blocks are processed immediately, before any other withState
blocks can be processed.
Warning
While there are other ways to access the state in your ViewModel, using the withState
function is the safest way to do so. Since state updates are processed asynchronously, other methods are not guaranteed to have the latest state when you access it. The withState
block always receives the latest state as a parameter when it is processed.
Note
There's also a currentState
property in a VectorViewModel
, but it should not be used in place of a withState
block. currentState
only provides a convenient way for external classes to access the current state without subscribing to it.
Observing state changes¶
A VectorViewModel
exposes state to fragments and activities through a Kotlin Flow.
A Flow is a cold stream of values, which is active only while there is someone subscribing to it.
You can subscribe to state changes like this:
class UserActivity: AppCompatActivity() { private val userViewModel: UserViewModel by viewModel() private val coroutineScope = MainScope() override fun onCreate(...) { coroutineScope.launch { userViewModel.state.collect { state -> updateState(state) } } } fun updateState(state: UserState) { ... } override fun onDestroy(...) { coroutineScope.cancel() } }
Example¶
Here's an example of how to use the VectorViewModel:
data class UserState( val user: User? = null, isError: Boolean = false, isLoading: Boolean = false ): VectorState class UserViewModel( initState: UserState, private val repository: UserRepository ): VectorViewModel<UsersState>(initState) { init { viewModelScope.launch { getUserDetails() } } private suspend fun getUserDetails() { setState { copy(isLoading = true) } val users = repository.getUser() if (user == null) { setState { copy(user = null, isError = true, isLoading = false) } } else { setState { copy(usersList = users, isLoading = false) } } } } class UserFragment: VectorFragment() { private val viewModel: UserViewModel by fragmentViewModel { initialState, savedStateHandle -> UserViewModel(initialState, savedStateHandle) } override fun onCreate(...) { renderState(viewModel) { state -> ... } } }
Warning
ViewModels only survive configuration changes such as screen rotations. They do NOT survive process death.
State Persistence¶
While ViewModels are great for storing UI state because they survive configuration changes, you still need to take care of persisting your UI state in the event of a process death.
To make this process easier, the library ships with a specialized version of VectorViewModel
, named the SavedStateVectorViewModel
which leverages the ViewModel SavedState Module for state persistence.