Android Databinding with LiveData - Validating multiple fields together - android-databinding

I am using two-way Databinding with LiveData inside a ViewModel to handle a sign-up form.
As fields are filled out, there are fields that need to be evaluated together for validity (as well as the overall form), and only enable the Submit button when everything is correct.
With a regular Observable object and using #Bindable this would be straightforward by just annotating a 'getter' function for each validation that is required and doing the necessary validation steps and return the appropriate result inside each function.
BUT I am working with LiveData. I came up with a solution using MediatoLiveData (see basic example below), but it seems to me like there should be another, perhaps better, approach.
class RegistrationViewModel : ViewModel() {
val email: MutableLiveData<String> by lazy { MutableLiveData<String>() }
val emailConf: MutableLiveData<String> by lazy { MutableLiveData<String>() }
val emailValid: MediatorLiveData<Boolean> = MediatorLiveData()
val emailChanged: (Any) -> Unit = cc# {
//Obviously there is more validation needed but this is just an example
if (email.value != null && emailConf.value != null) {
emailValid.value = email.value.isNotEmpty() && emailConf.value.isNotEmpty() && email.value == emailConf.value
return#cc
}
emailValid.value = false
}
init {
emailValid.addSource(email, emailChanged)
emailValid.addSource(emailConf, emailChanged)
}
}
To tie it to the UI, for example, the enabled property of a button or the visibility property of a label could be bound to the emailValid property appropriately.

Your approach is correct, but code has some design issues:
lazy fields is used in init section. Field become not lazy
emailValid field can be exposed as LiveData
validation can be encapsulated in inner class, but it's not required
class RegistrationViewModel : ViewModel() {
val email = MutableLiveData<String>()
val emailConf = MutableLiveData<String>()
#Suppress("UNCHECKED_CAST")
val emailValid: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
val validator = Validator(::postValue)
addSource(email, validator as Observer<String>)
addSource(emailConf, validator as Observer<String>)
}
private inner class Validator(private val validationConsumer: (Boolean) -> Unit) : Observer<Any> {
override fun onChanged(ignored: Any?) {
//Obviously there is more validation needed but this is just an example
val email = email.value
val emailConf = emailConf.value
validationConsumer(when {
email.isNullOrEmpty() -> false
emailConf.isNullOrEmpty() -> false
email == emailConf -> true
else -> false
})
}
}
}
If you require some heavy validation, you can make hidden mutable field for emailValid and manually update it when background validation completed

Related

Combine search and sort with kotlin flow

