Synchronizing value change in Coroutines - android

I have a ViewModel in android and I am trying to validate whether the person has entered his name and age before moving on to the next page.
Here is my code:
fun onContinueClick() {
val navigateNextPage: (Int) -> Unit = lambda#{ validation ->
if (validation < 2)
return#lambda
nextPageUseCase().onEach {
_navigationNotify.value = it // Moving on to the next page
}
}
viewModelScope.launch {
var valid = 0
getName().collect { name ->
if (name != null) navigateNextPage(++valid)
}
getAge().collect { age ->
if (age != null) navigateNextPage(++valid)
}
}
}
Although this is working as expected, is it efficient to perform this operation? I think that if both the ++valid happen at the same time, I wouldn't be able to go to the next page.
I want to know how to synchronize the code to avoid that situation.

Related

How to use RecyclerView or other ui elements that depend on multiple livedata and may change on interaction android kotlin

I have a rather difficult task for me here. I have a RecyclerView that depends on 3-4 livedata objects. For this I use MediatorLiveData:
class TripleMediatorLiveData<F, S, T>(
firstLiveData: LiveData<F>,
secondLiveData: LiveData<S>,
thirdLiveData: LiveData<T>
) : MediatorLiveData<Triple<F?, S?, T?>>() {
init {
addSource(firstLiveData) {
firstLiveDataValue: F -> value =
Triple(firstLiveDataValue, secondLiveData.value, thirdLiveData.value)
}
addSource(secondLiveData) {
secondLiveDataValue: S -> value =
Triple(firstLiveData.value, secondLiveDataValue, thirdLiveData.value)
}
addSource(thirdLiveData) {
thirdLiveDataValue: T -> value =
Triple(firstLiveData.value, secondLiveData.value, thirdLiveDataValue)
}
}
}
I get data from ROOM Database. As I said earlier, I have a RecyclerView, the logic of which looks like this:
viewModel.tripleMediatorLiveData.observe(viewLifecycleOwner) { triple ->
val settings = triple.first
val availability = triple.second
val chapters = triple.third
when(availability) {
0 -> {
createCourseNotAvailableItems()
binding.btnBegin.setOnClickListener {
viewModel.setTimersForChapters(chapters!!)
viewModel.updateCourseAvailability(1)
val courseEndTime = TimeManager().currentDatePlusMinutes(55)
viewModel.setCourseEndTime(courseEndTime)
}
}
1 -> {
createCourseAvailableItems(chapters!!)
binding.btnBegin.setOnClickListener {
viewModel.resetTimersForChapters(chapters)
viewModel.updateCourseAvailability(0)
viewModel.setCourseEndTime()
}
viewModel.courseCompleted.observe(viewLifecycleOwner) { endDate ->
val currentDate = TimeManager().getCurrentTimeInSeconds()
endDate?.let {
if (currentDate > it && it != 0L) {
CourseCompletedDialog(
onClick = {
viewModel.resetTimersForChapters(chapters)
viewModel.updateCourseAvailability(0)
viewModel.setCourseEndTime()
}
).show(childFragmentManager, CourseCompletedDialog.TAG)
}
}
}
}
}
}
As you can see, the RecyclerView handles a large number of LiveData objects ​​and is displayed differently depending on the state. The problem arises in the appearance of the dialog.
When OnClick triggered, the data is updated and the Observer is triggered, which is why the Observer does not work quite predictably, because of which the dialog may appear several times in a row, BUT IT SHOULD NOT APPEAR AT ALL AFTER THE ONCLICK TRIGGERED.
How can I improve the code so that the dialog does not appear multiple times in a row?

Add multiple source to MediatorLiveData and change its value

