Architecture

Let's connect our Quiz app to internet.

Overview

Architecture basics

Everything You NEED to Know About MVVM Architecture Patterns

Data layer for KMP

Data layer in KMP is under building but largly inspired by Android Architecture patternopen in new window

data layer overview

Repository classes are responsible for the following tasks:

  • Exposing data to the rest of the app.
  • Centralizing changes to the data.
  • Resolving conflicts between multiple data sources.
  • Abstracting sources of data from the rest of the app.
  • Containing business logic.

Kotlin flowopen in new window

"A flow is an asynchronous data stream that sequentially emits values and completes normally or with an exception."

There are multiple types of flow, for the Hands-on Lab, we will focus on StateFlowopen in new window

A state flow is a hot flow because its active instance exists independently of the presence of collectors (our composables that consume the data)

Coroutineopen in new window

"A coroutine is an instance of suspendable computation. It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one."

๐Ÿงช DataSource and Repository

  • Create a mock datasource, that generate a list of question
  • Use it with a repository
  • Use the repository on the root of your application ( navHost in App.kt)

๐ŸŽฏ Solutions

Add coroutine dependancy to your project.

build.gradle.kts (commonMain)
commonMain.dependencies {
           ...
            implementation(libs.kotlinx.coroutines.core)
        }
MockDataSource.kt
package com.worldline.quiz.data.datasources

class MockDataSource {

  fun generateDummyQuestionsList():List<Question>{
        return listOf(
            Question(
                1,
                "Android is a great platform ?",
                1,
                listOf(
                    Answer( 1,"YES"),
                    Answer(2,"NO")
                )
            ),
            Question(
                1,
                "Android is a bad platform ?",
                2,
                listOf(
                    Answer( 1,"YES"),
                    Answer(2,"NO")
                )
            )
        )
    }

}
QuizRepository.kt
package com.worldline.quiz.data

class QuizRepository()  {

    private val mockDataSource = MockDataSource()
    private val coroutineScope = CoroutineScope(Dispatchers.Main)
    private var _questionState=  MutableStateFlow(listOf<Question>())
    var questionState = _questionState

    init {
        updateQuiz()
    }

    private fun updateQuiz(){
        coroutineScope.launch {
            _questionState.update {
                    mockDataSource.generateDummyQuestionsList()
            }
        }
    }
}
App.kt
@Composable
fun App(
    navController: NavHostController = rememberNavController(),
    quizRepository: QuizRepository = QuizRepository()
) {

    MaterialTheme {
        NavHost(
            navController = navController,
            startDestination = "/welcome",
        ) {


            composable(route = "/welcome") {
                welcomeScreen(
                    onStartButtonPushed = {
                        navController.navigate(route = "/quiz")
                    }
                )
            }
            composable(route = "/quiz") {
                val questions by quizRepository.questionState.collectAsState()
                    questionScreen(
                        questions = questions,
                        /* FOR SPEAKER TALK DEMO ON WEB APP */
                        onFinishButtonPushed = {
                            score: Int, questionSize: Int -> navController.navigate(route = "/score/$score/$questionSize")
                        }
                    )
            }
            composable(route = "/score/{score}/{total}") {
                scoreScreen(
                    score = it.arguments?.getString("score")?.toInt() ?:-1,
                    total = it.arguments?.getString("total")?.toInt() ?:-1,
                    onResetButtonPushed = {
                        navController.navigate(route = "/quiz")
                    }
                )
            }

        }
    }
}

Sources

The full solution for this section is availabe hereopen in new window

๐Ÿงช ViewModel

  • Create a ViewModel class
  • Upgrade the repository that is no more storing the flow and move it to the ViewModel
  • Upgrade the App to use the ViewModel instead of the Repository

Third party Architecture libraries

Domain layer framework such as ViewModelsopen in new window are just available on KMP. But you can also use a third party library such as Moko-MVVMopen in new window or KMM-ViewModelopen in new window or precompose

gradle.build.kts (module : composeApp)
...
 commonMain.dependencies {
            ...
            implementation(libs.androidx.lifecycle.viewmodel.compose)
...
QuizViewModel.kt
package com.worldline.quiz

class QuizViewModel : ViewModel() {
    private var quizRepository: QuizRepository = QuizRepository()
    private var _questionState = MutableStateFlow(listOf<Question>())
    var questionState: StateFlow<List<Question>> = _questionState

    /* Can be replaced with explicit backing fields
    val questionState : StateFlow<List<Question>>
       field =  MutableStateFlow(listOf<Question>())
    -> in build.gradle.kts : sourceSets.all { languageSettings.enableLanguageFeature("ExplicitBackingFields") }
    */

    init {
        getQuestionQuiz()
    }

    private fun getQuestionQuiz() {
        viewModelScope.launch(Dispatchers.Default) {
            _questionState.update {
                quizRepository.updateQuiz()
            }
        }
    }
}
QuizRepository.kt
class QuizRepository  {
    private val mockDataSource = MockDataSource()
    fun updateQuiz():List<Question>{
            return mockDataSource.generateDummyQuestionsList()
    }
}
App.kt
fun App(
    navController: NavHostController = rememberNavController(),
    quizViewModel: QuizViewModel = QuizViewModel()
) {
...
composable(route = "/quiz") {
                val questions by quizViewModel.questionState.collectAsState()

Sources

The full solution for this section is availabe hereopen in new window

โœ… If everything is fine, go to the next chapter โ†’

๐Ÿ“– Further reading

Last Updated:
Contributors: Ibrahim Gharbi