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 pattern
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 flow
"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 StateFlow
A state flow is a hot flow because its active instance exists independently of the presence of collectors (our composables that consume the data)
Coroutine
"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 = WelcomeRoute,
) {
composable<WelcomeRoute>() {
welcomeScreen(
onStartButtonPushed = {
navController.navigate(route = QuizRoute)
}
)
}
composable<QuizRoute>() {
val questions by quizRepository.questionState.collectAsState()
questionScreen(
questions = questions,
/* FOR SPEAKER TALK DEMO ON WEB APP */
onFinishButtonPushed = {
score: Int, questionSize: Int -> navController.navigate(route = ScoreRoute(score, questionSize))
}
)
}
composable<ScoreRoute> { backStackEntry ->
val scoreRoute: ScoreRoute = backStackEntry.toRoute<ScoreRoute>()
scoreScreen(
score = scoreRoute.score,
total = scoreRoute.questionSize,
onResetButtonPushed = {
navController.navigate(route = QuizRoute)
}
)
}
}
}
}
Sources
The full solution for this section is availabe here
🧪 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 ViewModels
are just available on KMP. But you can also use a third party library such as Moko-MVVM
or KMM-ViewModel
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 = QuizRoute) {
val questions by quizViewModel.questionState.collectAsState()
Sources
The full solution for this section is availabe here
✅ If everything is fine, go to the next chapter →