Basically I have a screen, and there are a few EditTexts and a Button.
Users have to fill in all fields otherwise the Button is disabled.
I am using DataBinding to achieve this. Below is my code in the viewmodel.
val isNextEnabled = MediatorLiveData<Boolean>()
isNextEnabled.apply {
addSource(field1LiveData) {
isNextEnabled.value =
it != null
&& field2LiveData.value != null
&& field3LiveData.value != null
}
addSource(field2LiveData) {
isNextEnabled.value =
it != null
&& field1LiveData.value != null
&& field3LiveData.value != null
}
addSource(field3LiveData) {
isNextEnabled.value =
it != null
&& field2LiveData.value != null
&& field1LiveData.value != null
}
}
In the xml
<Button
android:enabled="#{viewmodel.isNextEnabled}"
.
.
.
</Button>
Everything works fine as expected. But the logic above looks cumbersome. What if I have more EditText ? The code would be painful to write/maintain.
Is there any way I can simplify it?
Ultimately you have a UseCase/Logic where you decide when the next button is enabled.
I think you should separate the logic into useCases where it makes sense.
E.g.
// update these when they change in the UI for e.g.
val field1Flow: Flow<Boolean> = flow { ... }
val field2Flow: Flow<Boolean> = flow { ... }
val nextButtonState = combine(field1Flow, field2Flow) { f1, f2 ->
f1 && f2
}.collect { state ->
// use your state.
}
Now... if you need special logic and not just two-boolean algebra here, you can always extract it into use-cases that return more flows.
Or map it or various operations you could do:
E.g.
class YourUseCase() {
operator fun invoke(field1: Boolean, field2: Boolean) {
// Your Logic
return field1 && field2
}
}
// And now...
val _nextButtonState = combine(field1Flow, field2Flow) { f1, f2 ->
YourUseCase(f1, f2)
}
val _uiState = _nextButtonState.transformLatest {
emit(it) // you could add a when(it) { } and do more stuff here
}
// And if you don't want to change your UI to use flows, you can expose this as live data
val uiState = _uiState.asLiveData()
Keep in mind this is Pseudo-code written on SO.. not even Notepad ;)
I hope that makes a bit of sense. The idea is to separate the bits into use-cases (that you can ultimately test in isolation) and to have a flow of data. When buttons change state, the fieldNFlow emits the values and this triggers the whole chain for you.
If you have the latest Coroutines (2.4.0+) you can use the new operators to avoid using LiveData, but overall, I'd try to think in that direction.
Lastly, your liveData code with a mediator is not bad, I'd at the very least, extract the "logic" into 3 different useCases so it's not all together in a series of if/else statements.
A word of caution: I haven't used Databinding in over 3(?) years, I'm personally not a fan of it so I cannot tell you if it would cause a problem with this approach.

Wrong If check statement

