Hey, got a question regarding KMP ObservableViewMo...
# multiplatform
r
Hey, got a question regarding KMP ObservableViewModel/NativeCoroutines. More in 🧵
I’m currenty using the
ViewModel
from the
ObservableViewModel
. I have a function that sets a value into
DataStore
lib from KMM, let’s call it
foo
function
Copy code
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
import com.rickclephas.kmp.observableviewmodel.ViewModel
import com.rickclephas.kmp.observableviewmodel.launch
import com.rickclephas.kmp.observableviewmodel.stateIn
...

open class MyViewModel(useCase: UseCase): ViewModel() {

  private val isProcessing: MutableStateFlow<Boolean> = MutableStateFlow(false)

  @NativeCoroutinesState
  val value: StateFlow<FooState<String>> = combine(isProcessing, useCase.valueFlow) { isProcessing, value ->
     if(isProcessing) {
       return@combine FooState.Processing
     }
     if(value != null) FooState.Success else FooState.Empty
  }.stateIn(viewModelScope, started = SharingStarted.Lazily, initialValue = FooState.Loading)
  
  fun foo(value: String) {
     viewModelScope.launch {
        isProcessing.emit(true)
        useCase.setValue(value)
        isProcessing.emit(false)
     }
  }
}
When calling the
viewModel.foo(value)
from iOS. It should show a progress view due to
FooState.Processing
but it’s not shown due to the non-responsive UI. This works if I delay the execution of the function
useCase.setValue(value)
. Do you know if I’m doing a beginner mistake here with the iOS thread? I’ve tried setting
<http://Dispatchers.IO|Dispatchers.IO>
to the
viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) { .. }
but not working
Asking because maybe someone faced this issue before?
r
It would be good to know how your
UseCase
class looks. Most likely the
setValue
call is too fast.
šŸ‘ 1
r
This is how it looks like:
Copy code
class UseCaseImpl constructor(private val preference: LocalePreference): UseCase {

    override val locale: Flow<LocaleItem?> = preference.getStringDataFlow(PREFERENCE_APP_LOCALE, null).map {
        it?.toLocaleItem()
    }

    override suspend fun setValue(value: String) {
        preference.setStringData(PREFERENCE_APP_LOCALE, value)
    }
}
Copy code
class LocalePreference(DataStore<Preference>) {
 fun getStringDataFlow(preferenceKey: String, ifNotAvailable: String?): Flow<String?> = developmentDataStore.data.map { preferences ->
        preferences[stringPreferencesKey(preferenceKey)] ?: ifNotAvailable
    }
}
Most likely the
setValue
call is too fast.
So it’s nothing related to the thread? Or what would be the root issue when
setValue
is called too fast?
If I didn’t add any
isProcessing
state, it’d still take a moment for the UI to respond. I’ve set processing state due to how long it took to respond the UI and then realized the UI is frozen.
So before I just had this
Copy code
fun foo(value: String) {
     viewModelScope.launch {
        useCase.setValue(value)
     }
  }
Also the Swift UI observing this value looks like this
Copy code
.onChange(of: viewModel.appLanguage) {
                let result: PreferenceState<LocaleItem> = viewModel.appLanguage
                let isEmpty = PreferenceState.isEmpty(result)
                let isSuccess = PreferenceState.isSuccess(result)
                let isProcessing = PreferenceState.isProcessing(result)
                withAnimation(.easeInOut) {
                    if isSuccess() {
                        let locale = (result as! PreferenceStateSuccess<LocaleItem>).data
                        self.locale = Locale(identifier: "\(locale!.language)-\(locale!.country)")
                            showMainView = true
                        showOverlay = false
                    } else if isEmpty() {
                        showLanguageView = true
                        showMainView = false
                        showOverlay = false
                    } else if isProcessing() {
//                        showOverlay.toggle()
                    } else {
                        showOverlay = false
                    }
                }
            }
PS: Not the most beautiful code for sure, still learning swift with KMM
r
In that case it's most likely a thread problem. The viewModelScope is bound to the main thread. So if setValue is resource intensive it would block the main thread. Unless it's suspending
šŸ‘ 1
r
So if setValue is resource intensive it would block the main thread. Unless it’s suspending
In this case, it’s suspending or am I missing something here? šŸ¤”
But yeah, it definitely looks like a thread problem. Will go down the rabbit hole to investigate what’s the issue and try different approaches. (Will report back here if I I get to solve the issue)
Thanks @Rick Clephas šŸ™‡
r
Yeah it seems DataStore is suspending. So that shouldn't be a problem. You could try to replace setValue with a delay or something to verify the cause is somewhere in the setValue
r
Noted! Thanks šŸ‘Œ
a
I suspect SwiftUI isn't properly observing the flow. Have you used @StateViewModel as per documentation? :)
r
Yes,
@StateViewModel
is set as mentioned in the documentation
@StateViewModel var viewModel = HomeComponent().localeViewModel
Btw it’s nothing related to the DataStore. I’ve mocked the value from the
UseCase
and still happening. It must be something with the flows or Swift UI
Well, I found it’s nothing related to the data flow stream. It was the
withAnimation
on
showOverlay
which for some reason froze the app. Removing
withAnimation
for
showOverlay
works nicely šŸ‘Œ Not sure if this is a patch or is it is expected to not animate the blur and progress view but anyhow this solution helps me to forget about it and I can continue learning Swift UI and iOS ecosystem šŸ‘
Thank you both šŸ˜‰
I came to the conclusion that the value was being read set too fast like you mentioned before @Rick Clephas It’s just an issue I wouldn’t expect when developing with Android. Maybe it’s because these tools are not ā€œ_ideal_ā€ for iOS or does it happen also when developing with native iOS?
r
It's a time thing that could also occur in native iOS. You are launching a job on the main thread that isn't executed immediately. Once it's executed it will "block the main thread". If your code executes without ever unblocking the main thread (or only for a really short moment) the UI won't be updated (or just for a split second) to match the loading state. Instead it just goes straight to the loaded state.
šŸ‘ 1
āœ… 1
gratitude thank you 1