User interface

๐Ÿ“š Reminder

Compose Multiplatformopen in new window

Compose Multiplatform simplifies and accelerates UI development for Desktop and Web applications, and allows extensive UI code sharing between Android, iOS, Desktop and Web. It's a modern toolkit for building native UI. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs. It is based on Android Jetpack Composeopen in new window declarative UI approach ( which is similar also to SwiftUIopen in new window for iOS )

How to create composables ?

Composables are UI components that can be simply declared with code as functions, properties (such as text color, fonts...) as function parameters and subviews are declared on function declaration.

  • An @Composable annotation come always before the composable function.
  • Properties of size, behaviors of components can be set thanks to Modifiers
  • You can align components with containers composables such as Column (Vertically), Box, Row (Horizontally)
  • Also you can preview composables with the annotation @Preview before the composable annotation.

Example: 2 texts vertically aligned that fit all the width of the screen.

@Composable
internal fun App() {
    MaterialTheme {
        Column(Modifier.fillMaxWidth()) {
            Text( "My Text1", color = Color.Blue)
            Text(text = "My Text2")
        }
    }
}

TIP

For more information, you can have a look to Android Jetpack Compose documentation

Create composable for the Quiz

๐Ÿงช WelcomeScreen

Welcome Screen preview

You can now create your first view. For the Quiz we need a welcome screen displaying a Card centered with a button inside to start the quiz It is simply compose of the following composables :

  • a Card rounded shape container

  • a Text

  • a Button

  • Create a new composable WelcomeScreen.kt on commonMain module

  • Make sure that the App() composable is using it has below

@Composable
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            QuizAppAndroidTheme {
                  WelcomeScreen()
            }
        }
    }
}
  • Run you first view on all platforms , it should work.

๐ŸŽฏ Solutions

WelcomeScreen.kt (SourseSet : commonMain)
@Composable()
internal fun WelcomeScreen(){

    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier.fillMaxWidth().fillMaxHeight()
    ) {
        Card(
            shape = RoundedCornerShape(8.dp),
            modifier = Modifier.padding(10.dp),
        ) {
            Column(horizontalAlignment = Alignment.CenterHorizontally) {


                    Column(horizontalAlignment = Alignment.CenterHorizontally) {
                        Text(
                            text = "Quiz",
                            fontSize = 30.sp,
                            modifier = Modifier.padding(all = 10.dp)
                        )
                        Text(
                            modifier = Modifier.padding(all = 10.dp),
                            text = "A simple Quiz to discovers KMP and compose.",
                        )
                        Button(
                            modifier = Modifier.padding(all = 10.dp),
                            onClick = {  }

                        ) {
                            Text("Start the Quiz")
                        }
                    }
            }
        }
    }
}

๐Ÿงช ScoreScreen

Score Screen preview

The second view will be quite similar but able de show final scores

  • Create a new composable ScoreScreen.kt on commonMain module
  • Make sure that the App() composable is using it has below
  • The composable will have a String value as parameter
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            QuizAppAndroidTheme {
                  ScoreScreen("10/20")
            }
        }
    }
}
  • Run you first view on all platforms , it should work.

๐ŸŽฏ Solutions

ScoreScreen.kt
@Composable()
internal fun ScoreScreen(score: String){
    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier.fillMaxWidth().fillMaxHeight()
    ) {
        Card(
            shape = RoundedCornerShape(8.dp),
            modifier = Modifier.padding(10.dp),
            backgroundColor = Color.Green

        ) {
            Column(horizontalAlignment = Alignment.CenterHorizontally) {


                    Column(horizontalAlignment = Alignment.CenterHorizontally) {

                        Text(
                            fontSize = 15.sp,
                            text = "score",
                        )
                        Text(
                            fontSize = 30.sp,
                            text = score,
                        )
                        Button(
                            modifier = Modifier.padding(all = 20.dp),
                            onClick = {
                            }
                        ) {
                            Icon(Icons.Filled.Refresh, contentDescription = "Localized description")
                            Text(text = "Retake the Quiz",)

                        }
                    }
            }
        }
    }
}