I wanted to make a method that determine if the application is started for the very first time, no matter the current version of the application. People suggest that we should use SharedPreferences as seen from this qustion. Below is the function that determine if application is started for the very first time.
companion object {
const val APP_LAUNCH_FIRST_TIME: Int = 0 // first start ever
const val APP_LAUNCH_FIRST_TIME_VERSION: Int = 1 // first start in this version (when app is updated)
const val APP_LAUNCH_NORMAL: Int = 2 // normal app start
/**
* Method that checks if the application is started for the very first time, or for the first time
* of the updated version, or just normal start.
*/
fun checkForFirstAppStart(context: Context): Int {
val sharedPreferencesVersionTag = "last_app_version"
val sharedPreferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(context)
var appStart = APP_LAUNCH_NORMAL
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val lastVersionCode = sharedPreferences.getLong(sharedPreferencesVersionTag, -1L)
val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
appStart = when {
lastVersionCode == -1L -> APP_LAUNCH_FIRST_TIME
lastVersionCode < currentVersionCode -> APP_LAUNCH_FIRST_TIME_VERSION
lastVersionCode > currentVersionCode -> APP_LAUNCH_NORMAL
else -> APP_LAUNCH_NORMAL
}
// Update version in preferences
sharedPreferences.edit().putLong(sharedPreferencesVersionTag, currentVersionCode).commit()
} catch (e: PackageManager.NameNotFoundException) {
// Unable to determine current app version from package manager. Defensively assuming normal app start
}
return appStart
}
}
Now in my MainActivity I make the check in this way, but strangely enough I always end up inside the if statement, although appLaunch is different from MainActivityHelper.APP_LAUNCH_FIRST_TIME
val appLaunch = MainActivityHelper.checkForFirstAppStart(this)
if (appLaunch == MainActivityHelper.APP_LAUNCH_FIRST_TIME) {
val c = 299_792_458L
}
Here we see that appLaunch is 2
Here we see that MainActivityHelper.APP_LAUNCH_FIRST_TIME is 0
I am in the main thread I check using Thread.currentThread(), and when I add watches in the debugger (appLaunch == MainActivityHelper.APP_LAUNCH_FIRST_TIME) I get false.
So I suggest that there is some delay, and by the time the if check is made the result is changed?
There's nothing wrong with the code. I tested it and it works as intended. I get all three return values depending on the circumstances. I simplified the code a bit but the original code should nevertheless works.
enum class AppLaunch {
LAUNCH_FIRST_TIME, // first start ever
FIRST_TIME_VERSION, // first start in this version (when app is updated)
NORMAL // normal app start
}
/**
* Method that checks if the application is started for the very first time, or for the first time
* of the updated version, or just normal start.
*/
fun checkForFirstAppStart(context: Context): AppLaunch {
val sharedPreferencesVersionTag = "last_app_version"
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val lastVersionCode = sharedPreferences.getLong(sharedPreferencesVersionTag, -1L)
val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
// Update version in preferences
sharedPreferences.edit().putLong(sharedPreferencesVersionTag, currentVersionCode).commit()
when (lastVersionCode) {
-1L -> AppLaunch.LAUNCH_FIRST_TIME
in 0L until currentVersionCode -> AppLaunch.FIRST_TIME_VERSION
else -> AppLaunch.NORMAL
}
} catch (e: PackageManager.NameNotFoundException) {
// Unable to determine current app version from package manager. Defensively assuming normal app start
AppLaunch.NORMAL
}
}
I experimented a bit and the issue you see looks like a bug in Android Studio. If the code in the if statement is a NOP (no operation) then the debugger seems to stop there. If the code does have a side effect, the debugger doesn't stop.
Things like this can be infuriating but with Android, Android Studio and the tooling, bugs like this are pretty common (unfortunately).
if (appLaunch == APP_LAUNCH_FIRST_TIME) {
val c = 299_792_458L
}
translates to the following byte code:
L3 (the if statement)
LINENUMBER 32 L3
ILOAD 4
IFNE L4
L5
LINENUMBER 33 L5
LDC 299792458
LSTORE 2
Converting c to a var
var c = 1L
if (appLaunch == APP_LAUNCH_FIRST_TIME) {
c = 299_792_458L
}
results in identical byte code so it's certainly not a code problem but an issue with Android Studio.
Update
If you need fast writes with enums you can use something like this:
fun appLaunchById(id: Int, def: AppLaunch = AppLaunch.NORMAL) = AppLaunch.values().find { it.id == id } ?: def
enum class AppLaunch(val id: Int) {
LAUNCH_FIRST_TIME(0), // first start ever
FIRST_TIME_VERSION(1), // first start in this version (when app is updated)
NORMAL(2); // normal app start
}
^^^ writes an Int so fast and short. Reading is certainly not super fast though.
Update 2
Generic version of the enum solution:
inline fun <reified T : Enum<*>> enumById(hash: Int, def: T) = enumValues<T>()
.find { it.hashCode() == hash }
?: def
enum class AppLaunch {
LAUNCH_FIRST_TIME, // first start ever
FIRST_TIME_VERSION, // first start in this version (when app is updated)
NORMAL // normal app start
}
Usage:
val enum = enumById(value.hashCode(), AppLaunch.NORMAL)

Handle multiple errors when the user fill inputs in Kotlin

I'm pretty new with Kotlin (and also I've never coded in Java), and I want to know if there is another way to to this, or if its okay.
To put you in context, I'm working with the Model-View-Presenter. The presenter here, receives the username and login from the login screen, and in order to handle errors (such as empty fields, invalid email), I created an Enum (with possible errors), and then I fill the list of these errors by checking conditions.
Presenter:
fun onLoginButtonClicked(email: String, password: String) {
val errorEnum = mutableListOf<ErrorEnum>()
if (email.isEmpty()) errorEnum.add(ErrorEnum.EMPTY_EMAIL)
if (password.isEmpty()) errorEnum.add(ErrorEnum.EMPTY_PASSWORD)
if (email.isNotEmpty() && !Patterns.EMAIL_ADDRESS.matcher(email).matches()) errorEnum.add(ErrorEnum.INVALID_EMAIL)
if (errorEnum.isEmpty()) {
userSession.email = email
userSession.password = password
view?.goToViewPager(email)
} else {
view?.checkErrors(errorEnum)
}
}
After that, the fragment reflects on the view the errors, iterating through the list.
Fragment:
override fun checkErrors(Errors: MutableList<ErrorEnum>) {
Errors.forEach {
when (it) {
ErrorEnum.EMPTY_PASSWORD -> binding.password.error = getString(R.string.login_alert_input)
ErrorEnum.INVALID_EMAIL -> binding.email.error = getString(R.string.login_alert_bad_email)
ErrorEnum.EMPTY_EMAIL -> binding.email.error = getString(R.string.login_alert_input)
}
}
}
Clearly, you are doing redundant work here, first your are building up List and then passing the List to View and again View is doing some work to iterate and compare, instead you can have separate methods in view to handle each without comparison and call them directly
override fun showEmailInvalidError() {
binding.password.error = getString(R.string.login_alert_input)
}
override fun showEmptyEmailError() {
binding.email.error = getString(R.string.login_alert_input)
}
override fun showEmptyPasswordError() {
binding.email.error = getString(R.string.login_alert_bad_email)
}
In presenter
fun onLoginButtonClicked(email: String, password: String) {
if(email.isEmpty()) view?.showEmptyEmailError()
if(password.isEmpty()) view?.showEmptyPasswordError()
if(email.isNotEmpty() && !Patterns.EMAIL_ADDRESS.matcher(email).matches())
view?.showEmailInvalidError()
if(errorEnum.isEmpty()) {
userSession.email = email
userSession.password = password
view?.goToViewPager(email)
}else {
view?.checkErrors(errorEnum)
}
}

