rickclephas/KMP-ObservableViewModel

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

sangampokharel opened this issue · 4 comments

Hi. Could you possibly share the relevant code or a reproduction sample?

yeah sure,
The problem i am facing right now is that state is not being observed contiously in IOS its fluctuating sometimes it works as expected and sometimes it doesnot.Example code of login/logout senario when login is pressed sometimes i dont see loading, or sometimes the loginstate is not updating something like that. Please help !

I guess its because of that warning ** Publishsing the UI changes from background thread **

Android:

BaseViewModel

open class BaseKMMViewModel : KMMViewModel() {
private val _snackBarVisibleState = MutableStateFlow(viewModelScope, false)
val snackBarVisibleState = _snackBarVisibleState.asStateFlow()

private val _showLoading = MutableStateFlow(viewModelScope, false)
val showLoading = _showLoading.asStateFlow()

private val _showLoadingDialog = MutableStateFlow(viewModelScope, false)
val showLoadingDialog = _showLoadingDialog.asStateFlow()

private val _showMessage = MutableStateFlow(viewModelScope, "")
val showMessage = _showMessage.asStateFlow()

private val _showMessageDialog = MutableStateFlow(viewModelScope, "")
val showMessageDialog = _showMessageDialog.asStateFlow()

private val _logout = MutableStateFlow(viewModelScope, false)
val logout = _logout.asStateFlow()

fun hideSnackBar() {
    _snackBarVisibleState.value = false
}

fun showSnackBar() {
    _snackBarVisibleState.value = true
}

fun showMessage(msg: String) {
    showSnackBar()
    _showMessage.value = msg
}

private fun showMessageDialog(msg: String?) {
    _showMessageDialog.value = msg ?: ApiConstants.defaultErrorMsg
}

fun hideMessageDialog() {
    _showMessageDialog.value = ""
}

fun showLoading() {
    _showLoading.value = true
}

fun hideLoading() {
    _showLoading.value = false
}

fun showLoadingDialog() {
    _showLoadingDialog.value = true
}

fun hideLoadingDialog() {
    _showLoadingDialog.value = false
}

fun showError(error: Exception?) {
    when (error) {

        is UnAuthorizedError -> {
            _logout.value=true
        }

        is IOException -> {
            showMessageDialog("No internet")
        }

        else ->
            showMessageDialog(error?.message ?: ApiConstants.defaultErrorMsg)
    }

}

suspend fun getToken(): String? {
    return if (isTokenValid())
        loginInfo?.accessToken
    else {
       getNewToken()
    }
}

private suspend fun getNewToken(): String? {
    return coroutineScope {
        when (val result =
            LoginRepository.doLogin(
                Login(
                    grantType = ApiConstants.refreshToken,
                    type= ApiConstants.Driver,
                    refreshToken = loginInfo?.refreshToken
                )
            )) {
            is Response.Success<*> -> {
                val loginResponse = result.data as? LoginResponse
                loginResponse?.loginTime = Clock.System.now().toEpochMilliseconds()
                loginInfo = loginResponse
                loginInfo?.accessToken
            }

            is Response.Error<*> -> {
                hideLoading()
                _logout.value=true
                null
            }
        }
    }
}

private fun isTokenValid() =
    (((loginInfo?.expiresIn?.times(1000))?.plus(loginInfo?.loginTime?: 0L))
        ?: 0L) > Clock.System.now()
        .toEpochMilliseconds()

}

=================

LoginViewModel

