Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 Google LLC
* Copyright 2022-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,11 +21,15 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse

/** Various types of rows that can be used in a Questionnaire RecyclerView. */
internal sealed interface QuestionnaireAdapterItem {

/** A row for a question in a Questionnaire RecyclerView. */
data class Question(val item: QuestionnaireViewItem) : QuestionnaireAdapterItem
data class Question(val item: QuestionnaireViewItem) : QuestionnaireAdapterItem {
var id: String? = item.questionnaireItem.linkId
}

/** A row for a repeated group response instance's header. */
data class RepeatedGroupHeader(
val id: String,
/** The response index. This is 0-indexed, but should be 1-indexed when rendered in the UI. */
val index: Int,
/** Callback that is invoked when the user clicks the delete button. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,19 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.res.use
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
Expand All @@ -33,9 +43,11 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.extensions.inflate
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.ReviewViewHolderFactory
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.Questionnaire
Expand Down Expand Up @@ -93,8 +105,8 @@ class QuestionnaireFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val questionnaireEditRecyclerView =
view.findViewById<RecyclerView>(R.id.questionnaire_edit_recycler_view)
val questionnaireReviewRecyclerView =
view.findViewById<RecyclerView>(R.id.questionnaire_review_recycler_view)
val questionnaireReviewComposeView =
view.findViewById<ComposeView>(R.id.questionnaire_review_recycler_view)
val questionnaireTitle = view.findViewById<TextView>(R.id.questionnaire_title)

// This container frame floats at the bottom of the view to make navigation controls visible at
Expand Down Expand Up @@ -139,7 +151,6 @@ class QuestionnaireFragment : Fragment() {
view.findViewById(R.id.questionnaire_progress_indicator)
val questionnaireEditAdapter =
QuestionnaireEditAdapter(questionnaireItemViewHolderFactoryMatchersProvider.get())
val questionnaireReviewAdapter = QuestionnaireReviewAdapter()

val reviewModeEditButton =
view.findViewById<View>(R.id.review_mode_edit_button).apply {
Expand All @@ -152,8 +163,11 @@ class QuestionnaireFragment : Fragment() {
// Animation does work well with views that could gain focus
questionnaireEditRecyclerView.itemAnimator = null

questionnaireReviewRecyclerView.adapter = questionnaireReviewAdapter
questionnaireReviewRecyclerView.layoutManager = LinearLayoutManager(view.context)
questionnaireReviewComposeView.setContent {
val questionerStateFlow = viewModel.questionnaireStateFlow.collectAsState()

QuestionnaireReviewList(questionerStateFlow)
}

// Listen to updates from the view model.
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
Expand All @@ -162,10 +176,7 @@ class QuestionnaireFragment : Fragment() {
is DisplayMode.ReviewMode -> {
// Set items
questionnaireEditRecyclerView.visibility = View.GONE
questionnaireReviewAdapter.submitList(
state.items,
)
questionnaireReviewRecyclerView.visibility = View.VISIBLE
questionnaireReviewComposeView.visibility = View.VISIBLE
reviewModeEditButton.visibility =
if (displayMode.showEditButton) {
View.VISIBLE
Expand All @@ -189,7 +200,7 @@ class QuestionnaireFragment : Fragment() {
}
is DisplayMode.EditMode -> {
// Set items
questionnaireReviewRecyclerView.visibility = View.GONE
questionnaireReviewComposeView.visibility = View.GONE
questionnaireEditAdapter.submitList(state.items)
questionnaireEditRecyclerView.visibility = View.VISIBLE
reviewModeEditButton.visibility = View.GONE
Expand Down Expand Up @@ -234,7 +245,7 @@ class QuestionnaireFragment : Fragment() {
}
}
is DisplayMode.InitMode -> {
questionnaireReviewRecyclerView.visibility = View.GONE
questionnaireReviewComposeView.visibility = View.GONE
questionnaireEditRecyclerView.visibility = View.GONE
questionnaireProgressIndicator.visibility = View.GONE
reviewModeEditButton.visibility = View.GONE
Expand Down Expand Up @@ -283,6 +294,48 @@ class QuestionnaireFragment : Fragment() {
}
}

@Composable
private fun QuestionnaireReviewList(questionerStateFlow: State<QuestionnaireState>) {
LazyColumn {
items(
questionerStateFlow.value.items,
key = { item ->
when (item) {
is QuestionnaireAdapterItem.Question -> item.id
?: throw IllegalStateException("Missing id for the QuestionnaireAdapterItem: $item")
is QuestionnaireAdapterItem.RepeatedGroupHeader -> item.id
is QuestionnaireAdapterItem.Navigation -> "navigation"
}
},
) { item: QuestionnaireAdapterItem ->
AndroidView(
factory = { context ->
LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
when (item) {
is QuestionnaireAdapterItem.Question -> {
val viewHolder = ReviewViewHolderFactory.create(this)
viewHolder.bind(item.item)
addView(viewHolder.itemView)
}
is QuestionnaireAdapterItem.Navigation -> {
val viewHolder =
NavigationViewHolder(inflate(R.layout.pagination_navigation_view))
viewHolder.bind(item.questionnaireNavigationUIState)
addView(viewHolder.itemView)
}
is QuestionnaireAdapterItem.RepeatedGroupHeader -> {
TODO("Not implemented yet")
}
}
}
},
modifier = Modifier.fillMaxWidth(),
)
}
}
}

/** Calculates the progress percentage from given [count] and [totalCount] values. */
internal fun calculateProgressPercentage(count: Int, totalCount: Int): Int {
return if (totalCount == 0) 0 else (count * 100 / totalCount)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
// Case 3
add(
QuestionnaireAdapterItem.RepeatedGroupHeader(
id = "${index}_${question.item.questionnaireItem.linkId}",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so you may have repeated groups nested under repeated groups....

so the safest thing to do here, is actually generate an id that is a concatenation of all ancesters of this code, plus their indices (if there're repeated items) - that's a bit complex.

repeated group within another repeated group.

please test have a think about this. there might be an easier way to do this - you may have a way of assigning shorter ids to each node in this tree but you'll have to remember it...

index = index,
onDeleteClicked = { viewModelScope.launch { question.item.removeAnswerAt(index) } },
responses = nestedResponseItemList,
Expand All @@ -1017,11 +1018,21 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
addAll(
getQuestionnaireAdapterItems(
// If nested display item is identified as instructions or flyover, then do not create
// questionnaire state for it.
questionnaireItemList = questionnaireItem.item.filterNot { it.isDisplayItem },
questionnaireResponseItemList = nestedResponseItemList,
),
// If nested display item is identified as instructions or flyover, then do not
// create
// questionnaire state for it.
questionnaireItemList = questionnaireItem.item.filterNot { it.isDisplayItem },
questionnaireResponseItemList = nestedResponseItemList,
)
.onEach {
// Reset the question id to avoid duplicate keys in LazyColumn composable. The new
// id is derived from the the repeated group index, the parent question
// questionnaire item linkId and the linkId of the nested questions
if (it is QuestionnaireAdapterItem.Question) {
it.id =
"${index}_${question.item.questionnaireItem.linkId}_${it.item.questionnaireItem.linkId}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue here. make sure uniqueness is guaranteed with nested repeated groups.

}
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
app:layout_constraintTop_toBottomOf="@+id/questionnaire_progress_indicator"
/>

<androidx.recyclerview.widget.RecyclerView
<androidx.compose.ui.platform.ComposeView
android:id="@+id/questionnaire_review_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
Expand Down

This file was deleted.

Loading