How to show empty view while using Android Paging 3 library

I am using Paging 3 lib. and i am able to check if refresh state is "Loading" or "Error" but i am not sure how to check "empty" state.I am able to add following condition but i am not sure if its proper condition
adapter.loadStateFlow.collectLatest { loadStates ->
viewBinding.sflLoadingView.setVisibility(loadStates.refresh is LoadState.Loading)
viewBinding.llErrorView.setVisibility(loadStates.refresh is LoadState.Error)
viewBinding.button.setOnClickListener { pagingAdapter.refresh() }
if(loadStates.refresh is LoadState.NotLoading && (viewBinding.recyclerView.adapter as ConcatAdapter).itemCount == 0){
viewBinding.llEmptyView.setVisibility(true)
}else{
viewBinding.llEmptyView.setVisibility(false)
}
}
Also I am running into other problem
I have implemented search functionality and until more than 2 characters are entered i am using same paging source like following but the above loadstate callback is executed only once.So thats why i am not able to hide empty view if search query is cleared.I am doing so to save api call from front end.
private val originalList : LiveData<PagingData<ModelResponse>> = Transformations.switchMap(liveData){
repository.fetchSearchResults("").cachedIn(viewModelScope)
}
val list : LiveData<LiveData<PagingData<ModelResponse>>> = Transformations.switchMap{ query ->
if(query != null) {
if (query.length >= 2)
repository.fetchSearchResults(query)
else
originalList
}else
liveData { emptyList<ModelResponse>() }
}
Here is the proper way of handling the empty view in Android Paging 3:
adapter.addLoadStateListener { loadState ->
if (loadState.source.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && adapter.itemCount < 1) {
recycleView?.isVisible = false
emptyView?.isVisible = true
} else {
recycleView?.isVisible = true
emptyView?.isVisible = false
}
}
adapter.loadStateFlow.collect {
if (it.append is LoadState.NotLoading && it.append.endOfPaginationReached) {
emptyState.isVisible = adapter.itemCount < 1
}
}
The logic is,If the append has finished (it.append is LoadState.NotLoading && it.append.endOfPaginationReached == true), and our adapter items count is zero (adapter.itemCount < 1), means there is nothing to show, so we show the empty state.
PS: for initial loading you can find out more at this answer: https://stackoverflow.com/a/67661269/2472350
I am using this way and it works.
EDITED:
dataRefreshFlow is deprecated in Version 3.0.0-alpha10, we should use loadStateFlow now.
viewLifecycleOwner.lifecycleScope.launch {
transactionAdapter.loadStateFlow
.collectLatest {
if(it.refresh is LoadState.NotLoading){
binding.textNoTransaction.isVisible = transactionAdapter.itemCount<1
}
}
}
For detailed explanation and usage of loadStateFlow, please check
https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data#load-state-listener
If you are using paging 3 with Jetpack compose, I had a similar situation where I wanted to show a "no results" screen with the Pager 3 library. I wasn't sure what the best approach was in Compose but I use now this extension function on LazyPagingItems. It checks if there are no items and makes sure there are no items coming by checking if endOfPaginationReached is true.
private val <T : Any> LazyPagingItems<T>.noItems
get() = loadState.append.endOfPaginationReached && itemCount == 0
Example usage is as follows:
#Composable
private fun ArticlesOverviewScreen(screenContent: Content) {
val lazyBoughtArticles = screenContent.articles.collectAsLazyPagingItems()
when {
lazyBoughtArticles.noItems -> NoArticlesScreen()
else -> ArticlesScreen(lazyBoughtArticles)
}
}
Clean and simple :).

Categories

Resources