Skip to main content

Events, Data and State flow

As you may already know, Kunafa relies heavily on observables and events to handle data manipulation and state representation. I'll touch briefly on each one of these topics and provide an simple example that ties them together at the end.

 

Events

Basic events: 

  • onChange, onClick, onHover ... etc
    you'll find those quite a lot in forms in general. 
    textInput {
      text = formViewModel.username
      onChange = {
        formViewModel.username = text
      }
    }

Not so basic:

  • observables
    you'll use them to make your own types of events. For further reading refer to this and this
	override fun onViewCreated(lifeCycleOwner) {
      viewModel.getFormData()
      viewModel.submitState.observe {
      	if( it == BasicUiState.Loaded ) {
        	formViewModel.username = ""
          	formViewModel.email = ""
        }
      }
    }
    

Data:

It can be anything, in our example we deal  with two strings that we populate the form input with and edit then submit to send it back to the server.

 

State Flow:

We start with getting the data from the server in onViewCreated and then we edit the data and submit that to the server after clicking submit.

class ExampleForm(private val viewModel: FormViewModel): Component() {
	override fun onViewCreated(lifeCycleOwner) {
      viewModel.getFormData()
    }
    
    override fun View?.getView() = verticalLayout {
    	form {
          	label("Username", isRequired = true)
        	textInput {
            	text = formViewModel.username
              	onChange = {
                	formViewModel.username = text
                }
            }
            
            label("Email", isRequired = true)
            textInput {
            	text = formViewModel.email
              	onChange = {
                	formViewModel.email = text
                }
            }
            button {
            	text = "submit"
              	onClick = {
                  viewModel.submitData()
                }
            }
        }
    }
}

class FormViewModel() {
  
  	var username = ""
    var email = ""
    
	val uiState = Observable<BasicUiState>()
	val submitState = Observable<BasicUiState>()
    
	fun getFormData() {
    	basicNetworkCall(uiState) {
          val someData = someGlobalClient.getDataFromServer()
          username = someData.username
          email = someData.email
        }
    }
    
    fun submitData() {
      	basicNetworkCall(submitState) {
          someGlobalClient.submitData(username, email)
        }
    }
}

 

Basic network call

we use basicNetworkCall to make network calls and tie a uiState to the state of the network call whether its failure or success. this uiState enum can be observed in the component to trigger an event.

 basicNetworkCall wraps networkCall  to make the network call

fun basicNetworkCall(uiState: Observable<BasicUiState>, call: suspend CoroutineScope.() -> Unit) {
    networkCall(
        before = { uiState.value = BasicUiState.Loading },
        onConnectionError = { uiState.value = BasicUiState.Error }
    ) {
        try {
            call()
            uiState.value = BasicUiState.Loaded
        } catch (t: Throwable) {
            uiState.value = BasicUiState.Error
            t.printStackTrace()
        }
    }
}


Network call

networkCall handles the actual network call delegated by basicNetworkCall, you have functions that can be provided by the user if they care about fine grained error handling, otherwise default functions will be used.

fun networkCall(
    before: () -> Unit = {},
    final: suspend CoroutineScope.() -> Unit = { },
    onConnectionError: suspend CoroutineScope.() -> Unit = { },
    onUnknownError: suspend CoroutineScope.() -> Unit = onConnectionError,
    onUnauthorized: suspend CoroutineScope.() -> Unit = onConnectionError,
    onInvalidRequest: suspend CoroutineScope.() -> Unit = onConnectionError,
    onUserDisabled: suspend CoroutineScope.() -> Unit = { logoutUser() },
    call: suspend CoroutineScope.() -> Unit
): Job {
    before()
    return GlobalScope.launch(Dispatchers.Default) {
        try {
            withTimeout(30_000) {
                call()
            }
        } catch (e: ConnectionErrorException) {
            withContext(Dispatchers.Main) { onConnectionError() }
        } catch (e: UnknownErrorException) {
            withContext(Dispatchers.Main) { onUnknownError() }
        } catch (e: UnauthorizedException) {
            withContext(Dispatchers.Main) { onUnauthorized() }
        } catch (e: DisabledUserException) {
            withContext(Dispatchers.Main) { onUserDisabled() }
        } catch (e: InvalidRequestException) {
            withContext(Dispatchers.Main) { onInvalidRequest() }
        } catch (e: TimeoutCancellationException) {
            withContext(Dispatchers.Main) { onConnectionError() }
        } catch (e: Throwable) {
            withContext(Dispatchers.Main) { onInvalidRequest() }
        } finally {
            withContext(Dispatchers.Main) { final() }
        }
    }
}