I wanted to know what is the difference between the two approaches for settings values in the viewmodels:
Approach one is using function to set the new value to the variable. The second approach is using the setter to set the value to the variable.
I know it is not recommended to expose mutable variables to the view but the execution is the same if we call the function or set the variable in the views.
A:
``
class SampleViewModel(): ViewModel {
private val _title = MutableLiveData<String>()
val title: LiveData<String>
get() = _title
// Setting the title
fun setTitle(newTitle: String) {
_title.value = newTitle
}
}
B:
class SampleViewModel(): ViewModel {
private val _title = MutableLiveData<String>()
val title: LiveData<String>
get() = _title
// Setting the title
var setTitle: String
set(value) = {
field = value
_title.value = value
}
}
Any input is appreciated.
I tried both approaches and it is working fine on both cases.
Related
I'm trying to implement a "settings" screen for my app using DataStore pereferences. Basically, an url is created, according to the preferences, so a WebView can load it later. The data is saved correctly, and the UI from the settings screen is updating the values when a preference is changed.
The problem is that I found out, when trying to load an updated url, that the preferences are not read instantly (the WebView was loading a previous url value after being updated). So my question is how to convert it to that type so the preferences are read in real time?
Here are some code snippets:
UserPreferences Data class
data class UserPreferences(
val conexionSegura: Boolean,
val servidor: String,
val puerto: String,
val pagina: String,
val parametros: String,
val empresa: String,
val inicioDirecto: Boolean,
val usuario: String,
val password: String,
val url: String
)
DataStore repository impl getPreferences()
class DataStoreRepositoryImpl #Inject constructor(
private val dataStore: DataStore<Preferences>
) : DataStoreRepository {
override suspend fun getPreferences() =
dataStore.data
.map { preferences ->
UserPreferences(
conexionSegura = preferences[PreferencesKeys.CONEXION_SEGURA] ?: false,
servidor = preferences[PreferencesKeys.SERVIDOR] ?: "",
puerto = preferences[PreferencesKeys.PUERTO] ?: "",
pagina = preferences[PreferencesKeys.PAGINA] ?: "",
parametros = preferences[PreferencesKeys.PARAMETROS] ?: "",
empresa = preferences[PreferencesKeys.EMPRESA] ?: "",
inicioDirecto = preferences[PreferencesKeys.INICIO_DIRECTO] ?: false,
usuario = preferences[PreferencesKeys.USUARIO] ?: "",
password = preferences[PreferencesKeys.PASSWORD] ?: "",
url = preferences[PreferencesKeys.URL] ?: CONTENT_URL
)
}
...
...
ViewModel readPreferences()
private val _state = mutableStateOf(SettingsState())
val state: State<SettingsState> = _state
init {
readPreferences()
updateUrl()
}
private fun readPreferences() {
viewModelScope.launch {
dataStoreRepositoryImpl.getPreferences().collect {
_state.value = state.value.copy(
conexionSegura = it.conexionSegura,
servidor = it.servidor,
puerto = it.puerto,
pagina = it.pagina,
parametros = it.parametros,
empresa = it.empresa,
inicioDirecto = it.inicioDirecto,
usuario = it.usuario,
password = it.password
)
}
}
}
...
...
After investigating and reading a little bit, I realised that DataStore emits flows, not stateFlows, which would be needed in compose. I thought about deleting the UserPreferences class, and simply read one preference at a time, collecting it asState inside the screen composable. But, I think the code would be cleaner of I don't do that :) I really like the idea of using a separate class to hold the preferences' values
When you want to transform a Flow into a StateFlow in a ViewModel to be consumed in the view, the correct way is using the stateIn() method.
Use of stateIn
Assuming you have the following class and interface :
class SettingsState()
sealed interface MyDatastore {
fun getPreferences(): Flow<SettingsState>
}
In your viewModel, create a val that will use the datatStore.getPreferences method and transform the flow into a stateFlow using stateIn
class MyViewModel(
private val dataStore: MyDatastore
) : ViewModel() {
val state: StateFlow<SettingsState> = dataStore
.getPreferences()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SettingsState()
)
}
Collect in the composable
To start getting preferences, you have just to collect the stateFlow as state :
#Composable
fun MyComposable(
myViewModel: MyViewModel = hiltViewModel()
) {
val state = myViewModel.state.collectAsState()
//...
}
Pros
As you can see, you don't need to use init in the ViewModel. Like 90% of time, use of initis not required. The ViewModel became more testable because you don't need to mock everything that is in the init block.
I want to implement search functionality among lists of banks in my app.
So somehow I need to use stringResId() but you can't call it without composable func(). Also by using Resources.getSystem().getString() is giving me resources not found exception.
This is my viewModel code
class BankViewModel: ViewModel() {
private val _bankAccount = MutableStateFlow(BankAccount())
val bankAccount: StateFlow<BankAccount> = _bankAccount.asStateFlow()
var bankList = mutableStateOf(Banks)
private var cachedBankList = listOf<Bank>()
private var isSearchStarting = true
var isSearching = mutableStateOf(false)
fun updateBankSearch(searchName: String) {
_bankAccount.update { bankAccount ->
bankAccount.copy(bankName = searchName)
}
}
fun searchBankName(query: String) {
val listToSearch = if(isSearchStarting) {
bankList.value
}else {
cachedBankList
}
viewModelScope.launch {
if (query.isEmpty()) {
bankList.value = cachedBankList
isSearching.value = false
isSearchStarting = true
return#launch
}
val results = listToSearch.filter {
Resources.getSystem().getString(it.bankName).contains(query.trim(), ignoreCase = true)
}
if (isSearchStarting) {
cachedBankList = bankList.value
isSearchStarting = false
}
bankList.value = results
isSearching.value = true
}
}
}
This is my Bank
data class Bank (
#StringRes val bankName: Int,
#DrawableRes val bankLogo: Int = R.drawable.bank_image_2
)
So my question is how can I get a string by using id so that I can compare it with the query??
You need to use the Application Context to retrieve resources like these.
If you're not using any DI framework, or you don't want to use custom ViewModelProvider.Factory, extend AndroidViewModel (which holds a reference to Application) instead of regular ViewModel.
Ideally ViewModels shouldn't contain any Android-specific objects, and you would use some custom StringProvider, or something similar, but that's not a topic of your question.
In ViewModel:
val drillerCatList: List<Int> = emptyList()
val shownCategoriesFlow = wordDao.getShownCategories() // which returns type Flow<List<CategoryItem>>
Catigory object:
data class CategoryItem(
val categoryName: String,
val categoryNumber: Int,
val categoryShown: Boolean = false,
#PrimaryKey(autoGenerate = true) var id: Int = 0
) : Parcelable {
}
How can I retrieve all categoryNumber values from shownCategoriesFlow: FLow<List> and fill drillerCatList: List with these values in ViewModel?
First make drillerCatList mutable as shown below:
val drillerCatList: ArrayList<Int> = ArrayList()
Now collect the list from the shownCategoriesFlow:
shownCategoriesFlow.collect {
it.forEach{ categoryItem ->
drillerCatList.add(categoryItem.categoryNumber)
}
}
First, your property either needs to be a MutableList or a var. Usually, var with a read-only List is preferable since it is less error-prone. Then you call collect on your Flow in a coroutine. collect behaves sort of like forEach does on an Iterable, except that it doesn't block the thread in between when elements are ready.
val drillerCatList: List<Int> = emptyList()
val shownCategoriesFlow = wordDao.getShownCategories()
init { // or you could put this in a function do do it passively instead of eagerly
viewModelScope.launch {
shownCategoriesFlow.collect { drillerCatList = it.map(CategoryItem::categoryNumber) }
}
}
Alternate syntax:
init {
shownCategoriesFlow
.onEach { drillerCatList = it.map(CategoryItem::categoryNumber) }
.launchIn(viewModelScope)
}
I have connect my android application to firebase and am using it to retrieve Authentication details and data from firestone. I am using an MVVM architecture and live data for this. The problem is that I need to retrieve email address first and then used this data to query the firestone which contain documents with ID = emailID. You can see my viewmodel. The value for the emailID is null when every I run this. How can I accomplish this while following the MVVP style of coding ?
#Edit: I need to understand how can check if the live data has been initialised with a value in the case where one livedata value depends on the other.
class ProfileViewModel(): ViewModel() {
var random =""
private var _name = MutableLiveData<String>()
val userName
get()=_name
private var _post = MutableLiveData<String>()
val userPost
get()=_post
private var _imgUrl = MutableLiveData<Uri>()
val userImgUrl
get()=_imgUrl
private var _emailId = MutableLiveData<String>()
val userEmailId
get()=_emailId
init{
getUserDataFromProfile()
getUserPostFromFirestone()
}
private fun getUserPostFromFirestone() {
val mDatabaseInstance: FirebaseFirestore = FirebaseFirestore.getInstance()
// _emailId.observe(getApplication(), Observer {
//
// } )
if(_emailId.value!=null){
mDatabaseInstance.collection("users").document(_emailId.value)
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
_post.value = task.result?.data?.get("post").toString()
} else {
// Log.w("firestone", "Error getting documents.", task.exception)
_post.value = "Unable to Retrieve"
}
}
}
}
private fun getUserDataFromProfile() {
val mAuth = FirebaseAuth.getInstance()
val currentUser = mAuth.currentUser
random = currentUser?.displayName!!
_name.value = currentUser?.displayName
_post.value = "Unknown"
_imgUrl.value = currentUser?.photoUrl
_emailId.value = currentUser?.email
}
}
If you write a wrapper over the Firebase call and expose it as a LiveData (or, in this case, I'll pretend it's wrapped in a suspendCoroutineCancellable), in which case whenever you want to chain stuff, you either need MediatorLiveData to combine multiple LiveDatas into a single stream (see this library I wrote for this specific purpose) or just switchMap.
private val auth = FirebaseAuth.getInstance()
val imgUrl: LiveData<Uri> = MutableLiveData<Uri>(auth.currentUser?.photoUrl)
val emailId: LiveData<String> = MutableLiveData<String>(auth.currentUser?.email)
val post = emailId.switchMap { emailId ->
liveData {
emit(getUserByEmailId(emailId))
}
}
you can set observer to LiveData and remove it when you don't need it:
class ProfileViewModel : ViewModel() {
private val _email = MutableLiveData<String>()
private val emailObserver = Observer<String> { email ->
//email is here
}
init {
_email.observeForever(emailObserver)
}
override fun onCleared() {
_email.removeObserver(emailObserver)
super.onCleared()
}
}
Try using coroutines for the sequential execution of the code. so once you get the output of one and then the second one starts executing. If this isnt working Please let me know i can try help you.
init{
viewModelScope.launch{
getUserDataFromProfile()
getUserPostFromFirestone()
}
}
I am trying to use a data class but I can't figure out how to save the data properly.
I have created a data class:
data class SavedValue( var currentValue:String, var previousValue:String = "")
What I want is each time I am want to save a new currentValue, the already saved value for current is copy to previousValue and the new currentValue overwrite the currentValue field.
Thanks for the help
A data class in Kotlin is not supposed to provide such functionalities, as they are designed to hold data.
You could just use a simple class.
Nevertheless you can achieve what you want (with or without a data class), but you will have to move currentValue inside the class and use a setter (and a getter).
In its place use a private property like _currentValue:
data class SavedValue(private var _currentValue: String, var previousValue: String = "") {
var currentValue: String
get() = _currentValue
set(value) {
previousValue = _currentValue
_currentValue = value
}
}
This code:
val sv = SavedValue("abc")
println("currentValue = ${sv.currentValue} and previousValue = ${sv.previousValue}")
will print:
currentValue = abc and previousValue =
and this:
sv.currentValue = "ABC"
println("currentValue = ${sv.currentValue} and previousValue = ${sv.previousValue}")
will print:
currentValue = ABC and previousValue = abc
Also, I think that you need previousValue as a read only property, right?
So move it too inside the class and make its setter private:
data class SavedValue(private var _currentValue: String) {
var _previousValue: String = ""
var currentValue: String
get() = _currentValue
set(value) {
previousValue = _currentValue
_currentValue = value
}
var previousValue: String
get() = _previousValue
private set(value) {
_previousValue = value
}
}
What you are trying to achieve isn't straight forward using data class. Instead, you can use POJO class and use custom setter and getter.
class SavedValue(currentValue: String, previousValue: String) {
private var _currentValue: String = currentValue
private var _previousValue: String = previousValue
var currentValue: String
get() {
return _currentValue
}
set(value) {
_previousValue = _currentValue
_currentValue = value
}
override fun toString(): String {
return "SavedValue(_currentValue='$_currentValue',
_previousValue='$_previousValue')"
}
}
The first solution works fine but if you do not want the third field just to hold the current value you can do:
data class SavedValue(var previousValue: String = "") {
var currentValue: String = ""
set(value) {
if (field != value) previousValue = field
field = value
}
}
E.g.
val savedValue = SavedValue()
savedValue.currentValue = "initial value"
println("current: ${savedValue.currentValue} - previous: ${savedValue.previousValue}")
savedValue.currentValue = "second value"
println("current: ${savedValue.currentValue} - previous: ${savedValue.previousValue}")
savedValue.currentValue = "third value"
println("current: ${savedValue.currentValue} - previous: ${savedValue.previousValue}")
Outputs:
I/System.out: current: initial value - previous:
I/System.out: current: second value - previous: initial value
I/System.out: current: third value - previous: second value
Or if you want the non-mutable previousValue you'll need the third field:
data class SavedValue(private var _previousValue: String = "") {
var currentValue: String = ""
set(value) {
if (field != value) _previousValue = field
field = value
}
val previousValue: String
get() = _previousValue
}