I'm using a pattern that I've used a few times before to instantiate a ViewModel object. In this case, the data is saved as a string in SharedPreferences. I just need to read that string, parse it to the correct object, and assign that object as the value to my view model.
But when I do the assignment, I create an infinite loop.
class UserDataViewModel(private val prefs: SharedPreferences): ViewModel() {
val userData: MutableLiveData<UserData> by lazy {
MutableLiveData<UserData>().also {
val userDataString = prefs.getString(Authenticator.USER_DATA, "")
val ud = Gson().fromJson(userDataString, UserData::class.java)
userData.value = ud // infinite loop is here
}
}
fun getUserData(): LiveData<UserData> {
return userData
}
}
This is in onCreateView() of the fragment that keeps the reference to the ViewModel:
userDataViewModel = activity?.run {
ViewModelProviders
.of(this, UserDataViewModelFactory(prefs))
.get(UserDataViewModel::class.java)
} ?: throw Exception("Invalid Activity")
userDataViewModel
.getUserData()
.observe(this, Observer {
binding.userData = userDataViewModel.userData.value
})
FWIW, in the fragment, I have break points on both getUserData() and on binding.userData.... The last break point that gets hit is on getUserData().
I don't see where the loop is created. Thanks for any help.
The userData field is only initialized once the by lazy {} block returns. You're accessing the userData field from within the by lazy {} block and that's what is creating the loop - the inner access sees that it hasn't finishing initializing, so it runs the block again..and again and again.
Instead, you can access the MutableLiveData you're modifying in the also block by using it instead of userData, breaking the cycle:
val userData: MutableLiveData<UserData> by lazy {
MutableLiveData<UserData>().also {
val userDataString = prefs.getString(Authenticator.USER_DATA, "")
val ud = Gson().fromJson(userDataString, UserData::class.java)
it.value = ud
}
}
Related
I'm having an issue trying to display the data saved in my DataStore on startup in Jetpack Compose.
I have a data store set using protocol buffers to serialize the data. I create the datastore
val Context.networkSettingsDataStore: DataStore<NetworkSettings> by dataStore(
fileName = "network_settings.pb",
serializer = NetworkSettingsSerializer
)
and turn it into a livedata object in the view model
val networkSettingsLive = dataStore.data.catch { e ->
if (e is IOException) { // 2
emit(NetworkSettings.getDefaultInstance())
} else {
throw e
}
}.asLiveData()
Then in my #Composable I try observing this data asState
#Composable
fun mycomposable(viewModel: MyViewModel) {
val networkSettings by viewModel.networkSettingsLive.observeAsState(initial = NetworkSettings.getDefaultInstance())
val address by remember { mutableStateOf(networkSettings.address) }
Text(text = address)
}
I've confirmed that the data is in the datastore, and saving properly. I've put some print statements in the composible and the data from the datastore makes it, eventually, but never actually displays in my view. I want to say I'm not properly setting my data as Stateful the right way, but I think it could also be not reading from the data store the right way.
Is there a display the data from the datastore in the composable, while displaying the initial data on start up as well as live changes?
I've figured it out.
What I had to do is define the state variables in the composable, and later set them via a state controlled variable in the view model, then set that variable with what's in the dataStore sometime after initilization.
class MyActivity(): Activity {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
MainScope.launch {
val networkSettings = viewModel.networkSettingsFlow.firstOrNull()
if (networkSettings != null) {
viewModel.mutableNetworkSettings.value = networkSettings
}
}
}
}
class MyViewModel(): ViewModel {
val networkSettingsFlow = dataStore.data
val mutableNetworkSettings = mutableStateOf(NetworkSettings.getInstance()
}
#Composable
fun NetworkSettings(viewModel: MyViewModel) {
val networkSettings by viewModel.mutableNetworkSettings
var address by remember { mutableStateOf(networkSettings.address) }
address = networkSettings.address
Text(text = address)
}
I've been using Datastore for a long time. Today i had to read the values in the main thread. After reviewing the documentation, I decided to use runblocking. I created a long value which name is lastInsertedId.
I reading lastInsertedId in Fragment A then navigated to Fragment B and I'm changing the value of lastInsertedId. When i pop back to Fragment A i read lastInsertedId again. But lastInsertedId's value was still same. Actually it's value is changing but i can't read its last value.
I think it was because Fragment A was not destroyed. Only onDestroyView called and created from onCreateView. What i want is i need to access lastInsertedID's current value whenever i want in main thread.
When i create it as a variable, it always returns the same value. But when i convert it to function it works well. But i don't think this is the best practices. What's the best way to access this value?
Thanks.
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "main")
#Singleton
class DataStoreManager #Inject constructor(#ApplicationContext appContext: Context) {
private val mainDataStore = appContext.dataStore
suspend fun setLastInsertedId(lastId: Long) {
mainDataStore.edit { main ->
main[LAST_INSERTED_ID] = lastId
}
}
// Returns always the same value
val lastInsertedId: Long = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
// Returns as expected
fun lastInsertedId(): Long = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
// This is also work perfectly but i need to access in main thread.
val lastInsertedId : Flow<Long> = mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: Constants.DEFAULT_FOOD_ID
}
companion object {
private val LAST_INSERTED_ID = longPreferencesKey("last_inserted_id")
}
}
You must add get() to your val definition like this.
val lastInsertedId: Long get() = runBlocking {
mainDataStore.data.map { preferences ->
preferences[LAST_INSERTED_ID] ?: 0
}.first()
}
You seems don't understand the differece between variable and function, take a look at this:
fun main() {
val randomNum1 = (1..10).random()
repeat(5) { println(randomNum1) }
repeat(5) { println(getRandomNum()) }
}
fun getRandomNum() = (1..10).random()
Output:
2
2
2
2
2
8
7
8
10
1
Variable holds a value, and it doesn't change until you assign it a new value.
I have a huge understanding problem here, I have a ecommerce app and I cannot properly calculate value of users cart.
The problem is, my solution works well to the point but I have an issue when there are no products in the cart. Obviously LiveData observer or switchMap will not get executed when it's value is empty.
It seems like something trivial, only thing I want to do here is handle the situation when user have no products in the cart. Is the livedata and switchMap a wrong approach here?
I get userCart from the repo -> I calculate its value in the viewModel and expose it to the view with dataBinding.
#HiltViewModel
class CartFragmentViewModel
#Inject
constructor(
private val repository: ProductRepository,
private val userRepository: UserRepository,
private val priceFormatter: PriceFormatter
) : ViewModel() {
private val user = userRepository.currentUser
val userCart = user.switchMap {
repository.getProductsFromCart(it.cart)
}
val cartValue = userCart.switchMap {
calculateCartValue(it)
}
private fun calculateCartValue(list: List<Product>?): LiveData<String> {
val cartVal = MutableLiveData<String>()
var cartValue = 0L
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
return cartVal
}
fun removeFromCart(product: Product) {
userRepository.removeFromCart(product)
getUserData()
}
private fun getUserData() {
userRepository.getUserData()
}
init {
getUserData()
}
}
Default value is to solve the "initial" empty cart.
Now if you need to trigger it when there's no data... (aka: after you remove items and the list is now empty), I'd use a sealed class to wrap the actual value.
(names and code are pseudo-code, so please don't copy-paste)
Something like this:
Your Repository should expose the cart, user, etc. wrapped in a sealed class:
sealed class UserCartState {
object Empty : UserCartState()
data class HasItems(items: List<things>)
object Error(t: Throwable) :UserCartState() //hypotetical state to signal problems
}
In your CartFragmentViewModel, you observe and use when (for example), to determine what did the repo responded with.
repo.cartState.observe(...) {
when (state) {
is Empty -> //deal with it
is HasItems -> // do what it takes to convert it, calculate it, etc.
is Error -> // handle it
}
}
When the user removes the last item in the cart, your repo should emit Empty.
The VM doesn't care how that happened, it simply reacts to the new state.
The UI cares even less. :)
You get the idea (I hope).
That's how I would look into it.
You can even use a flow of cart items, or the new "FlowState" thingy (see the latest Google I/O 21) to conserve resources when the lifecycle owner is not ready.
I suppose that this part of code creates the problem
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
Probably, list is not null but is empty. Please try this:
if (list.isNullOrEmpty) {
list.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} else {
cartVal.postValue(priceFormatter.formatPrice(0))
}
after hours of trying and searching I'm still stuck and hope somebody can help me.
I'm trying to get Live Updates from Firestore (using a SnapshotListener) through my ViewModel to my Activity, but all my attemps failed. Below is my current setup - trying to update LiveData in my Repository and handing it over to the View Model...
My problem:
I have UserData (inside a document) in my Firestore Collection. I try to listen to changes to the current user at runtime using the observeUserData() Function. The Data provided by this function (on Document Change) should be send through my ViewModel to my Activiy. If I change something to my User document in Firestore the SnapshotListener fires as expected, but the update is not reaching my ViewModel and therefore is not reaching my Activity.
Is anybody able to help me archive this? The only solution I see right now is to add a SnapshotListener within my ViewModel but as far as I know this is bad practice?
Thank you very much.
Firestore Repository
object FirestoreService {
val db = FirebaseFirestore.getInstance()
val userDataLiveData = MutableLiveData<UserData>()
fun observeUserData(userId: String) {
try {
db.collection("userData").document(userId).addSnapshotListener{ documentSnapshot: DocumentSnapshot?, firebaseFirestoreException: FirebaseFirestoreException? ->
firebaseFirestoreException?.let {
Log.e(TAG, firebaseFirestoreException.toString())
return#addSnapshotListener
}
val data = documentSnapshot?.toUserData()
data?.let {
Log.d(TAG, "post new value")
userDataLiveData.postValue(data)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error getting user data", e)
}
}
ViewModel
class MyViewModel (private val uid : String) : ViewModel() {
private var _userData = MutableLiveData<UserData>()
val userData: LiveData<UserData> = _userData
fun getUserData() {
Log.d(TAG, "getUserData called")
FirestoreService.observeUserData(uid)
var data = FirestoreService.userDataLiveData
_userData = data
}
}
Activity
//ViewModel Setup
val factory = MyViewModelFactory(user.uid.toString())
val viewModel = ViewModelProvider(this, factory).get(MyViewModel::class.java)
//Initialize UserData
viewModel.getUserData()
viewModel.userData.observe(this, Observer {
userData = it
Log.d(TAG, userData.toString())
})
You need to initialise your viewModel first
lateinit var userViewModel : MyViewModel
userViewModel = new ViewModelProvider(this).get(MyViewModel::class.java)
Then use the getViewLifeCycleOwner method on your userViewModel, and then observe the LiveData which would have your userDataModel
The onChanged() method would be where you can access the methods(getters & setters) within your userDataModel
There is a much easier way to do this without any listener. First, add this dependency:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.9"
Then lets say you have the following Repository:
inteface FirestoreService {
fun sendUserData(uid: String): LiveData<String> // Or whatever it should emit
}
It's implementation which emits livedata could be the following:
class FirestoreServiceImpl : FireStoreService {
val db = FirebaseFirestore.getInstance()
fun sendUserData(uid: String): LiveData<String> = liveData {
val mySnapshot = db.collection("userData)".document(userId).await()
emit(mySnapshot?.toUserData)
}
}
If you want to call this method in the viewModel, it would be better to change LiveData with Flow
fun sendUserData(uid: String): Flow<String> = flow {
val mySnapshot = db.collection("userData)".document(userId).await()
emit(mySnapshot?.toUserData)
}
In your ViewModel you than could do this:
val myInput = "TestUUID"
val myUserID = sendUserData(myInput).asLiveData()
And then, finally in the fragment / activity:
yourViewModel.myUserID.observe(viewLifecycleOwner) {
// do stuff here
}
Thank you all for your replies - fortunately I found my problem on this one.
I think i mixed up too much stuff from different "guides" / "approaches" - luckily I found Doug Stevenson's fantastic session below which got me out of this:
https://www.youtube.com/watch?v=WXc4adLMDqk
Thank you Doug :)
I want to verify that my MediatorLiveData has an actual value set:
val entries = MediatorLiveData<List<Entry>>()
get() = when {
entries.value == null -> throw IllegalStateException("wah")
else -> field
}
I get no compiler errors, but when running the code I get an StackOverflow, because the getter is called over and over again in the (entries.value == null) path.
1. How to throw an exception in a custom getter and stop the app from running
UPDATE WITH FINAL SOLUTION:
Thanks to #zapl and #kcoppock for your answers. Both of them helped me to get to my final solution:
private lateinit var _entries: LiveData<List<Entry>>
val entries = MediatorLiveData<List<Entry>>()
get() = when {
!::_entries.isInitialized -> throw IllegalStateException("EntryListViewModel was not initialized. Please call init() first.")
else -> field
}
fun init() {
_entries = getEntries(false)
entries.addSource(_entries) { entries.value = it.orEmpty() }
}
fun refreshEntries() {
entries.removeSource(_entries)
_entries = getEntries(true)
entries.addSource(_entries) { entries.value = it.orEmpty() }
}
I also learned from another source about .isInitialized for lateinit vars which can be used for exactly what I needed. Also the graceful fallback to an empty list was good idea.
What I would do is keep the LiveData private and surface a separate accessor for the checked property:
private val _entries = MediatorLiveData<List<Entry>>()
val entries: List<Entry>
get() = _entries.value!!
Better yet, unless you explicitly want to crash in this case, you could just return an empty list instead:
private val _entries = MediatorLiveData<List<Entry>>()
val entries: List<Entry>
get() = _entries.value.orEmpty()
That said, the point of LiveData is to use an observer, so you only get an update when a value is posted.
EDIT: If your goal is to force an initial value, you could create a subclass that enforces this:
class NonNullMediatorLiveData<T>(initialValue: T) : MediatorLiveData<T>() {
init { value = initialValue }
override fun getValue(): T = super.getValue()!!
override fun setValue(value: T) {
// assert since the parent is defined in Java
super.setValue(value!!)
}
}