I need to search and sort data simultaneously. I did it for search but it wont trigger for sort. I'm also using pagination.
User can type in searchView and flow will trigger, but problem is when i change sortState (ascending or descending) it wont trigger flow for searching articles on api endpoint.
ViewModel:
private val currentQuery = MutableStateFlow(DEFAULT_QUERY)
private val sortState = MutableStateFlow<SortOrderState>(SortOrderState.Ascending)
val flow = currentQuery
.debounce(2300)
.filter {
it.trim().isNotEmpty()
}
.distinctUntilChanged()
.flatMapLatest { query ->
articleRepository.getSearchResult(query.lowercase(Locale.ROOT),sortState.value)
}
Fragment:
lifecycleScope.launch {
viewModel.flow.collectLatest { articles ->
binding.recyclerViewTop.layoutManager = LinearLayoutManager(context)
binding.recyclerViewTop.adapter = adapter.withLoadStateHeaderAndFooter(
header = ArticleLoadStateAdapter { adapter.retry() },
footer = ArticleLoadStateAdapter { adapter.retry() }
)
adapter.submitData(articles)
}
}
In fragment I have function: viewModel.searchNews(newText)
And in Main activity: viewModel.setSortState(SortOrderState.Ascending) (one menu item clicked) to see if MutableStateFlow.value is changed. I can see that in ViewModel i can change these values but if I do:
val flow=currentQuery.combine(sortState){
query,state ->
}
I never changes if I click on sort menu item, only if I type something to search.
Edit: sortState is not updating in flow variable, I checked setSortState and I can clearly see that state is changed but in flow I only send ascending all the time.
Main activity:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_sortAsc -> {
viewModel.setSortState(SortOrderState.Ascending)
}
R.id.menu_sortDesc -> {
viewModel.setSortState(SortOrderState.Descening)
}
}
return super.onOptionsItemSelected(item)
}
ViewModel:
fun setSortState(sortOrderState: SortOrderState) {
sortState.value = sortOrderState
}
SortOrderState:
sealed interface SortOrderState{
object Ascending : SortOrderState
object Descening : SortOrderState
}
Edit 2: Collecting in HomeFragment it always gives me Ascending value even if i click on menu item for descending sort
lifecycleScope.launch {
viewModel.sortState.collectLatest {
Log.d(TAG, "onCreateViewSort: $it")
}
In ViewModel I can see sortState is changed:
fun setSortState(sortOrderState: SortOrderState) {
sortState.value = sortOrderState
Log.d(TAG, "setSortState: ${sortState.value}")
}
You aren't using your sort state as a Flow. You're only passively using its value, so your output flow won't automatically update when the value changes.
Instead, you need to combine your flows.
Here, I also moved your lowercase transformation before the distinctUntilChanged because I think that makes more logical sense. Also, it makes sense to include the trim in the transformation and not just in the filter.
val flow = currentQuery
.debounce(2300)
.map { it.trim().lowercase(Locale.ROOT) }
.filter { it.isNotEmpty() }
.distinctUntilChanged()
.combine(sortState) { query, sort -> query to sort }
.flatMapLatest { (query, sort) ->
articleRepository.getSearchResult(query, sort)
}
You might also consider tagging this with shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 1) so the search doesn't have to restart on a screen rotation.

viewModelScope blocks UI in Jetpack Compose

viewModelScope blocks UI in Jetpack Compose
I know viewModelScope.launch(Dispatchers.IO) {} can avoid this problem, but how to use viewModelScope.launch(Dispatchers.IO) {}?
This is my UI level code
#Composable
fun CountryContent(viewModel: CountryViewModel) {
SingleRun {
viewModel.getCountryList()
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
// ...
}
Here is my ViewModel, Pager is my pagination
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
This is the small package
#Composable
fun SingleRun(onClick: () -> Unit) {
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
onClick()
execute.value = false
}
}
I don't use Compose much yet, so I could be wrong, but this stood out to me.
I don't think your thread is being blocked. I think you subscribed to an empty flow before replacing it, so there is no data to show.
You shouldn't use a var property for your flow, because the empty original flow could be collected before the new one replaces it. Also, it defeats the purpose of using cachedIn because the flow could be replaced multiple times.
You should eliminate the getCountryList() function and just directly assign the flow. Since it is a cachedIn flow, it doesn't do work until it is first collected anyway. See the documentation:
It won't execute any unnecessary code unless it is being collected.
So your view model should look like:
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
val countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
...and you can remove the SingleRun block from your Composable.
You are not doing anything that would require you to specify dispatchers. The default of Dispatchers.Main is fine here because you are not calling any blocking functions directly anywhere in your code.

Kotlin on Android: How to use LiveData from a database in a fragment?