class LoginViewModel : BaseKMMViewModel() {
private val _loginState = MutableStateFlow(viewModelScope, false)
var loginState = _loginState.asStateFlow()
private fun validateLoginData(login: Login?): Boolean {
if (login?.account.isNullOrEmpty() && login?.password.isNullOrEmpty()) {
showSnackBar()
showMessage("Phone Number and Email shouldn't be empty.")
hideLoading()
return false
}

    if (login?.account.isNullOrEmpty()) {
        showSnackBar()
        showMessage("Phone Number shouldn't be empty.")
        hideLoading()
        return false
    }

    if (login?.password.isNullOrEmpty()) {
        showSnackBar()
        showMessage("Password shouldn't be empty.")
        hideLoading()
        return false
    }

    if ((login?.account?.length ?: 0) < 10) {
        showSnackBar()
        showMessage("Phone Number shouldn't be less than 10 digit.")
        hideLoading()
        return false
    }

    if (login?.account?.startsWith("9") == false) {
        showSnackBar()
        showMessage("The Phone Number format is invalid.")
        hideLoading()
        return false
    }

    return true
}

fun doLogin(login: Login?) {
    if (validateLoginData(login)) {
        viewModelScope.coroutineScope.launch(Dispatchers.IO) {
            showLoading()
            when (val result = LoginRepository.doLogin(login)) {
                is Response.Success<*> -> {
                    hideLoading()
                    val loginResponse=result.data as? LoginResponse
                    loginResponse?.loginTime=Clock.System.now().toEpochMilliseconds()
                    KeyValueStorageImp.loginRequest = login
                    KeyValueStorageImp.loginInfo = loginResponse
                    _loginState.value = true
                }

                is Response.Error<*> -> {
                    hideLoading()
                    showError(result.error as Exception?)
                }
            }
        }
    }
}

}

========== Swift UI = =========
I am observing like this

import SwiftUI
import shared
import KMMViewModelSwiftUI
import SwiftUISnackbar
import FirebaseDatabaseInternal
struct LoginView: View {
@EnvironmentObject var appStateVM:AppStateViewModel
@StateViewModel var viewModel = LoginViewModel()
@StateViewModel var profileVM = ProfileViewModel()

private var isLoggedIn: Binding<Bool> {
    Binding { viewModel.loginState.value as! Bool } set: { _ in        }
}

private var isLoading:Binding<Bool> {
    Binding { viewModel.showLoading.value as! Bool } set: { _ in }
}

private var isProfileLoading:Binding<Bool>{
    Binding {
        profileVM.showLoading.value as! Bool
    } set: { _ in }
}

private var profileStateData:Binding<Profile>{
    Binding {
        profileVM.profileDetailState.value as? Profile ?? Profile(id: nil, firstName: nil, lastName: nil, email: nil, username: nil, phoneNumber: nil, imageName: nil, totalDelivery: nil, licenseNumber: nil, address: nil)
    } set: { _ in }
}


// only with error
private var showMessage:Binding<String> {
    Binding { viewModel.showMessage.value as! String } set: { _ in }
}

private var showMessageDialog:Binding<String> {
    Binding { viewModel.showMessageDialog.value as! String } set: { _ in  }
}

private var snackBarVisible :Binding<Bool> {
    Binding { viewModel.snackBarVisibleState.value as! Bool } set: { _ in  }
}

private func handleValidation() {
    viewModel.doLogin(login: Login(account: phone, password: password, grantType: "password", type: "driver", refreshToken: nil))
}

var body: some View {
   ZStack{
   // other view here
   CustomButton(title: "login", handleAction: {
                    handleValidation()
                })
                .padding([.horizontal],24)
   }

.onChange(of: showMessageDialog.wrappedValue) { newValue in
print("Error (newValue)")
if !newValue.isEmpty {
isAlertShown = true

        }
    }
    .onChange(of:profileStateData.wrappedValue){ newValue in
        print("is logged In \(newValue)")
        // store it to fireabse
        let userId = newValue.id ?? 0
        saveDataToFirebase(userId: Int(truncating: userId))
    }
    .onChange(of:viewModel.loginState.value as! Bool){ newValue in
        print("is logged In \(newValue)")
        if newValue {
            profileVM.getProfile()
        }
        
        
    }
}

Thanks that helps a lot!
In the doLogin function you are launching a job on the IO dispatcher:

viewModelScope.coroutineScope.launch(Dispatchers.IO)

All the state updates inside that job won't be performed on the main thread.

Please try and move the dispatcher to the LoginRepository instead.

Thank you so much...It fixed the issue !