I need to observe change in list which I am fetching from firebase
viewModel.courseList.observe(viewLifeCycleOwner,{
})
But I'm not able to use viewLifeCycleOwner.
class MyViewModel: ViewModel() {
private var _courseList = MutableLiveData<Whatever>()
var courseList: LiveData<List<Whatever>> = _courseList
}
#Composable
fun MyComposable() {
val list = myViewModel.courseList.observeAsState().value
}
Demo app has plenty of examples:
https://github.com/JohannBlake/Jetmagic
Related
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`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<>
I have a property with late initialization.
Now I want to provide a live data which does not emit anything until the property is initialized completely.
How to do this in proper Kotlin way?
class SomeConnection {
val data: Flow<SomeData>
...
class MyViewModel {
private lateinit var _connection: SomeConnection
// private val _connection: CompletableDeferred<SomeConnection>()
val data = _coonection.ensureInitilized().data.toLiveData()
fun connect(){
viewModelScope.launch {
val conn = establishConnection()
// Here I have to do something for the call ensureInitilized to proceed
}
}
private suspend fun establishConnection(){
...
}
Declare a MutableLiveData emitting values of type SomeConnection and a corresponding LiveData.
private val _connectionLiveData = MutableLiveData<SomeConnection>()
val connectionLiveData: LiveData<SomeConnection> = _connectionLiveData
Then assign value to _connectionLiveData when _connection is initialized:
if (::_connection.isInitialized()) _connectionLiveData.value = _connection
(or _connectionLiveData.postValue(_connection) if your code works concurrently)
Now observe this LiveData in another place in code, I'll use fragment here for the sake of example:
override fun firstOnViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.connectionLiveData.observe(this, ::sendData)
}
Then send the desired data via the corresponding view model method
class RequestViewModel(private val repository: RequestRepository) : ViewModel() {
// request currency type
private val currencySearchType = MutableLiveData<String>()
val requests: LiveData<PagedList<Request>> = repository.getRequests(currencySearchType.value!!)
fun updateSearchType(type: String) {
currencySearchType.postValue(type)
}}
above is the code in my viewModel.
private fun initAdapter() {
recyclerView.adapter = adapter
viewModel.requests.observe(viewLifecycleOwner, Observer {
adapter.submitList(it)
})
}
and this is the code in fragment. So basically, what i'm doing is I observe "requests" in viewModel, and in fragment, I will also call updateSearchType to update the currencySearchType. I was hoping that once currencySearchType changed, then the viewmodel.requests will change too. But it turns out the requests never gotta called again. Does anyone know where it went wrong? Appreciate for the help!
Changing the value of currencySearchType does not triggers live data you just using it as function parameter. You have to use Transformation for this.
https://developer.android.com/reference/android/arch/lifecycle/Transformations
class RequestViewModel(private val repository: RequestRepository) : ViewModel() {
// request currency type
lateinit var currencySearchType:String
val requests: LiveData<PagedList<Request>> = Transformations.switchMap(currencySearchType) { repository.getRequests(it) }
fun updateSearchType(type: String) {
currencySearchType = type
}}