I use MVVM and have a list of data elements in a database that is mapped through a DAO and repository to ViewModel functions.
Now, my problem is rather banal; I just want to use the data in fragment variables, but I get a type mismatch.
The MVVM introduces a bit of code, and for completeness of context I'll run through it, but I'll strip it to the essentials:
The data elements are of a data class, "Objects":
#Entity(tableName = "objects")
data class Objects(
#ColumnInfo(name = "object_name")
var objectName: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
In ObjectsDao.kt:
#Dao
interface ObjectsDao {
#Query("SELECT * FROM objects")
fun getObjects(): LiveData<List<Objects>>
}
My database:
#Database(
entities = [Objects::class],
version = 1
)
abstract class ObjectsDatabase: RoomDatabase() {
abstract fun getObjectsDao(): ObjectsDao
companion object {
// create database
}
}
In ObjectsRepository.kt:
class ObjectsRepository (private val db: ObjectsDatabase) {
fun getObjects() = db.getObjectsDao().getObjects()
}
In ObjectsViewModel.kt:
class ObjectsViewModel(private val repository: ObjectsRepository): ViewModel() {
fun getObjects() = repository.getObjects()
}
In ObjectsFragment.kt:
class ObjectsFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ObjectsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(ObjectsViewModel::class.java)
// I use the objects in a recyclerview; rvObjectList
rvObjectList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = ObjectsAdapter(listOf(), viewModel)
// And I use an observer to keep the recyclerview updated
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
}
}
The adapter:
class ObjectsAdapter(var objects: List<Objects>,
private val viewModel: ObjectsViewModel):
RecyclerView.Adapter<ObjectsAdapter.ObjectsViewHolder>() {
// Just a recyclerview adapter
}
Now, all the above works fine - but my problem is that I don't want to use the observer to populate the recyclerview; in the database I store some objects, but there are more objects that I don't want to store.
So, I try to do this instead (in the ObjectsFragment):
var otherObjects: List<Objects>
// ...
if (condition) {
adapter.objects = viewModel.getObjects()
} else {
adapter.objects = otherObjects
}
adapter.notifyDataSetChanged()
And, finally, my problem; I get type mismatch for the true condition assignment:
Type mismatch: inferred type is LiveData<List> but List was expected
I am unable to get my head around this. Isn't this pretty much what is happening in the observer? I know about backing properties, such as explained here, but I don't know how to do that when my data is not defined in the ViewModel.
We need something to switch data source. We pass switching data source event to viewModel.
mySwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.switchDataSource(isChecked)
}
In viewModel we handle switching data source
(To use switchMap include implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
class ObjectsViewModel(private val repository: ObjectsRepository) : ViewModel() {
// Best practice is to keep your data in viewModel. And it is useful for us in this case too.
private val otherObjects = listOf<Objects>()
private val _loadDataFromDataBase = MutableLiveData<Boolean>()
// In case your repository returns liveData of favorite list
// from dataBase replace MutableLiveData(otherObjects) with repository.getFavorite()
fun getObjects() = _loadDataFromDataBase.switchMap {
if (it) repository.getObjects() else MutableLiveData(otherObjects)
}
fun switchDataSource(fromDataBase: Boolean) {
_loadDataFromDataBase.value = fromDataBase
}
}
In activity/fragment observe getObjects()
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
You can do something like this:
var displayDataFromDatabase = true // Choose whatever default fits your use-case
var databaseList = emptyList<Objects>() // List we get from database
val otherList = // The other list that you want to show
toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
displayDataFromDatabase = isChecked // Or the negation of this
// Update adapter to use databaseList or otherList depending upon "isChecked"
}
viewModel.getObjects.observe(viewLifecycleOwner) { list ->
databaseList = list
if(displayDataFromDatabase)
// Update adapter to use this databaseList
}

Pattern to avoid if else chain methods call using kotlin sealed class and enums

