marcinmoskala
11/29/2024, 7:35 PMmarcinmoskala
11/29/2024, 7:39 PMdata class Lesson(
val lessonId: String,
val name: String,
val steps: List<LessonStep>,
)
sealed interface LessonStep {
data class Text(val text: String) : LessonStep
data class SingleChoiceQuestion(
val question: String,
val answers: ImmutableList<String>,
val correctAnswer: Int,
) : LessonStep
data class MultipleChoiceQuestion(
val question: String,
val answers: ImmutableList<String>,
val correctAnswers: ImmutableSet<Int>,
) : LessonStep
...
}
Most of the approaches I've seen would be very hard to apply to this use case. A view model with StateFlow properties for each option, like optionChosen, seen unimaginable to me, with such a huge variation of different "modes" of what can be displayed.
Here are the options I see for now for modeling it:
1. React-like hierarchy of views with defining state as low as possible. (pseudocode to make it shorter and to only show key concepts as simply as possible)
LessonScreen(repository)
val loading by remember { mutableStateOf<Boolean>(true) }
val lessons by remember { mutableStateOf<...>(...) }
val currentLesson by remember { mutableStateOf(0) }
val answer by remember { mutableStateOf<AnserStatus>(NotGiven) }
LaunchEffect { lessons = repository.fetchLessons() }
if (loading) Loading() else Lesson(lessons[currentLesson], onAnswer = { currentLesson++ })
Lesson(lesson, onAnswer)
when(lesson) {
is Text -> TextLesson(lesson)
is SingleQuestion -> SingleQuestionLesson(lesson, onAnswer)
}
SingleQuestion(lesson, onAnswer)
val answer by remember { mutableStateOf<Int>(-1) }
val answerGiven by remember { mutableStateOf<Boolean>(false) }
lesson.options.forEachIndexed { i, v -> Option(v.text, onChosen = { if(!answerGiven) answer = i }) }
Button(onClick = {
answerGiven = true
onAnswer(if(answer = lesson.correctAnswer) Correct else Incorrect)
) { Text("Submit") }
...
2. ViewModel representing view as a contract that shows what should be displayed, and accepting generic actions as function calls. Stateless view.
LessonViewModel
uiState = StateFlow<UiState>(Loading)
currentLesson: Lesson?
init {
viewModelScope.launch {
val lesson = courseRepository.getLesson(courseId, lessonId)
currentLesson = lesson
uiState = UiState.LessonStep(step = lesson.steps.first().toUiStep())
}
}
fun onOptionClicked(option: String) {
when(currentLesson) {
is SingleChoiceQuestion -> uiState.update { /* Changing question so only the clicked one is selected */ }
is Text -> error("Impossible")
}
}
fun onChosenOptionClicked(option: String) ...
fun onOptionSubmitActionClicked(option: String) ...
sealed interface UiState {
data object Loading: UiState
data class LessonStep(
val step: LessonStepUi
): UiState {
sealed interface LessonStepUi
data class Text(
val text: String,
) : LessonStepUi
data class SingleChoiceQuestion(
val question: String,
val options: ImmutableList<ChoiceQuestionOption>,
) : LessonStepUi
...
}
...
}
LessonScreen(viewModel)
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when(uiState) {
is Loading -> Loading()
is LessonStep -> Lesson(uiState.lesson, onOptionClicked=viewModel::onOptionClicked, ...)
}
Lesson(lesson, onOptionClicked, onSubmitClicked)
when(lesson) {
is LessonStep.Text -> TextLesson(lesson)
is LessonStep.SingleQuestion -> SingleQuestionLesson(lesson, onOptionClicked)
}
SingleQuestion(lesson, onOptionClicked, onSubmitClicked)
lesson.options.forEachIndexed { i, v -> Option(v.text, onChosen = onOptionClicked) }
Button(onClick = onSubmitClicked) { Text("Submit") }
3. ViewModel representing view as a contract that shows what should be displayed, and accepting one action with possible UI actions. Stateless view.
// Same as before, but
ViewModel
fun onAction(uiAction: UiAction) {
when(uiAction) {
is OnOptionClicked -> when(currentLessing) {
is LessonStep.Text -> TextLesson(lesson)
is LessonStep.SingleQuestion -> SingleQuestionLesson(lesson, onOptionClicked)
...
...
}
}
sealed class UiAction
class OnOptionClicked(option: String)
...
LessonScreen(viewModel)
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when(uiState) {
is Loading -> Loading()
is LessonStep -> Lesson(uiState.lesson, onAction=viewModel::onAction)
}
// How deep should it go?
4. A different view models for the screen and different simple view models for different lessons. (How should they communicate?)
Something else? How would you see that?marcinmoskala
11/29/2024, 11:22 PMDaniel Weidensdörfer
11/30/2024, 12:28 PM