joreilly/BikeShare

Using StateFlow, so that the model is fully on the shared/multiplatform section

dbaroncelli opened this issue · 5 comments

Hi John,

I have been experimenting with StateFlow and Compose in the last days, and it seems really amazing.
I was able to really move the whole model to the shared/multiplatform section, so that the platform-specific code is really just the UI.

If you use StateFlow, you can really stop using platform-specific structures such as LiveData.
You might want to have a look into that, as it greatly simplifies everything.

This is how StateFlow works with Compose:

class MainActivity : AppCompatActivity() {

    val viewModel = ViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyTextComponent(viewModel.state)
        }
    }
}

@Composable
@ExperimentalCoroutinesApi
fun MyTextComponent(stateFlow: StateFlow<DataModel>) {
    val state by stateFlow.collectAsState()
    Text(text = state.mytext)
}

One of the big architecture philosophy behind the StateFlow is essentially the MVI pattern.
You don't talk anymore in terms of dataActions, but in terms of userActions (also referred to as "Intents").
So, in the platform-specific section, you would not call functions as "fetchBikeShareInfo", but as "ClickOnButton", "ClickOnLink", etc.
On the platform-specific section, you don't deal with data at all, as the whole model is fully contained in the shared/multiplatform section. Your platform-specific user intents would just trigger a function that is defined inside the shared/multiplatform section, and all the data processing happens there.

class ViewModel {

    private val webservicesAPI = WebservicesAPI()

    private val _state = MutableStateFlow(DataModel())
    val state: StateFlow<DataModel>
        get() = _state

    fun ClickOnButton(buttonType: String) {
        launch {
            val fetchedText = webservicesAPI.fetchText(buttonType)
            _state.value = _state.value.copy(mytext = fetchedText)
        }
    }

}


data class DataModel(
    val mytext : String = "initial text"
)

I had started to look a a bit at StateFlow in https://github.com/joreilly/PeopleInSpace/tree/coroutines_1_3_6 branch but hadn't been able to use in master at that time due to non-availability of version of native-mt version of kotlinx coroutines that supported it.....now that that's not the case I must definitely take a look again.

yes, I think StateFlow is really what makes the new declarative UIs amazing, as the platform-specific code would just consist of that

just merged initial change to just swap in StateFlow for LiveData ....need to think a bit more about role of view model (in conjunction with how it's used on iOS/macOS as well). I've tended to avoid pushing shared code too high up the stack to allow native capabilities where they're best suited (and more likely to be used).

I think Kotlin is an awesome language, and it's doing a great job embedding inside the language what previously could only be found in the specific platform. StateFlow is a great example, replacing LiveData.
I haven't really looked into SwiftUI yet, but if I can assume there will be an easy way to link it to StateFlow.

Another consideration it came out to me, is that if you use a truly MVI pattern via StateFlow, with the full model stored in the shared/multiplatform section, then you don't need to manage the state such as bottomBarSelectedIndex inside the UI.
Such state would be kept inside the model, it wouldn't need to be duplicated for each platform.