I've a question about, how would you handle this case?
Imagine that you have to do a validation of an object and that validation should have a sort of importance, in this case we only have 3 validations, each one can result Valid or his own QualityCheck enum value.
This is the method example in kotlin and the validations
sealed class Validation {
abstract fun validate(bobject: ObjectToCheck): QualityCheck
object VeryImportantValidation : Validation() {
override fun validate(bobject: ObjectToCheck): QualityCheck =
if (isValid(bobject.valueX)) QualityCheck.Valid
else QualityCheck.VeryImportantInvalid
}
object SecondMostImportant : Validation() {
override fun validate(bobject: ObjectToCheck): QualityCheck =
if (isValid(bobject.valueNotSoImportant)) QualityCheck.Valid
else QualityCheck.SecondMostImportantInvalid
}
object NotSoImportant : Validation() {
override fun validate(bobject: ObjectToCheck): QualityCheck =
if (isValid(bobject.valueNothingImportant)) QualityCheck.Valid
else QualityCheck.NotSoImportantInvalid
}
}
fun getQualityCheck(object: ObjectToCheck): QualityCheck =
if (VeryImportantValidation.validate(object) === QualityCheck.Valid) {
if (SecondMostImportant.validate(object) === QualityCheck.Valid) {
NotSoImportant(paymentsRepository.getSystemPayments()).validate(object)
} else {
QualityCheck.SecondMostImportantInvalid
}
} else {
QualityCheck.VeryImportantInvalid
}
I think this is not scalable neither easy to read/understand or modify if we would want to add a new one.
There is any kind to do this elegant and easier to include more validations?
If you invert your Boolean conditions, you can eliminate the nesting. Then you can change it to a when statement for simplicity:
fun getQualityCheck(object: ObjectToCheck): QualityCheck = when {
VeryImportantValidation.validate(object) !== QualityCheck.Valid ->
QualityCheck.VeryImportantInvalid
SecondMostImportant.validate(object) !== QualityCheck.Valid ->
QualityCheck.SecondMostImportantInvalid
else ->
NotSoImportant(paymentsRepository.getSystemPayments()).validate(object)
}
Validation like this is a perfect candidate for the "Rules engine pattern"... mostly known as a for loop.
You just set up a List<Validation> with all of the validations you want to run and iterate over them calling the validate method. You have 2 options, collect all errors (doing a fold on the list), or stop the loop after the first error with a asSequence().map().takeWhile().
I forgot to say, you don't need to seal the Validation class. What is your intent with that?
Scalability/Extensibility would depend from situation to situation and a code cannot be open to all types of changes. One rule of thumb is to keep it as simple as possible and when a requirement is changed we ensure that the code is open to such kind of changes.
Also, I agree with #Augusto. Your use of the sealed class is not really how it is intended to be used.
Anyways let's look at how it would be easier to add a new validation, change the severity of the violation, or have several validations with the same severity.
Lets define an interface for Validations.
interface Validation {
fun validate(value: Int): Boolean
}
Now let's define a few Validations
class LimitValidation: Validation{
override fun validate(value: Int) = value < 100
}
class PositiveValidation: Validation {
override fun validate(value: Int) = value > 0
}
class EvenValidation: Validation {
override fun validate(value: Int) = value % 2 == 0
}
Let's say you have the following Violations
enum class Violation {
SEVERE,
MODERATE,
TYPICAL
}
We can make use of sealed class to define the quality.
sealed class Quality {
object High : Quality()
data class Low(val violation: Violation) : Quality()
}
We can create a class responsible for checking the Quality.
class QualityEvaluator {
private val violationMap: MutableMap<KClass<*>, Violation> = mutableMapOf()
init {
violationMap[LimitValidation::class] = Violation.SEVERE
violationMap[PositiveValidation::class] = Violation.MODERATE
violationMap[EvenValidation::class] = Violation.TYPICAL
}
fun evaluateQuality(value: Int, validations: List<Validation>) : Quality {
val sortedValidations = validations.sortedBy(::violationFor)
sortedValidations.forEach {
if(!it.validate(value)) {
return Quality.Low(violationFor(it))
}
}
return Quality.High
}
private fun <T: Validation> violationFor(validation: T): Violation {
return if (violationMap.containsKey(validation::class)) {
requireNotNull(violationMap[validation::class])
} else {
Violation.TYPICAL
}
}
}
Finally, we can use all this like so:
val validations = listOf(LimitValidation(), PositiveValidation(), EvenValidation())
when(val quality = QualityEvaluator().evaluateQuality(8, validations)) {
is Quality.High -> println("Quality is High")
is Quality.Low -> println("Quality is Low. Violation: ${quality.violation}")
}

