Skip to content

Commit 07a3aa3

Browse files
authored
Migrate QuestionnaireReviewRecyclerView to compose (#2881)
* Refactor QuestionnaireReviewRecyclerView to LazyColumn composable Signed-off-by: Elly Kitoto <[email protected]> * Delete unused code Signed-off-by: Elly Kitoto <[email protected]> * Refactor how key provision for questionnaire review lazycolumn Signed-off-by: Elly Kitoto <[email protected]> * Run spotless apply Signed-off-by: Elly Kitoto <[email protected]> --------- Signed-off-by: Elly Kitoto <[email protected]>
1 parent e0e08d8 commit 07a3aa3

File tree

6 files changed

+92
-178
lines changed

6 files changed

+92
-178
lines changed

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
2323
internal sealed interface QuestionnaireAdapterItem {
2424
/** A row for a question in a Questionnaire RecyclerView. */
2525
data class Question(val item: QuestionnaireViewItem) :
26-
QuestionnaireAdapterItem, ReviewAdapterItem
26+
QuestionnaireAdapterItem, ReviewAdapterItem {
27+
var id: String? = item.questionnaireItem.linkId
28+
}
2729

2830
/** A row for a repeated group response instance's header. */
2931
data class RepeatedGroupHeader(
32+
val id: String,
3033
/** The response index. This is 0-indexed, but should be 1-indexed when rendered in the UI. */
3134
val index: Int,
3235
/** Callback that is invoked when the user clicks the delete button. */
@@ -37,6 +40,7 @@ internal sealed interface QuestionnaireAdapterItem {
3740
) : QuestionnaireAdapterItem
3841

3942
data class RepeatedGroupAddButton(
43+
var id: String?,
4044
val item: QuestionnaireViewItem,
4145
) : QuestionnaireAdapterItem
4246

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,17 @@ import android.os.Bundle
2121
import android.view.LayoutInflater
2222
import android.view.View
2323
import android.view.ViewGroup
24+
import android.widget.LinearLayout
2425
import android.widget.TextView
2526
import androidx.annotation.VisibleForTesting
2627
import androidx.appcompat.view.ContextThemeWrapper
28+
import androidx.compose.foundation.layout.fillMaxWidth
29+
import androidx.compose.foundation.lazy.LazyColumn
30+
import androidx.compose.foundation.lazy.items
31+
import androidx.compose.runtime.Composable
32+
import androidx.compose.ui.Modifier
33+
import androidx.compose.ui.platform.ComposeView
34+
import androidx.compose.ui.viewinterop.AndroidView
2735
import androidx.core.content.res.use
2836
import androidx.core.os.bundleOf
2937
import androidx.fragment.app.Fragment
@@ -33,9 +41,11 @@ import androidx.fragment.app.viewModels
3341
import androidx.lifecycle.lifecycleScope
3442
import androidx.recyclerview.widget.LinearLayoutManager
3543
import androidx.recyclerview.widget.RecyclerView
44+
import com.google.android.fhir.datacapture.extensions.inflate
3645
import com.google.android.fhir.datacapture.validation.Invalid
3746
import com.google.android.fhir.datacapture.views.NavigationViewHolder
3847
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory
48+
import com.google.android.fhir.datacapture.views.factories.ReviewViewHolderFactory
3949
import com.google.android.material.progressindicator.LinearProgressIndicator
4050
import kotlinx.coroutines.launch
4151
import org.hl7.fhir.r4.model.Questionnaire
@@ -93,8 +103,8 @@ class QuestionnaireFragment : Fragment() {
93103
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
94104
val questionnaireEditRecyclerView =
95105
view.findViewById<RecyclerView>(R.id.questionnaire_edit_recycler_view)
96-
val questionnaireReviewRecyclerView =
97-
view.findViewById<RecyclerView>(R.id.questionnaire_review_recycler_view)
106+
val questionnaireReviewComposeView =
107+
view.findViewById<ComposeView>(R.id.questionnaire_review_recycler_view)
98108
val questionnaireTitle = view.findViewById<TextView>(R.id.questionnaire_title)
99109

100110
// This container frame floats at the bottom of the view to make navigation controls visible at
@@ -139,7 +149,6 @@ class QuestionnaireFragment : Fragment() {
139149
view.findViewById(R.id.questionnaire_progress_indicator)
140150
val questionnaireEditAdapter =
141151
QuestionnaireEditAdapter(questionnaireItemViewHolderFactoryMatchersProvider.get())
142-
val questionnaireReviewAdapter = QuestionnaireReviewAdapter()
143152

144153
val reviewModeEditButton =
145154
view.findViewById<View>(R.id.review_mode_edit_button).apply {
@@ -152,20 +161,16 @@ class QuestionnaireFragment : Fragment() {
152161
// Animation does work well with views that could gain focus
153162
questionnaireEditRecyclerView.itemAnimator = null
154163

155-
questionnaireReviewRecyclerView.adapter = questionnaireReviewAdapter
156-
questionnaireReviewRecyclerView.layoutManager = LinearLayoutManager(view.context)
157-
158164
// Listen to updates from the view model.
159165
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
160166
viewModel.questionnaireStateFlow.collect { state ->
161167
when (val displayMode = state.displayMode) {
162168
is DisplayMode.ReviewMode -> {
163169
// Set items
164170
questionnaireEditRecyclerView.visibility = View.GONE
165-
questionnaireReviewAdapter.submitList(
166-
state.items.filterIsInstance<ReviewAdapterItem>(),
167-
)
168-
questionnaireReviewRecyclerView.visibility = View.VISIBLE
171+
172+
questionnaireReviewComposeView.visibility = View.VISIBLE
173+
questionnaireReviewComposeView.setContent { QuestionnaireReviewList(state.items) }
169174
reviewModeEditButton.visibility =
170175
if (displayMode.showEditButton) {
171176
View.VISIBLE
@@ -189,7 +194,7 @@ class QuestionnaireFragment : Fragment() {
189194
}
190195
is DisplayMode.EditMode -> {
191196
// Set items
192-
questionnaireReviewRecyclerView.visibility = View.GONE
197+
questionnaireReviewComposeView.visibility = View.GONE
193198
questionnaireEditAdapter.submitList(state.items)
194199
questionnaireEditRecyclerView.visibility = View.VISIBLE
195200
reviewModeEditButton.visibility = View.GONE
@@ -234,7 +239,7 @@ class QuestionnaireFragment : Fragment() {
234239
}
235240
}
236241
is DisplayMode.InitMode -> {
237-
questionnaireReviewRecyclerView.visibility = View.GONE
242+
questionnaireReviewComposeView.visibility = View.GONE
238243
questionnaireEditRecyclerView.visibility = View.GONE
239244
questionnaireProgressIndicator.visibility = View.GONE
240245
reviewModeEditButton.visibility = View.GONE
@@ -283,6 +288,53 @@ class QuestionnaireFragment : Fragment() {
283288
}
284289
}
285290

291+
@Composable
292+
private fun QuestionnaireReviewList(items: List<QuestionnaireAdapterItem>) {
293+
LazyColumn {
294+
items(
295+
items = items,
296+
key = { item ->
297+
when (item) {
298+
is QuestionnaireAdapterItem.Question -> item.id
299+
?: throw IllegalStateException("Missing id for the Question: $item")
300+
is QuestionnaireAdapterItem.RepeatedGroupHeader -> item.id
301+
is QuestionnaireAdapterItem.Navigation -> "navigation"
302+
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> item.id
303+
?: throw IllegalStateException("Missing id for the RepeatedGroupAddButton: $item")
304+
}
305+
},
306+
) { item: QuestionnaireAdapterItem ->
307+
AndroidView(
308+
factory = { context ->
309+
LinearLayout(context).apply {
310+
orientation = LinearLayout.VERTICAL
311+
when (item) {
312+
is QuestionnaireAdapterItem.Question -> {
313+
val viewHolder = ReviewViewHolderFactory.create(this)
314+
viewHolder.bind(item.item)
315+
addView(viewHolder.itemView)
316+
}
317+
is QuestionnaireAdapterItem.Navigation -> {
318+
val viewHolder =
319+
NavigationViewHolder(inflate(R.layout.pagination_navigation_view))
320+
viewHolder.bind(item.questionnaireNavigationUIState)
321+
addView(viewHolder.itemView)
322+
}
323+
is QuestionnaireAdapterItem.RepeatedGroupHeader -> {
324+
TODO("Not implemented yet")
325+
}
326+
is QuestionnaireAdapterItem.RepeatedGroupAddButton -> {
327+
TODO("Not implemented yet")
328+
}
329+
}
330+
}
331+
},
332+
modifier = Modifier.fillMaxWidth(),
333+
)
334+
}
335+
}
336+
}
337+
286338
/** Calculates the progress percentage from given [count] and [totalCount] values. */
287339
internal fun calculateProgressPercentage(count: Int, totalCount: Int): Int {
288340
return if (totalCount == 0) 0 else (count * 100 / totalCount)

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireReviewAdapter.kt

Lines changed: 0 additions & 68 deletions
This file was deleted.

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
10081008
// Case 3
10091009
add(
10101010
QuestionnaireAdapterItem.RepeatedGroupHeader(
1011+
id = "${index}_${question.item.questionnaireItem.linkId}",
10111012
index = index,
10121013
onDeleteClicked = { viewModelScope.launch { question.item.removeAnswerAt(index) } },
10131014
responses = nestedResponseItemList,
@@ -1017,16 +1018,31 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
10171018
}
10181019
addAll(
10191020
getQuestionnaireAdapterItems(
1020-
// If nested display item is identified as instructions or flyover, then do not create
1021-
// questionnaire state for it.
1022-
questionnaireItemList = questionnaireItem.item.filterNot { it.isDisplayItem },
1023-
questionnaireResponseItemList = nestedResponseItemList,
1024-
),
1021+
// If nested display item is identified as instructions or flyover, then do not
1022+
// create
1023+
// questionnaire state for it.
1024+
questionnaireItemList = questionnaireItem.item.filterNot { it.isDisplayItem },
1025+
questionnaireResponseItemList = nestedResponseItemList,
1026+
)
1027+
.onEach {
1028+
// Reset the question id to avoid duplicate keys in LazyColumn composable. The new
1029+
// id is derived from the the repeated group index, the parent question
1030+
// questionnaire item linkId and the linkId of the nested questions
1031+
if (it is QuestionnaireAdapterItem.Question) {
1032+
it.id =
1033+
"${index}_${question.item.questionnaireItem.linkId}_${it.item.questionnaireItem.linkId}"
1034+
}
1035+
},
10251036
)
10261037
}
10271038

10281039
if (questionnaireItem.isRepeatedGroup) {
1029-
add(QuestionnaireAdapterItem.RepeatedGroupAddButton(question.item))
1040+
add(
1041+
QuestionnaireAdapterItem.RepeatedGroupAddButton(
1042+
id = "${question.item.questionnaireItem.linkId}_add_btn",
1043+
item = question.item,
1044+
),
1045+
)
10301046
}
10311047
}
10321048
currentPageItems = items

datacapture/src/main/res/layout/questionnaire_fragment.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
app:layout_constraintTop_toBottomOf="@+id/questionnaire_progress_indicator"
7676
/>
7777

78-
<androidx.recyclerview.widget.RecyclerView
78+
<androidx.compose.ui.platform.ComposeView
7979
android:id="@+id/questionnaire_review_recycler_view"
8080
android:layout_width="match_parent"
8181
android:layout_height="0dp"

datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireReviewAdapterTest.kt

Lines changed: 0 additions & 90 deletions
This file was deleted.

0 commit comments

Comments
 (0)