feat: add quiz bottom sheet UI for video content#987
feat: add quiz bottom sheet UI for video content#987
Conversation
HarishV14
commented
Nov 4, 2025
- Add VideoQuizSheetFragment with bottom sheet dialog
- Add layouts for quiz options and feedback display
- Support radio, checkbox, and gap-fill question types
- Add visual feedback with correct/incorrect borders and icons
- Add string resources for quiz buttons and feedback messages
- Add drawable resources for quiz borders (correct/incorrect)
- Add VideoQuizSheetFragment with bottom sheet dialog - Add layouts for quiz options and feedback display - Support radio, checkbox, and gap-fill question types - Add visual feedback with correct/incorrect borders and icons - Add string resources for quiz buttons and feedback messages - Add drawable resources for quiz borders (correct/incorrect)
Summary of ChangesHello @HarishV14, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a quiz bottom sheet UI for video content, designed to enhance user engagement and provide a more interactive learning experience. It includes layouts for various question types, visual feedback for answers, and resources for customization. The changes also incorporate mock questions and an auto-trigger feature for testing purposes. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a quiz feature within a bottom sheet for video content, which is a nice addition. The implementation supports various question types and provides clear visual feedback. However, there are some significant areas for improvement. The most critical issue is the inclusion of a large amount of test-only code—including mock data, UI elements, and logic—directly within production fragments. This code must be moved to a debug-only build variant to keep the production app clean and lean. Additionally, the new VideoQuizSheetFragment is quite large and complex, handling too many responsibilities. It would benefit from refactoring to use a ViewModel for logic and state management, which would improve its maintainability and testability. Other suggestions include making listener attachment more robust and avoiding brittle patterns like checking a button's text to determine its state.
| class VideoQuizSheetFragment : BottomSheetDialogFragment() { | ||
|
|
||
| private lateinit var question: DummyQuestion | ||
| private var listener: OnQuizCompleteListener? = null | ||
|
|
||
| private var answerViews = mutableListOf<View>() | ||
| private var actionButton: TextView? = null | ||
| private var feedbackText: TextView? = null | ||
| private var radioGroup: RadioGroup? = null | ||
|
|
||
| private var selectedAnswerIds = mutableListOf<Long>() | ||
| private var isCorrect = false | ||
| private var gapFillResults = mutableListOf<Boolean>() | ||
|
|
||
| interface OnQuizCompleteListener { | ||
| fun onQuizCompleted(questionId: Long) | ||
| } | ||
|
|
||
| companion object { | ||
| private const val ARG_QUESTION = "ARG_QUESTION" | ||
| fun newInstance(question: DummyQuestion): VideoQuizSheetFragment { | ||
| val fragment = VideoQuizSheetFragment() | ||
| val args = Bundle() | ||
| args.putString(ARG_QUESTION, Gson().toJson(question)) | ||
| fragment.arguments = args | ||
| return fragment | ||
| } | ||
| } | ||
|
|
||
| override fun onAttach(context: Context) { | ||
| super.onAttach(context) | ||
| if (parentFragment is OnQuizCompleteListener) { | ||
| listener = parentFragment as OnQuizCompleteListener | ||
| } else { | ||
| // When using activity's fragment manager (fullscreen), parentFragment may be null | ||
| activity?.supportFragmentManager?.fragments?.forEach { fragment -> | ||
| if (fragment is OnQuizCompleteListener) { | ||
| listener = fragment as OnQuizCompleteListener | ||
| return@forEach | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
| question = Gson().fromJson( | ||
| arguments?.getString(ARG_QUESTION), | ||
| DummyQuestion::class.java | ||
| ) | ||
| isCancelable = false | ||
| setStyle(STYLE_NORMAL, com.google.android.material.R.style.Theme_MaterialComponents_BottomSheetDialog) | ||
| } | ||
|
|
||
| override fun onCreateView( | ||
| inflater: LayoutInflater, | ||
| container: ViewGroup?, | ||
| savedInstanceState: Bundle? | ||
| ): View? { | ||
| return inflater.inflate(R.layout.fragment_video_quiz_sheet, container, false) | ||
| } | ||
|
|
||
| override fun onStart() { | ||
| super.onStart() | ||
| dialog?.let { | ||
| it.setOnKeyListener { _, keyCode, event -> | ||
| if (keyCode == android.view.KeyEvent.KEYCODE_BACK && event.action == android.view.KeyEvent.ACTION_UP) { | ||
| true | ||
| } else { | ||
| false | ||
| } | ||
| } | ||
|
|
||
| val bottomSheet = it.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet) | ||
| if (bottomSheet != null) { | ||
| val behavior = BottomSheetBehavior.from(bottomSheet) | ||
| behavior.peekHeight = 0 | ||
| behavior.state = BottomSheetBehavior.STATE_EXPANDED | ||
| behavior.isHideable = false | ||
| behavior.isDraggable = false | ||
| behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { | ||
| override fun onStateChanged(bottomSheet: View, newState: Int) { | ||
| // Prevent any state changes - keep it expanded | ||
| if (newState != BottomSheetBehavior.STATE_EXPANDED) { | ||
| behavior.state = BottomSheetBehavior.STATE_EXPANDED | ||
| } | ||
| } | ||
| override fun onSlide(bottomSheet: View, slideOffset: Float) { | ||
| // Prevent sliding - keep it at expanded position | ||
| if (slideOffset < 0f) { | ||
| behavior.state = BottomSheetBehavior.STATE_EXPANDED | ||
| } | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| // Fullscreen support: Ensure dialog appears above fullscreen video dialogs | ||
| val window = it.window | ||
| window?.let { w -> | ||
| w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) | ||
| // FLAG_LAYOUT_IN_SCREEN ensures dialog appears above fullscreen dialogs | ||
| w.setFlags( | ||
| android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, | ||
| android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | ||
| ) | ||
| // Ensure dialog is interactive (not disabled by fullscreen mode) | ||
| w.clearFlags(android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) | ||
| w.clearFlags(android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
|
|
||
| val questionContainer: FrameLayout = view.findViewById(R.id.quiz_question_container) | ||
| val optionsContainer: LinearLayout = view.findViewById(R.id.quiz_options_container) | ||
| actionButton = view.findViewById(R.id.quiz_action_button) | ||
| feedbackText = view.findViewById(R.id.quiz_feedback_text) | ||
|
|
||
| answerViews.clear() | ||
| buildQuestionUI(layoutInflater, questionContainer, optionsContainer, question) | ||
|
|
||
| actionButton?.setOnClickListener { | ||
| if (actionButton?.text.toString().equals(getString(R.string.quiz_button_check), ignoreCase = true)) { | ||
| isCorrect = checkAnswers() | ||
| showFeedback(isCorrect) | ||
| disableOptions() | ||
| actionButton?.text = getString(R.string.quiz_button_continue) | ||
| } else { | ||
| listener?.onQuizCompleted(question.id) | ||
| dismiss() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun buildQuestionUI( | ||
| inflater: LayoutInflater, | ||
| questionContainer: FrameLayout, | ||
| optionsContainer: LinearLayout, | ||
| question: DummyQuestion | ||
| ) { | ||
| val context = inflater.context | ||
| when (question.type) { | ||
| "R" -> { | ||
| val questionTextView = TextView(context).apply { | ||
| text = HtmlCompat.fromHtml(question.questionHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) | ||
| setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.testpress_text_size_large)) | ||
| setTypeface(null, Typeface.BOLD) | ||
| setTextColor(ContextCompat.getColor(context, R.color.testpress_black)) | ||
| } | ||
| questionContainer.addView(questionTextView) | ||
|
|
||
| radioGroup = RadioGroup(context).apply { | ||
| orientation = LinearLayout.VERTICAL | ||
| id = View.generateViewId() | ||
| } | ||
|
|
||
| question.answers.forEach { answer -> | ||
| val optionView = inflater.inflate(R.layout.list_item_quiz_option, radioGroup, false) | ||
| val choiceContainer = optionView.findViewById<FrameLayout>(R.id.choice_container) | ||
|
|
||
| val radioButton = MaterialRadioButton(context).apply { | ||
| text = HtmlCompat.fromHtml(answer.textHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) | ||
| tag = answer | ||
| id = View.generateViewId() | ||
| setTextColor(ContextCompat.getColor(context, R.color.testpress_table_text)) | ||
| setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.testpress_text_size_xmedium)) | ||
| } | ||
|
|
||
| radioButton.setOnCheckedChangeListener { _, isChecked -> | ||
| if (isChecked) actionButton?.isEnabled = true | ||
| } | ||
|
|
||
| choiceContainer.addView(radioButton) | ||
| radioGroup!!.addView(optionView) | ||
| answerViews.add(optionView) | ||
| } | ||
| optionsContainer.addView(radioGroup) | ||
| } | ||
| "C" -> { | ||
| val questionTextView = TextView(context).apply { | ||
| text = HtmlCompat.fromHtml(question.questionHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) | ||
| setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.testpress_text_size_large)) | ||
| setTypeface(null, Typeface.BOLD) | ||
| setTextColor(ContextCompat.getColor(context, R.color.testpress_black)) | ||
| } | ||
| questionContainer.addView(questionTextView) | ||
|
|
||
| question.answers.forEach { answer -> | ||
| val optionView = inflater.inflate(R.layout.list_item_quiz_option, optionsContainer, false) | ||
| val choiceContainer = optionView.findViewById<FrameLayout>(R.id.choice_container) | ||
|
|
||
| val checkBox = MaterialCheckBox(context).apply { | ||
| text = HtmlCompat.fromHtml(answer.textHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) | ||
| tag = answer | ||
| id = View.generateViewId() | ||
| setTextColor(ContextCompat.getColor(context, R.color.testpress_table_text)) | ||
| setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.testpress_text_size_xmedium)) | ||
| val states = arrayOf( | ||
| intArrayOf(android.R.attr.state_checked), | ||
| intArrayOf(-android.R.attr.state_checked) | ||
| ) | ||
| val colors = intArrayOf( | ||
| ContextCompat.getColor(context, R.color.testpress_green), | ||
| ContextCompat.getColor(context, android.R.color.darker_gray) | ||
| ) | ||
| this.buttonTintList = ColorStateList(states, colors) | ||
| } | ||
|
|
||
| checkBox.setOnCheckedChangeListener { _, _ -> | ||
| val hasSelection = answerViews.any { view -> | ||
| val frame = view.findViewById<FrameLayout>(R.id.choice_container) | ||
| val cb = frame?.getChildAt(0) as? CheckBox | ||
| cb?.isChecked == true | ||
| } | ||
| actionButton?.isEnabled = hasSelection | ||
| } | ||
|
|
||
| choiceContainer.addView(checkBox) | ||
| optionsContainer.addView(optionView) | ||
| answerViews.add(optionView) | ||
| } | ||
| } | ||
| "G" -> { | ||
| val flexboxLayout = FlexboxLayout(context) | ||
| flexboxLayout.flexWrap = FlexWrap.WRAP | ||
| flexboxLayout.alignItems = AlignItems.CENTER | ||
|
|
||
| val html = question.questionHtml | ||
| val cleanText = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY).toString().trim() | ||
|
|
||
| val pattern = Pattern.compile("\\[(.*?)\\]") | ||
| val matcher = pattern.matcher(cleanText) | ||
|
|
||
| val gapFillEditTexts = mutableListOf<EditText>() | ||
| var lastEnd = 0 | ||
|
|
||
| while (matcher.find()) { | ||
| val textBefore = cleanText.substring(lastEnd, matcher.start()) | ||
| if (textBefore.isNotEmpty()) { | ||
| val textView = TextView(context) | ||
| textView.text = textBefore | ||
| textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.testpress_text_size_large)) | ||
| textView.setTextColor(ContextCompat.getColor(context, R.color.testpress_black)) | ||
| flexboxLayout.addView(textView) | ||
| } | ||
|
|
||
| val editText = EditText(context).apply { | ||
| minEms = 4 | ||
| inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | ||
| setTextColor(ContextCompat.getColor(context, R.color.testpress_table_text)) | ||
| background = ContextCompat.getDrawable(context, R.drawable.testpress_gray_border) | ||
| val padding = resources.getDimensionPixelSize(R.dimen.testpress_horizontal_margin) | ||
| setPadding(padding, padding, padding, padding) | ||
| } | ||
|
|
||
| gapFillEditTexts.add(editText) | ||
| editText.addTextChangedListener(object : TextWatcher { | ||
| override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} | ||
| override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} | ||
| override fun afterTextChanged(s: Editable?) { | ||
| val allFilled = gapFillEditTexts.all { it.text.toString().trim().isNotEmpty() } | ||
| actionButton?.isEnabled = allFilled | ||
| } | ||
| }) | ||
|
|
||
| flexboxLayout.addView(editText) | ||
| answerViews.add(editText) | ||
| lastEnd = matcher.end() | ||
| } | ||
|
|
||
| val textAfter = cleanText.substring(lastEnd) | ||
| if (textAfter.isNotEmpty()) { | ||
| val textView = TextView(context) | ||
| textView.text = textAfter | ||
| textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.testpress_text_size_large)) | ||
| textView.setTextColor(ContextCompat.getColor(context, R.color.testpress_black)) | ||
| flexboxLayout.addView(textView) | ||
| } | ||
| questionContainer.addView(flexboxLayout) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun checkAnswers(): Boolean { | ||
| selectedAnswerIds.clear() | ||
| gapFillResults.clear() | ||
|
|
||
| val correctAnswers = question.answers | ||
| .filter { it.isCorrect } | ||
| .map { it.id } | ||
| .toSet() | ||
|
|
||
| when (question.type) { | ||
| "R" -> { | ||
| val checkedId = radioGroup?.checkedRadioButtonId ?: -1 | ||
| if (checkedId != -1) { | ||
| val selectedRadioButton = radioGroup?.findViewById<RadioButton>(checkedId) | ||
| if (selectedRadioButton != null) { | ||
| val selectedAnswer = selectedRadioButton.tag as DummyAnswer | ||
| selectedAnswerIds.add(selectedAnswer.id) | ||
| } | ||
| } | ||
| } | ||
| "C" -> { | ||
| answerViews.forEach { view -> | ||
| val choiceContainer = view.findViewById<FrameLayout>(R.id.choice_container) | ||
| val checkBox = choiceContainer?.getChildAt(0) as? CheckBox | ||
| if (checkBox != null && checkBox.isChecked) { | ||
| val selectedAnswer = checkBox.tag as DummyAnswer | ||
| selectedAnswerIds.add(selectedAnswer.id) | ||
| } | ||
| } | ||
| } | ||
| "G" -> { | ||
| val key = question.answers.map { it.textHtml.trim() } | ||
| val userAnswers = answerViews.filterIsInstance<EditText>().map { it.text.toString().trim() } | ||
|
|
||
| var allCorrect = true | ||
| for (i in key.indices) { | ||
| val isBoxCorrect = key[i].equals(userAnswers.getOrNull(i), ignoreCase = true) | ||
| gapFillResults.add(isBoxCorrect) | ||
| if (!isBoxCorrect) { | ||
| allCorrect = false | ||
| } | ||
| } | ||
| return allCorrect | ||
| } | ||
| } | ||
| return correctAnswers.isNotEmpty() && correctAnswers == selectedAnswerIds.toSet() | ||
| } | ||
|
|
||
| private fun disableOptions() { | ||
| answerViews.forEach { view -> | ||
| val choiceContainer = view.findViewById<FrameLayout>(R.id.choice_container) | ||
| if (choiceContainer != null) { | ||
| val child = choiceContainer.getChildAt(0) | ||
| when (child) { | ||
| is RadioButton -> child.isEnabled = false | ||
| is CheckBox -> child.isEnabled = false | ||
| } | ||
| } | ||
| if (view is EditText) { | ||
| view.isEnabled = false | ||
| } | ||
| view.findViewById<LinearLayout>(R.id.option_root)?.isEnabled = false | ||
| } | ||
| radioGroup?.isEnabled = false | ||
| } | ||
|
|
||
| private fun showFeedback(isCorrect: Boolean) { | ||
| val context = context ?: return | ||
|
|
||
| when (question.type) { | ||
| "R", "C" -> { | ||
| feedbackText?.visibility = View.GONE | ||
|
|
||
| answerViews.forEach { view -> | ||
| val optionRoot = view.findViewById<LinearLayout>(R.id.option_root) | ||
| val icon = view.findViewById<ImageView>(R.id.quiz_icon) | ||
| val choiceContainer = view.findViewById<FrameLayout>(R.id.choice_container) | ||
| val button = choiceContainer?.getChildAt(0) as? View | ||
|
|
||
| if (button == null || optionRoot == null) return@forEach | ||
|
|
||
| val answer = button.tag as? DummyAnswer ?: return@forEach | ||
|
|
||
| val isSelected = when (button) { | ||
| is RadioButton -> button.isChecked | ||
| is CheckBox -> button.isChecked | ||
| else -> selectedAnswerIds.contains(answer.id) | ||
| } | ||
|
|
||
| if (answer.isCorrect) { | ||
| // Show ALL correct answers in green (whether selected or not) | ||
| optionRoot.setBackgroundResource(R.drawable.quiz_border_correct) | ||
| icon.setImageResource(R.drawable.ic_baseline_done_24) | ||
| icon.setColorFilter(ContextCompat.getColor(context, R.color.testpress_green)) | ||
| icon.visibility = View.VISIBLE | ||
| } | ||
| // Show wrong answers that were selected in red | ||
| else if (isSelected) { | ||
| optionRoot.setBackgroundResource(R.drawable.quiz_border_incorrect) | ||
| icon.setImageResource(R.drawable.ic_baseline_error_24) | ||
| icon.setColorFilter(ContextCompat.getColor(context, R.color.testpress_red_incorrect)) | ||
| icon.visibility = View.VISIBLE | ||
| } | ||
| // Wrong answers that were NOT selected remain gray/default (no change needed) | ||
| } | ||
| } | ||
| "G" -> { | ||
| val editTexts = answerViews.filterIsInstance<EditText>() | ||
| editTexts.forEachIndexed { index, editText -> | ||
| val isBoxCorrect = gapFillResults.getOrNull(index) ?: false | ||
|
|
||
| if (isBoxCorrect) { | ||
| editText.setBackgroundResource(R.drawable.quiz_border_correct) | ||
| } else { | ||
| editText.setBackgroundResource(R.drawable.quiz_border_incorrect) | ||
| } | ||
| } | ||
|
|
||
| if (!isCorrect) { | ||
| feedbackText?.visibility = View.VISIBLE | ||
| val correctAnswers = question.answers.joinToString(", ") { it.textHtml } | ||
| feedbackText?.text = getString(R.string.quiz_correct_answer, correctAnswers) | ||
| feedbackText?.setTextColor(ContextCompat.getColor(context, R.color.testpress_red_incorrect)) | ||
| } else { | ||
| feedbackText?.visibility = View.GONE // Hide feedback text if correct | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
This fragment is over 450 lines long and handles many responsibilities: building UI for different question types, managing state, checking answers, and showing feedback. This violates the Single Responsibility Principle and makes the class difficult to maintain, test, and reason about. Consider refactoring this by introducing a ViewModel to manage state and business logic. The UI building logic for each question type could also be extracted into smaller, more focused helper functions or classes.
| showMockQuestion(mockQuestionIndex) | ||
| } | ||
| // Trigger after 5 seconds (5000ms) | ||
| quizTriggerHandler?.postDelayed(quizTriggerRunnable!!, 2000) |
There was a problem hiding this comment.
The non-null assertion operator (!!) is used here. While it might be safe in the current context, it's a code smell and can lead to NullPointerExceptions if the code is refactored. It's safer to use a safe call (?.let).
| quizTriggerHandler?.postDelayed(quizTriggerRunnable!!, 2000) | |
| quizTriggerRunnable?.let { quizTriggerHandler?.postDelayed(it, 2000) } |
| data class DummyQuestion( | ||
| val id: Long, | ||
| val questionHtml: String, | ||
| val type: String, | ||
| val answers: List<DummyAnswer> | ||
| ) | ||
| data class DummyAnswer( | ||
| val id: Long, | ||
| val textHtml: String, | ||
| val isCorrect: Boolean | ||
| ) |
There was a problem hiding this comment.
The data classes DummyQuestion and DummyAnswer are defined inside the VideoQuizSheetFragment.kt file. According to Kotlin coding conventions, classes should be declared in their own files to improve code organization, readability, and reusability. Please move these data classes to their own files, DummyQuestion.kt and DummyAnswer.kt.
| override fun onAttach(context: Context) { | ||
| super.onAttach(context) | ||
| if (parentFragment is OnQuizCompleteListener) { | ||
| listener = parentFragment as OnQuizCompleteListener | ||
| } else { | ||
| // When using activity's fragment manager (fullscreen), parentFragment may be null | ||
| activity?.supportFragmentManager?.fragments?.forEach { fragment -> | ||
| if (fragment is OnQuizCompleteListener) { | ||
| listener = fragment as OnQuizCompleteListener | ||
| return@forEach | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The onAttach method includes a fallback to find the OnQuizCompleteListener by iterating through all fragments in the activity?.supportFragmentManager. This approach is not robust and can be error-prone. If multiple fragments in the back stack implement this interface, it might attach to the wrong one, leading to unpredictable behavior. Consider using a more reliable communication pattern, such as a shared ViewModel between the fragments, or having the hosting Activity implement the interface and act as a delegate.
| actionButton?.setOnClickListener { | ||
| if (actionButton?.text.toString().equals(getString(R.string.quiz_button_check), ignoreCase = true)) { | ||
| isCorrect = checkAnswers() | ||
| showFeedback(isCorrect) | ||
| disableOptions() | ||
| actionButton?.text = getString(R.string.quiz_button_continue) | ||
| } else { | ||
| listener?.onQuizCompleted(question.id) | ||
| dismiss() | ||
| } | ||
| } |
There was a problem hiding this comment.
The logic to decide the action button's behavior relies on comparing its current text with a string resource. This is a brittle pattern because if the string value changes (e.g., for localization), the logic will break. It's better to use a dedicated state variable (e.g., private var isAnswerSubmitted = false) to track whether the answer has been submitted. You would set it to true after checking the answer and check its value in the onClickListener.
| question: DummyQuestion | ||
| ) { | ||
| val context = inflater.context | ||
| when (question.type) { |
There was a problem hiding this comment.
Magic strings like "R", "C", and "G" are used throughout the fragment to identify question types. This makes the code harder to read and maintain. If a type identifier changes, it needs to be updated in multiple places, which is error-prone. Please define these as const val in a companion object or use an enum for type safety and better readability.