MutableStateFlow is not emitting values after 1st emit kotlin coroutine

This is my FirebaseOTPVerificationOperation class, where my MutableStateFlow properties are defined, and values are changed,
#ExperimentalCoroutinesApi
class FirebaseOTPVerificationOperation #Inject constructor(
private val activity: Activity,
val logger: Logger
) {
private val _phoneAuthComplete = MutableStateFlow<PhoneAuthCredential?>(null)
val phoneAuthComplete: StateFlow<PhoneAuthCredential?>
get() = _phoneAuthComplete
private val _phoneVerificationFailed = MutableStateFlow<String>("")
val phoneVerificationFailed: StateFlow<String>
get() = _phoneVerificationFailed
private val _phoneCodeSent = MutableStateFlow<Boolean?>(null)
val phoneCodeSent: StateFlow<Boolean?>
get() = _phoneCodeSent
private val _phoneVerificationSuccess = MutableStateFlow<Boolean?>(null)
val phoneVerificationSuccess: StateFlow<Boolean?>
get() = _phoneVerificationSuccess
fun resendPhoneVerificationCode(phoneNumber: String) {
_phoneVerificationFailed.value = "ERROR_RESEND"
}
}
This is my viewmodal, from where i am listening the changes in stateflow properties, as follows,
class OTPVerificationViewModal #AssistedInject constructor(
private val coroutinesDispatcherProvider: AppCoroutineDispatchers,
private val firebasePhoneVerificationListener: FirebaseOTPVerificationOperation,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
#AssistedInject.Factory
interface Factory {
fun create(savedStateHandle: SavedStateHandle): OTPVerificationViewModal
}
val phoneAuthComplete = viewModelScope.launch {
firebasePhoneVerificationListener.phoneAuthComplete.filter {
Log.e("1","filter auth $it")
it.isNotNull()
}.collect {
Log.e("2","complete auth $it")
}
}
val phoneVerificationFailed = viewModelScope.launch {
firebasePhoneVerificationListener.phoneVerificationFailed.filter {
Log.e("3","filter failed $it")
it.isNotEmpty()
}.collect {
Log.e("4","collect failed $it")
}
}
val phoneCodeSent = viewModelScope.launch {
firebasePhoneVerificationListener.phoneCodeSent.filter {
Log.e("5","filter code $it")
it.isNotNull()
}.collect {
Log.e("6","collect code $it")
}
}
val phoneVerificationSuccess = viewModelScope.launch {
firebasePhoneVerificationListener.phoneVerificationSuccess.filter {
Log.e("7","filter success $it")
it.isNotNull()
}.collect {
Log.e("8","collect success $it")
}
}
init {
resendVerificationCode()
secondCall()
}
private fun secondCall() {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
delay(10000)
resendVerificationCode()
}
}
fun resendVerificationCode() {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
firebasePhoneVerificationListener.resendPhoneVerificationCode(
getNumber()
)
}
}
private fun getNumber() =
"+9191111116055"
}
The issue is that
firebasePhoneVerificationListener.phoneVerificationFailed
is fired in viewmodal for first call of,
init {
resendVerificationCode()
}
but for second call of:
init {
secondCall()
}
firebasePhoneVerificationListener.phoneVerificationFailed is not fired in viewmodal, I don't know why it happened, any reason or explanation will be very appericated.
Current Output:
filter auth null
filter failed
filter code null
filter success null
filter failed ERROR_RESEND
collect failed ERROR_RESEND
Expected Output:
filter auth null
filter failed
filter code null
filter success null
filter failed ERROR_RESEND
collect failed ERROR_RESEND
filter failed ERROR_RESEND
collect failed ERROR_RESEND
Pankaj's answer is correct, StateFlow won't emit the same value twice. As the documentation suggests:
Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one.
Therefore, to resolve this issue you can create a wrapping class and override the equals (and hashCode) method to return false even if the classes are in fact the same:
sealed class VerificationError {
object Resend: VerificationError()
override fun equals(other: Any?): Boolean {
return false
}
override fun hashCode(): Int {
return Random.nextInt()
}
}
StateFlow is SharedFlow:
https://github.com/Kotlin/kotlinx.coroutines/issues/2034
Described in more detail in my article: https://veldan1202.medium.com/kotlin-setup-sharedflow-31debf613b91
val shared = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
shared.tryEmit(value)
The value emitted by state flow is conflated and doesn't emit the same consecutive result twice, you can think as if a condition check is validating the old emitted value is not equal to the newly emitted value.
Current Output:
filter auth null
filter failed
filter code null
filter success null
filter failed ERROR_RESEND
collect failed ERROR_RESEND
(filter failed ERROR_RESEND
collect failed ERROR_RESEND) This being the same old value which was emitted so you will not see them getting emitted.
Use a Channel: this does emit after sending the same value twice.
Add this to your ViewModel
val _intent = Channel<Intent>(Channel.CONFLATED)
Put values using send / trySend
_intent.send(intentLocal)
observe as flow
_intent.consumeAsFlow().collect { //do something }
I think I have some more in-depth understanding of this issue. The first thing to be sure is that for StateFlow, it is not recommended to use variable collection types (such as MutableList, etc.). Because MutableList is not thread safe. If there are multiple references in the core code, it may cause the program to crash.
Before, the method I used was to wrap the class and override the equals method. However, I think this solution is not the safest method. The safest way is for deep copy, Kotlin provides toMutableList() and toList() methods are both deep copy. The emit method judges whether there is a change depends on whether the result of equals() is equal.
The reason I have this problem is that the data type using emit() is: SparseArray<MutableList>. StateFlow calls the equals method for SparseArray. When MutableList changes, the result of equals does not change at this time (even if the equals and hashcode methods of MutableList change).
Finally, I changed the type to SparseArray<List>. Although the performance loss caused by adding and deleting data, this also solves the problem fundamentally.
As mentioned above, LiveData emits data every time, while StateFlow emits only different values. tryEmit() doesn't work. In my case I found two solutions.
If you have String data, you can emit again this way:
private fun emitNewValue() {
subscriber.value += " "
subscriber.value.dropLast(1)
}
For another class you can use this (or create an extension function):
private fun <T> emitNewValue(value: T) {
if (subscriber.value == value) {
subscriber.value = null
}
subscriber.value = value
}
But it's a bad and buggy way (values are emitted twice additionally).
Try to find all subscribers that change their values. It can be not evident. For instance, focus change listener, Switch (checkbox). When you toggle Switch, a text can also change, so you should subscribe to this listener. The same way when you focus other view, an error text can change.
Use wrapper object with any unique id, for example:
class ViewModel {
private val _listFlow = MutableStateFlow(ListData(emptyList()))
val listFlow: StateFlow<ListData> get() = _listFlow
fun update(list:List<String>){
_listFlow.value = ListData(list)
}
data class ListData constructor(
val list: List<String>,
private val id: UUID = UUID.randomUUID(),//added unique id
)
}
I had a similar problem after merging the streams.
The emit() function will not be executed if == is used to determine equality.
The way to solve the problem: You can wrap a layer and rewrite the hashCode() and equals() methods. The equals() method directly returns false.
This solution works in my code. The stream after the combine has also changed.
Pankaj's answer is correct, StateFlow will not emit the same value twice.
Before wrapping, the result of == is still true even if the content is different.
You could make _phoneVerificationFailed nullable and send null between the two calls!