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 !