Skip to content

Commit 49d37c5

Browse files
authored
Migrate QuestionnaireEditRecyclerview to compose (#2884)
* Migrate QuestionnaireEditRecyclerview to compose Signed-off-by: Elly Kitoto <[email protected]> * Fix formatting Signed-off-by: Elly Kitoto <[email protected]> * Fix questionnaire edit view rendering in lazycolumn - Fixed duplicate ids when using repeated groups - Fixed issues with progress indicator upon scroll - Fixed issues with showing validation errors - Fixed issues with state management on individual lazy column rows that renders form widgets. Tag the created views with the viewholder, and call viewholder bind method to update view content. Signed-off-by: Elly Kitoto <[email protected]> * Throw exception if questionnaire field linkId is not provided Every field in a questionnaire is expected to have a unique linkId. Signed-off-by: Elly Kitoto <[email protected]> * Update tests Signed-off-by: Elly Kitoto <[email protected]> * Run spotlessApply Signed-off-by: Elly Kitoto <[email protected]> * Add TODO to remove viewholder tagging View holder tagging on on composable views will no longer be necessary once all the views are migrated to compose. Signed-off-by: Elly Kitoto <[email protected]> * Fix rendering of nested repeated groups within repeated groups In order to prevent LazyColumn id name collisions when repeated groups are nested arbitrarily deep, the questionnaire ID is generated from the string derived from tracking all the accumulated paths from all ancestor repeated groups through recursive calls. Signed-off-by: Elly Kitoto <[email protected]> * Resolve unnessary recompositions in QuestionnaireEditList lazy column Commented code that invokes the handelInput function that triggers state change causing multiple recomposition. Added the onRest callback to the LazyColumn's AndroidView to reset the view before the update function is called during recomposition. Signed-off-by: Elly Kitoto <[email protected]> * Fix code formatting issues Signed-off-by: Elly Kitoto <[email protected]> * Run spotless Signed-off-by: Elly Kitoto <[email protected]> * Clean up code Signed-off-by: Elly Kitoto <[email protected]> --------- Signed-off-by: Elly Kitoto <[email protected]>
1 parent 07a3aa3 commit 49d37c5

File tree

10 files changed

+675
-1779
lines changed

10 files changed

+675
-1779
lines changed

datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactoryInstrumentedTest.kt

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import androidx.compose.ui.test.performTextReplacement
3131
import androidx.test.ext.junit.rules.ActivityScenarioRule
3232
import androidx.test.ext.junit.runners.AndroidJUnit4
3333
import androidx.test.platform.app.InstrumentationRegistry
34-
import com.google.android.fhir.datacapture.QuestionnaireEditAdapter
35-
import com.google.android.fhir.datacapture.QuestionnaireViewHolderType
3634
import com.google.android.fhir.datacapture.R
3735
import com.google.android.fhir.datacapture.test.TestActivity
3836
import com.google.android.fhir.datacapture.validation.Invalid
@@ -83,25 +81,6 @@ class PhoneNumberViewHolderFactoryInstrumentedTest {
8381
composeTestRule.unregisterIdlingResource(handlingTextIdlingResource)
8482
}
8583

86-
@Test
87-
fun createViewHolder_phoneNumberViewHolderFactory_returnsViewHolder() {
88-
val questionnaireEditAdapter = QuestionnaireEditAdapter()
89-
val viewHolderFromAdapter =
90-
questionnaireEditAdapter.createViewHolder(
91-
parent,
92-
QuestionnaireEditAdapter.ViewType.from(
93-
type = QuestionnaireEditAdapter.ViewType.Type.QUESTION,
94-
subtype = QuestionnaireViewHolderType.PHONE_NUMBER.value,
95-
)
96-
.viewType,
97-
) as QuestionnaireEditAdapter.ViewHolder.QuestionHolder
98-
99-
assertThat(
100-
viewHolderFromAdapter.holder.itemView.visibility,
101-
)
102-
.isEqualTo(View.VISIBLE)
103-
}
104-
10584
@Test
10685
fun shouldSetTextViewText() {
10786
viewHolder.bind(

datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/QuestionnaireUiEspressoTest.kt

Lines changed: 119 additions & 203 deletions
Large diffs are not rendered by default.

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

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

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

Lines changed: 86 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,26 @@ import android.os.Bundle
2121
import android.view.LayoutInflater
2222
import android.view.View
2323
import android.view.ViewGroup
24-
import android.widget.LinearLayout
2524
import android.widget.TextView
2625
import androidx.annotation.VisibleForTesting
2726
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
3327
import androidx.compose.ui.platform.ComposeView
34-
import androidx.compose.ui.viewinterop.AndroidView
3528
import androidx.core.content.res.use
3629
import androidx.core.os.bundleOf
3730
import androidx.fragment.app.Fragment
3831
import androidx.fragment.app.activityViewModels
3932
import androidx.fragment.app.setFragmentResult
4033
import androidx.fragment.app.viewModels
34+
import androidx.lifecycle.Lifecycle
4135
import androidx.lifecycle.lifecycleScope
42-
import androidx.recyclerview.widget.LinearLayoutManager
43-
import androidx.recyclerview.widget.RecyclerView
44-
import com.google.android.fhir.datacapture.extensions.inflate
36+
import androidx.lifecycle.repeatOnLifecycle
37+
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING
38+
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_URI
39+
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING
40+
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_URI
4541
import com.google.android.fhir.datacapture.validation.Invalid
4642
import com.google.android.fhir.datacapture.views.NavigationViewHolder
4743
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory
48-
import com.google.android.fhir.datacapture.views.factories.ReviewViewHolderFactory
4944
import com.google.android.material.progressindicator.LinearProgressIndicator
5045
import kotlinx.coroutines.launch
5146
import org.hl7.fhir.r4.model.Questionnaire
@@ -101,8 +96,8 @@ class QuestionnaireFragment : Fragment() {
10196

10297
/** @suppress */
10398
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
104-
val questionnaireEditRecyclerView =
105-
view.findViewById<RecyclerView>(R.id.questionnaire_edit_recycler_view)
99+
val questionnaireEditComposeView =
100+
view.findViewById<ComposeView>(R.id.questionnaire_edit_compose_view)
106101
val questionnaireReviewComposeView =
107102
view.findViewById<ComposeView>(R.id.questionnaire_review_recycler_view)
108103
val questionnaireTitle = view.findViewById<TextView>(R.id.questionnaire_title)
@@ -147,103 +142,97 @@ class QuestionnaireFragment : Fragment() {
147142
}
148143
val questionnaireProgressIndicator: LinearProgressIndicator =
149144
view.findViewById(R.id.questionnaire_progress_indicator)
150-
val questionnaireEditAdapter =
151-
QuestionnaireEditAdapter(questionnaireItemViewHolderFactoryMatchersProvider.get())
152145

153146
val reviewModeEditButton =
154147
view.findViewById<View>(R.id.review_mode_edit_button).apply {
155148
setOnClickListener { viewModel.setReviewMode(false) }
156149
}
157150

158-
questionnaireEditRecyclerView.adapter = questionnaireEditAdapter
159-
val linearLayoutManager = LinearLayoutManager(view.context)
160-
questionnaireEditRecyclerView.layoutManager = linearLayoutManager
161-
// Animation does work well with views that could gain focus
162-
questionnaireEditRecyclerView.itemAnimator = null
163-
164151
// Listen to updates from the view model.
165-
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
166-
viewModel.questionnaireStateFlow.collect { state ->
167-
when (val displayMode = state.displayMode) {
168-
is DisplayMode.ReviewMode -> {
169-
// Set items
170-
questionnaireEditRecyclerView.visibility = View.GONE
171-
172-
questionnaireReviewComposeView.visibility = View.VISIBLE
173-
questionnaireReviewComposeView.setContent { QuestionnaireReviewList(state.items) }
174-
reviewModeEditButton.visibility =
175-
if (displayMode.showEditButton) {
176-
View.VISIBLE
152+
viewLifecycleOwner.lifecycleScope.launch {
153+
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
154+
viewModel.questionnaireStateFlow.collect { state ->
155+
when (val displayMode = state.displayMode) {
156+
is DisplayMode.ReviewMode -> {
157+
// Set items
158+
159+
questionnaireReviewComposeView.visibility = View.VISIBLE
160+
questionnaireReviewComposeView.setContent { QuestionnaireReviewList(state.items) }
161+
questionnaireEditComposeView.visibility = View.GONE
162+
163+
reviewModeEditButton.visibility =
164+
if (displayMode.showEditButton) {
165+
View.VISIBLE
166+
} else {
167+
View.GONE
168+
}
169+
questionnaireTitle.visibility = View.VISIBLE
170+
questionnaireTitle.text = getString(R.string.questionnaire_review_mode_title)
171+
172+
// Set bottom navigation
173+
if (state.bottomNavItem != null) {
174+
bottomNavContainerFrame.visibility = View.VISIBLE
175+
NavigationViewHolder(bottomNavContainerFrame)
176+
.bind(state.bottomNavItem.questionnaireNavigationUIState)
177177
} else {
178-
View.GONE
178+
bottomNavContainerFrame.visibility = View.GONE
179179
}
180-
questionnaireTitle.visibility = View.VISIBLE
181-
questionnaireTitle.text = getString(R.string.questionnaire_review_mode_title)
182-
183-
// Set bottom navigation
184-
if (state.bottomNavItem != null) {
185-
bottomNavContainerFrame.visibility = View.VISIBLE
186-
NavigationViewHolder(bottomNavContainerFrame)
187-
.bind(state.bottomNavItem.questionnaireNavigationUIState)
188-
} else {
189-
bottomNavContainerFrame.visibility = View.GONE
190-
}
191180

192-
// Hide progress indicator
193-
questionnaireProgressIndicator.visibility = View.GONE
194-
}
195-
is DisplayMode.EditMode -> {
196-
// Set items
197-
questionnaireReviewComposeView.visibility = View.GONE
198-
questionnaireEditAdapter.submitList(state.items)
199-
questionnaireEditRecyclerView.visibility = View.VISIBLE
200-
reviewModeEditButton.visibility = View.GONE
201-
questionnaireTitle.visibility = View.GONE
202-
203-
// Set bottom navigation
204-
if (state.bottomNavItem != null) {
205-
bottomNavContainerFrame.visibility = View.VISIBLE
206-
NavigationViewHolder(bottomNavContainerFrame)
207-
.bind(state.bottomNavItem.questionnaireNavigationUIState)
208-
} else {
209-
bottomNavContainerFrame.visibility = View.GONE
181+
// Hide progress indicator
182+
questionnaireProgressIndicator.visibility = View.GONE
210183
}
211-
212-
// Set progress indicator
213-
questionnaireProgressIndicator.visibility = View.VISIBLE
214-
if (displayMode.pagination.isPaginated) {
215-
questionnaireProgressIndicator.updateProgressIndicator(
216-
calculateProgressPercentage(
217-
count =
218-
(displayMode.pagination.currentPageIndex +
219-
1), // incremented by 1 due to initialPageIndex starts with 0.
220-
totalCount = displayMode.pagination.pages.size,
221-
),
222-
)
223-
} else {
224-
questionnaireEditRecyclerView.addOnScrollListener(
225-
object : RecyclerView.OnScrollListener() {
226-
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
227-
super.onScrolled(recyclerView, dx, dy)
184+
is DisplayMode.EditMode -> {
185+
// Set items
186+
questionnaireReviewComposeView.visibility = View.GONE
187+
questionnaireEditComposeView.setContent {
188+
QuestionnaireEditList(
189+
items = state.items,
190+
displayMode = displayMode,
191+
questionnaireItemViewHolderMatchers =
192+
questionnaireItemViewHolderFactoryMatchersProvider.get(),
193+
onUpdateProgressIndicator = { currentPage, totalCount ->
228194
questionnaireProgressIndicator.updateProgressIndicator(
229195
calculateProgressPercentage(
230-
count =
231-
(linearLayoutManager.findLastVisibleItemPosition() +
232-
1), // incremented by 1 due to findLastVisiblePosition() starts with 0.
233-
totalCount = linearLayoutManager.itemCount,
196+
count = (currentPage + 1),
197+
totalCount = totalCount,
234198
),
235199
)
236-
}
237-
},
238-
)
200+
},
201+
)
202+
}
203+
questionnaireEditComposeView.visibility = View.VISIBLE
204+
reviewModeEditButton.visibility = View.GONE
205+
questionnaireTitle.visibility = View.GONE
206+
207+
// Set bottom navigation
208+
if (state.bottomNavItem != null) {
209+
bottomNavContainerFrame.visibility = View.VISIBLE
210+
NavigationViewHolder(bottomNavContainerFrame)
211+
.bind(state.bottomNavItem.questionnaireNavigationUIState)
212+
} else {
213+
bottomNavContainerFrame.visibility = View.GONE
214+
}
215+
216+
// Set progress indicator
217+
questionnaireProgressIndicator.visibility = View.VISIBLE
218+
if (displayMode.pagination.isPaginated) {
219+
questionnaireProgressIndicator.updateProgressIndicator(
220+
calculateProgressPercentage(
221+
count =
222+
(displayMode.pagination.currentPageIndex +
223+
1), // incremented by 1 due to initialPageIndex starts with 0.
224+
totalCount = displayMode.pagination.pages.size,
225+
),
226+
)
227+
}
228+
}
229+
is DisplayMode.InitMode -> {
230+
questionnaireReviewComposeView.visibility = View.GONE
231+
questionnaireEditComposeView.visibility = View.GONE
232+
questionnaireProgressIndicator.visibility = View.GONE
233+
reviewModeEditButton.visibility = View.GONE
234+
bottomNavContainerFrame.visibility = View.GONE
239235
}
240-
}
241-
is DisplayMode.InitMode -> {
242-
questionnaireReviewComposeView.visibility = View.GONE
243-
questionnaireEditRecyclerView.visibility = View.GONE
244-
questionnaireProgressIndicator.visibility = View.GONE
245-
reviewModeEditButton.visibility = View.GONE
246-
bottomNavContainerFrame.visibility = View.GONE
247236
}
248237
}
249238
}
@@ -288,53 +277,6 @@ class QuestionnaireFragment : Fragment() {
288277
}
289278
}
290279

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-
338280
/** Calculates the progress percentage from given [count] and [totalCount] values. */
339281
internal fun calculateProgressPercentage(count: Int, totalCount: Int): Int {
340282
return if (totalCount == 0) 0 else (count * 100 / totalCount)
@@ -589,6 +531,9 @@ class QuestionnaireFragment : Fragment() {
589531
*/
590532
internal const val EXTRA_SHOW_SUBMIT_ANYWAY_BUTTON = "show-submit-anyway-button"
591533

534+
/** Test tag for QuestionnaireEditList */
535+
const val QUESTIONNAIRE_EDIT_LIST = "questionnaire_edit_list"
536+
592537
fun builder() = Builder()
593538
}
594539

0 commit comments

Comments
 (0)