I don't understand why in a lot of Google's official examples, they use Flow combine function to combine from 2-10 different flows into a viewstate data object.
Is there a specific reason to do this? (other than possibly tidier code?)
They even make a boolean that is set in a button onClick into a flow for the sake of making the boolean into that viewstate object. (For example, the selectedCategory variable below is changed in some kind of tabview callback)
Personally I would have made that boolean variable into a MutableState. I don't see why to make it into a flow...
data class DiscoverViewState(
val categories: List<Category> = emptyList(),
val selectedCategory: Category? = null
)
class DiscoverViewModel(
...
) : ViewModel() {
private val _state = MutableStateFlow(DiscoverViewState())
val state: StateFlow<DiscoverViewState>
get() = _state
}
#Composable
fun Discover(
modifier: Modifier = Modifier
) {
val viewModel: DiscoverViewModel = viewModel()
val viewState by viewModel.state.collectAsState()
}
Related
My app uses hilt and I have some work with LoadManager inside my activity that read contacts using ContentResolver and when I finish work I get the cursor that I send to my viewModel in order to process the data and do some business logic which for that I declared the following on top of my activity :
#AndroidEntryPoint
class MainActivity : ComponentActivity(), LoaderManager.LoaderCallbacks<Cursor> {
private val contactsViewModel: ContactsViewModel by viewModels()
...
such that I use it inside onLoadFinished :
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
contactsViewModel.updateContactsListFromCursor(cursor, loader.id)
}
Inside my viewModel I have the following code which updates the ui state of the list with the contacts to be displayed:
data class ContactsListUiState(
val contacts: MutableList<Contact>,
val searchFilter: String)
#HiltViewModel
class ContactsViewModel #Inject constructor() : ViewModel() {
private val _contactsListUiState =
MutableStateFlow(ContactsListUiState(mutableStateListOf(), ""))
val contactsListUiState: StateFlow<ContactsListUiState> = _contactsListUiState.asStateFlow()
private fun updateContactsList(filter: String) {
viewModelScope.launch(Dispatchers.IO) {
...
_contactsListUiState.update { currentState ->
currentState.copy(contacts = list, searchFilter = filter)
}
}
Finally, I am supposed to display the contacts that a LazyColumn and I pass the viewModel to my composable function using hilt following the official documentation :
#Composable
fun ContactsListScreen(
navController: NavController,
modifier: Modifier = Modifier, viewModel: ContactsViewModel = hiltViewModel()
) {
val uiState by viewModel.contactsListUiState.collectAsStateWithLifecycle()
...
But when i access uiState.contacts it is empty and my lists does not show anything and I also noticed that the contactsViewModel which I used in the activity is not the same viewModel instance that I got from hiltViewModel() inside the composable function which probably causes this problem..
Any suggestions how to share the sameViewModel between the activity and the composable functions assuming that I have to call the viewModel from the onLoadFinished function(which is not composable) where I get the cursor therefore I must have a viewModel reference inside the activity itself
Based on the docs.
The function hiltViewModel() returns an existing ViewModel or creates
a new one scoped to the current navigation graph present on the
NavController back stack. The function can optionally take a
NavBackStackEntry to scope the ViewModel to a parent back stack entry.
It turns out the factories create a new instance of the ViewModel when they are part of a Navigation Graph. But since you already found out that to make it work you have to specify the ViewModelStoreOwner, so I took an approach based my recent answer from this post, and created a CompositionLocal of the current activity since its extending ComponentActivity being it as a ViewModelStoreOwner itself.
Here's my short attempt that reproduces your issue with the possible fix.
Activity
#AndroidEntryPoint
class HiltActivityViewModelActivity : ComponentActivity() {
private val myViewModel: ActivityScopedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalActivity provides this#HiltActivityViewModelActivity) {
Log.e("ActivityScopedViewModel", "Hashcode: ${myViewModel.hashCode()} : Activity Scope")
HiltActivitySampleNavHost()
}
}
}
}
ViewModel
#HiltViewModel
class ActivityScopedViewModel #Inject constructor(): ViewModel() {}
Local Activity Composition
val LocalActivity = staticCompositionLocalOf<ComponentActivity> {
error("LocalActivity is not present")
}
Simple Navigation Graph
enum class HiltSampleNavHostRoute {
DES_A, DES_B
}
#Composable
fun HiltActivitySampleNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = HiltSampleNavHostRoute.DES_A.name
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(HiltSampleNavHostRoute.DES_A.name) {
DestinationScreenA()
}
composable(HiltSampleNavHostRoute.DES_B.name) {
DestinationScreenB()
}
}
}
Screens
// here you can use the Local Activity as the ViewModelStoreOwner
#Composable
fun DestinationScreenA(
myViewModelParam: ActivityScopedViewModel = hiltViewModel(LocalActivity.current)
// myViewModelParam: ActivityScopedViewModel = viewModel(LocalActivity.current)
) {
Log.e("ActivityScopedViewModel", "Hashcode: ${myViewModelParam.hashCode()} : Composable Scope")
}
#Composable
fun DestinationScreenB(
modifier: Modifier = Modifier
) {}
Or better yet, like from this answer by Phil Dukhov, you can use LocalViewModelStoreOwner as the parameter when you invoke the builder.
Same NavHost
#Composable
fun HiltActivitySampleNavHost(
...
) {
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(HiltSampleNavHostRoute.DES_A.name) {
DestinationScreenA(
myViewModelParam = viewModel(viewModelStoreOwner)
)
}
...
}
}
Both logs from the activity and the composable in the nav graph shows the same hashcode
E/ActivityScopedViewModel: Hashcode: 267094635 : Activity Scope
E/ActivityScopedViewModel: Hashcode: 267094635 : Composable Scope
Also have a look at Thracian's answer. It has a very detailed explanation about ComponentActivity, and based from it I think my first proposed solution would probably work in your case.
I have a composable with viewmodel and I want to pass an id from the composable to the viewmodel.
My composable is:
#Composable
fun HttpRequestScreen(
viewModel: HttpRequestViewModel = hiltViewModel(),
id: String = EMPTYUUID,
onClick: (String, String, Int) -> Unit // respond, request Function: 1 - send request, 2 - edit request
) {
I have the id from a different screen and I want to pass it to my Hilt viewmodel.
Assuming that you have followed the documentation for compose navigation, you can find your parameter here:
#HiltViewModel
class RouteDetailsViewModel #Inject constructor(
private val getRouteByIdUC: GetRouteByIdUC,
private val savedStateHandle: SavedStateHandle
): ViewModel() {
private val routeId = savedStateHandle.get<String>("your-param-name") // for example String in my case
}
You will need to think in a Unidirectional Data Flow pattern, where events flow up and state flows down. For this, you need to expose some sort of state from your viewmodel that sends down the state of the request to the Composable as an observable state.
Your viewmodel could look like this.
class HttpRequestViewModel: ViewModel() {
private val _httpResponse = mutableStateOf("")
val httpResponse: State<String> = _httpResponse
fun onHttpRequest(requestUrl: String) {
//Execute your logic
val result = "result of your execution"
_httpResponse.value = result
}
}
Then in your Composable, you can send events up by calling the ViewModel function on the button click like so
#Composable
fun HttpRequestScreen(viewModel: HttpRequestViewModel) {
val state by viewModel.httpResponse
var userInput = remember { TextFieldValue("") }
Column {
Text(text = "Http Response = $state")
}
BasicTextField(value = userInput, onValueChange = {
userInput = it
})
Button(onClick = { viewModel.onHttpRequest(userInput.text) }) {
Text(text = "Make Request")
}
}
I hope that points you in the right direction. Good luck.
I'm migrating from Shared preference to data store using jetpack compose. everything works fine (data is saved and can be retreated successfully). However, whenever a Data is retrieved, the composable keeps on recomposing endlessly. I'm using MVVM architecture and below is how I have implemented data store.
Below is declared in my AppModule.kt
App module in SingletonComponent
#Provides
#Singleton
fun provideUserPreferenceRepository(#ApplicationContext context: Context):
UserPreferencesRepository = UserPreferencesRepositoryImpl(context)
Then here's my ViewModel:
#HiltViewModel
class StoredUserViewModel #Inject constructor(
private val _getUserDataUseCase: GetUserDataUseCase
): ViewModel() {
private val _state = mutableStateOf(UserState())
val state: State<UserState> = _state
fun getUser(){
_getUserDataUseCase().onEach { result ->
val name = result.name
val token = result.api_token
_state.value = UserState(user = UserPreferences(name, agentCode, token, balance))
}.launchIn(viewModelScope)
}}
Finally, Here's my Repository Implementation:
class UserPreferencesRepositoryImpl #Inject constructor(
private val context: Context
): UserPreferencesRepository {
private val Context.dataStore by preferencesDataStore(name = "user_preferences")
}
private object Keys {
val fullName = stringPreferencesKey("full_name")
val api_token = stringPreferencesKey("api_token")
}
private inline val Preferences.fullName get() = this[Keys.fullName] ?: ""
private inline val Preferences.apiToken get() = this[Keys.api_token] ?: ""
override val userPreferences: Flow<UserPreferences> = context.dataStore.data.catch{
// throws an IOException when an error is encountered when reading data
if (it is IOException) {
emit(emptyPreferences())
} else {
throw it
}
}.map { preferences ->
UserPreferences(name = preferences.fullName, api_token = preferences.apiToken)
}.distinctUntilChanged()
I don't know what causes the composable to recompose. Below Is the composable:
#Composable
fun LoginScreen(
navController: NavController,
userViewModel: StoredUserViewModel = hiltViewModel()
) {
Log.v("LOGIN_SCREEN", "CALLED!")
userViewModel.getUser()
}
If anyone can tell me where I've done wrong please enlighten me. I have tried to change the implementation in AppModule for UserPreferencesRepository but no luck.
Below is UseState.kt which is just a data class
data class UserState(
val user: UserPreferences? = null
)
Below is UserPreferences.kt
data class UserPreferences(val name: String, val api_token: String)
I also faced such problem. The solution was became to navigate with LauchedEffect in composable.
before:
if (hasFlight) {
navController.navigate(Screen.StartMovingScreen.route)
}
after:
if (hasFlight) {
LaunchedEffect(Unit) {
navController.navigate(Screen.StartMovingScreen.route)
}
}
This is expected behaviour: you're calling getUser on each recomposition.
#Composable function is a view builder, and should be side-effects free.
Instead you can use special side effect function, like LaunchedEffect, which will launch job only once, until it's removed from view tree or key argument is changed:
LaunchedEffect(Unit) {
userViewModel.getUser()
}
But this also will be re-called in case of configuration change, e.g. screen rotation. To prevent this case, you have two options:
Call getUser inside view model init: in this case it's guarantied that it's called only once.
Create some flag inside view model to prevent redundant request.
More info about Compose side effects in documentation.
I have a Composable and a viewmodel (VM) for it. The VM gets some data from a kotlin flow which I would like to expose as a State
Usually I would have the VM expose a state like this:
var title by mutableStateOf("")
private set
And I could use it in the Composable like this
Text(text = viewModel.title)
But since the data comes from a flow, i have to expose it like this
#Composable
fun title() = flowOf("TITLE").collectAsState(initial = "")
And have to use it in the Composable like this
Text(text = viewModel.title().value)
I try to minimize boilerplate code, so the .value kind of bothers me. Is there any way to collect the flow as state, but still expose it as viewModel.title or viewModel.title() and get the actual String and not the state object?
You can use delegated property.If your program just read it.
class FlowDeletedProperty<T>(val flow: Flow<T>, var initialValue: T, val scope: CoroutineScope) :
ReadOnlyProperty<ViewModel, T> {
private var _value = mutableStateOf(initialValue)
init {
scope.launch {
flow.collect {
_value.value = it
}
}
}
override fun getValue(thisRef: ViewModel, property: KProperty<*>): T {
return _value.value
}
}
fun <T> ViewModel.flowDeletedProperty(flow: Flow<T>, initialValue: T) =
FlowDeletedProperty(flow, initialValue, viewModelScope)
in viewModel
val a = flow {
while (true) {
kotlinx.coroutines.delay(100L)
println("out ")
emit((100..999).random().toString())
}
}
val title by flowDeletedProperty(a,"")
in ui
Text(text = viewModel.title)
I`m programing with Jetpack Compose.
I request data from net and save it into a ViewModel in a Composable,
but when I want to use the data in other Composable, the ViewModel returns null
// ViewModel:
class PartViewModel : ViewModel() {
private val mPicRepository: PicRepository = PicRepository()
private val _partsResult: MutableLiveData<PicResp> = MutableLiveData()
val partsResilt: LiveData<PicResp> get() = _partsResult
fun getPartsFromImage(id: Long) {
mPicRepository.getCloudPic(id, _partsResult)
}
}
// Composable which request data
#Composable
fun PagePhotoDetail(imageId: Long, navController: NavHostController) {
val vm: PartViewModel = viewModel()
vm.getPartsFromImage(imageId)
partsState.value?.data?.let {
Logger.d(it.parts) // this log show correct data
}
}
// Composable which use data
#Composable
fun PagePartListFromImage(navController: NavHostController) {
val vm: PartViewModel = viewModel()
Logger.d(vm.partsResilt.value) // this log cannot get data and show null
}
You are creating two different instances of your viewmodel. You need to initialise the viewmodel like val vm by viewmodels<PartViewModel>
Then pass this viewmodel as a parameter inside the Composable. You're done!
Well, if you still wish to initialise it inside a Composable, you can use val vm by viewmodel<PartViewModel>.
viewModel<> instead of viewModels<>