๐Ÿงช QuestionScreen

Data classes for Quiz modeling

class_diagram

We can create classes on the package network.data

Answer.kt
data class Answer(val id: Int, val label: String )
Question.kt
data class Question(val id:Int, val label:String, val correctAnswerId:Int, val answers:List<Answer>)
Quiz.kt
data class Quiz(var questions: List<Question>)

Make the composable

Now we can make a composable with interactions.

class_diagram

The screen is composed of :

  • The question label in a Card
  • Single choice answer component with RadioButton
  • A Button to submit the answer
  • A LinearProgressIndicator indicating the quiz progress

After creating the UI view, we can pass to this composable the list of questions. When the Appcomposable will create questionScreen() composable we will generate mock questions data for now to generate the list of questions.

State management

All views of question will be one unique composable that updates with the correct question/answers data each time we are clicking on the next button.

We use MutableState value for that. It permit to keep data value and recompose the view when the data is changed. It's exactly what we need for our quiz page :

  • Keep the value of the question position on the list
  • Keep the value of the answer selected by the user each time he switch between RadioButtons
  • Keep the score to get the final one at the end of the list.

Here is an example of MutableState value declaration

    var questionProgress by remember { mutableStateOf(0) }
    ...

You can declare the 2 other MutableState values and after use it on your composable ensuring that on the button click questionProgressis incrementing so the question and his answers can change on the view.

๐ŸŽฏ Solutions

QuestionScreen.kt


@Composable()
internal fun QuestionScreen(questions: List<Question>) {

    var questionProgress by remember { mutableStateOf(0) }
    var selectedAnswer by remember { mutableStateOf(1) }
    var score by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.fillMaxWidth().fillMaxHeight(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Card(
            shape = RoundedCornerShape(5.dp),
            modifier = Modifier.padding(60.dp)
        ) {
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier.padding(horizontal = 10.dp)
            ) {
                Text(
                    modifier = Modifier.padding(all = 10.dp),
                    text = questions[questionProgress].label,
                    fontSize = 25.sp,
                    textAlign = TextAlign.Center
                )
            }
        }
        Column(modifier = Modifier.selectableGroup()) {
            questions[questionProgress].answers.forEach { answer ->
                Row(
                    modifier = Modifier.padding(horizontal = 16.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    RadioButton(
                        modifier = Modifier.padding(end = 16.dp),
                        selected = (selectedAnswer == answer.id),
                        onClick = { selectedAnswer = answer.id },
                    )
                    Text(text = answer.label)
                }
            }
        }
        Column(modifier = Modifier.fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom) {
            Button(
                modifier = Modifier.padding(bottom = 20.dp),
                onClick = {
                    if(selectedAnswer == questions[questionProgress].correctAnswerId) {
                        score++
                    }
                    if (questionProgress < questions.size - 1) {
                        questionProgress++
                        selectedAnswer = 1
                    }else{
                        // Go to the score section
                    }
                }
            ) {
                if(questionProgress < questions.size - 1) nextOrDoneButton(Icons.Filled.ArrowForward,"Next")
                else nextOrDoneButton(Icons.Filled.Done,"Done")
            }
            LinearProgressIndicator(modifier = Modifier.fillMaxWidth().height(20.dp), progress = questionProgress.div(questions.size.toFloat()).plus(1.div(questions.size.toFloat())))
        }
    }
}

@Composable
internal fun nextOrDoneButton(iv: ImageVector, label:String){
    Icon(
        iv,
        contentDescription = "Localized description",
        Modifier.padding(end = 15.dp)
    )
    Text(label)
}
MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            QuizAppAndroidTheme {
                var questions = 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"))
                    )
                )
                QuestionScreen(questions)
            }
        }
    }
}

Your Quiz have now all his composable screens made. Let's connect it to the Internet

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

TIP

The full solution for this section is availabe here

๐Ÿ“– Further reading

Last Updated:
Contributors: Ibrahim Gharbi