icerockdev / moko-widgets

Multiplatform UI DSL with screen management in common code for mobile (android & ios) Kotlin Multiplatform development
https://moko.icerock.dev
Apache License 2.0
388 stars 32 forks source link

Error createContentWidget() #253

Closed Diy2210 closed 4 years ago

Diy2210 commented 4 years ago

Hi, I try parse in Screen using Coroutine, but createContentWidget() print error, I dont understand why, plz help. My code: Снимок экрана 2020-06-25 в 15 50 44 Снимок экрана 2020-06-25 в 15 51 50 Снимок экрана 2020-06-25 в 15 52 50

Tetraquark commented 4 years ago

Because the createContentWidget function must return an object of Widget class but you return object of Job class from launch function.

It is incorrect to run asynchronous coroutines inside the synchronous widget creation function createContentWidget. You send the request to the server that will send a response at an unspecified time. The screen will be rendered before the server sends a response. This is why widgets should observe data reactively something like this:

private val widgetData = MutableLiveData<String>("")

private fun launchAsyncRequest() {
    coroutineScope.launch {
        try {
            client.getStatusServer(url, "/endpoint", token) { response ->
                if (response.contains("success")) {
                    widgetData.value = response
                } else {
                    widgetData.value = "Error"
                }
            }
        } catch (exception: Exception) {
            widgetData.value = "Error"
        }
    }
}

override fun createContentWidget(): Widget<WidgetSize.Const<SizeSpec.AsParent, SizeSpec.AsParent>> {
    launchAsyncRequest()

    return with(theme) {
        container(size = WidgetSize.AsParent) {
            text(
                size = WidgetSize.WrapContent,
                text = widgetData.map { it.desc() }
            )
        }
    }
}

But it's bad way too. Do you use moko-mvvm library? A more correct way to run async code, launch all coroutines and send all requests to the server is to use ViewModel class. You can find the example of combined usage of moko-widgets and moko-mvvm here. Example of screen and viewmodel for the screen.

Diy2210 commented 4 years ago

Tnx!

Diy2210 commented 4 years ago

I have one more question. I create Model from screen and create fun async. How call this fun in screen and get parse value. My Model: Снимок экрана 2020-06-26 в 17 05 07 My Screen code: Снимок экрана 2020-06-26 в 17 05 48

Tetraquark commented 4 years ago
  1. You can start all async methods with coroutines in init block of the ViewModel class.
  2. You should wrap all the ViewModel properties that observed by the screen to LiveData objects, because widgets will get all data reactivly.
class DetailsViewModel : VewModel() {
    // ...

    private val _hostName = MutableLiveData<String>("initString")
    val hostName: LiveData<StringDesc> = _hostName.map { it.desc() }

    init {
        launchAsync()
    }

    fun launchAsync() {
        viewModelScope.launch {
            // ...
            _hostName.value = resultString
        }
    }
}

And in the WidgetScreen:

override fun createContentWidget(): Widget<WidgetSize.Const<SizeSpec.AsParent, SizeSpec.AsParent>> {
    val viewModel = getViewModel {
        createViewModel()
    }

    return with(theme) {
        container(size = WidgetSize.AsParent) {
            text(
                size = WidgetSize.WrapContent,
                text = viewModel.hostname
            )
        }
    }
}
Diy2210 commented 4 years ago

How use LiveData objects, else I use Model. I try add LiveData and use it in text, but text field is empty. My model data class: Снимок экрана 2020-06-30 в 14 01 13

My Screen Model: Снимок экрана 2020-06-30 в 15 59 30 Снимок экрана 2020-06-30 в 15 59 40

Screen code: Снимок экрана 2020-06-30 в 15 59 56

Tetraquark commented 4 years ago

Because you should change object in value property of the MutableLiveData, not the properties values of the object. So, change value of LiveData by creating new object oh the HostModel class:

data class HostModel(
    val hostName: String,
    val os: Int,
    val ip: String
)

val mutableLiveData = MutableLiveData<HostModel>(HostModel("Empty", 0, "0.0.0.0"))

fun launchAsyncRequest() {

    // ...

    mutableLiveData.value = HostModel(
        hostName = newHostName,
        os = newOs,
        ip = newIp
    )

    // ...
}
Diy2210 commented 4 years ago

Its work, thx again!

Diy2210 commented 4 years ago

Last question. How implement MutableLiveData type Array(String)?

Tetraquark commented 4 years ago

For array:

val stringArrayLiveData = MutableLiveData<Array<String>>(emptyArray())
stringArrayLiveData.value = arrayOf("1", "2", "3")

For list:

val stringListLiveData = MutableLiveData<List<String>>(emptyList())
stringListLiveData.value = listOf("1", "2", "3")
Diy2210 commented 4 years ago

Cool Tnx!