Skip to content

Commit 1adc09e

Browse files
committed
Incorporated review feedback
- Combined functionality with the ExternalAnswerValueSetResolver - Renamed various functions and arguments for improved clarity - Moved filter handling out of the adapter and into the textChangedListener - Moved resolver to the viewmodel
1 parent 21a0cf0 commit 1adc09e

File tree

10 files changed

+113
-97
lines changed

10 files changed

+113
-97
lines changed

catalog/src/main/assets/component_auto_complete.json

+21
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,27 @@
9393
}
9494
}
9595
]
96+
},
97+
{
98+
"extension": [
99+
{
100+
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
101+
"valueCodeableConcept": {
102+
"coding": [
103+
{
104+
"system": "http://hl7.org/fhir/questionnaire-item-control",
105+
"code": "autocomplete"
106+
}
107+
]
108+
}
109+
}
110+
],
111+
"linkId": "2",
112+
"text": "Procedure",
113+
"type": "choice",
114+
"required": true,
115+
"repeats": false,
116+
"answerValueSet": "https://my.url/fhir/ValueSet/my-valueset"
96117
}
97118
]
98119
}

catalog/src/main/java/com/google/android/fhir/catalog/CatalogApplication.kt

+21-10
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ import ca.uhn.fhir.context.FhirContext
2121
import com.google.android.fhir.FhirEngine
2222
import com.google.android.fhir.FhirEngineConfiguration
2323
import com.google.android.fhir.FhirEngineProvider
24-
import com.google.android.fhir.datacapture.CustomCallback.AutoCompleteCallback
25-
import com.google.android.fhir.datacapture.CustomCallbackType
2624
import com.google.android.fhir.datacapture.DataCaptureConfig
27-
import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewAnswerOption
25+
import com.google.android.fhir.datacapture.ExternalAnswerValueSetResolver
2826
import com.google.android.fhir.search.search
2927
import kotlinx.coroutines.CoroutineScope
3028
import kotlinx.coroutines.Dispatchers
29+
import kotlinx.coroutines.delay
3130
import kotlinx.coroutines.launch
3231
import org.hl7.fhir.r4.model.Bundle
32+
import org.hl7.fhir.r4.model.Coding
3333

3434
class CatalogApplication : Application(), DataCaptureConfig.Provider {
3535
// Only initiate the FhirEngine when used for the first time, not when the app is created.
@@ -47,15 +47,26 @@ class CatalogApplication : Application(), DataCaptureConfig.Provider {
4747
xFhirQueryResolver = { fhirEngine.search(it).map { it.resource } },
4848
questionnaireItemViewHolderFactoryMatchersProviderFactory =
4949
ContribQuestionnaireItemViewHolderFactoryMatchersProviderFactory,
50-
callbacks = mapOf(Pair(CustomCallbackType.AUTO_COMPLETE, AutoCompleteCallback(
51-
callback = { query ->
52-
run {
53-
listOf(AutoCompleteViewAnswerOption("a", "Type 2 Diabetes Mellitus"),
54-
AutoCompleteViewAnswerOption("b", "Test")
50+
valueSetResolverExternal =
51+
object : ExternalAnswerValueSetResolver {
52+
override suspend fun resolve(uri: String, query: String?): List<Coding> {
53+
delay(1000)
54+
// Here we can call out to our FHIR terminology server with the provided uri and query
55+
if (uri == "https://my.url/fhir/ValueSet/my-valueset" && !query.isNullOrBlank()) {
56+
return listOf(
57+
Coding().apply {
58+
code = "a"
59+
display = "Custom response A"
60+
},
61+
Coding().apply {
62+
code = "b"
63+
display = "Custom response B"
64+
},
5565
)
66+
}
67+
return emptyList()
5668
}
57-
}
58-
)))
69+
},
5970
)
6071

6172
CoroutineScope(Dispatchers.IO).launch {

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

+7-7
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@ data class DataCaptureConfig(
5656
var questionnaireItemViewHolderFactoryMatchersProviderFactory:
5757
QuestionnaireItemViewHolderFactoryMatchersProviderFactory? =
5858
null,
59-
60-
/**
61-
* A [CustomCallback] may be set by the client to override the behaviour of an existing component
62-
* in the sdc.
63-
*/
64-
var callback: CustomCallback<*>? = null,
6559
) {
6660

6761
/**
@@ -80,12 +74,18 @@ data class DataCaptureConfig(
8074
* allows the library to render answer options to `choice` and `open-choice` type questions more
8175
* dynamically.
8276
*
77+
* Optional query parameter can be used to accept the search string from user input for server-side
78+
* filtering.
79+
*
8380
* NOTE: The result of the resolution may be cached to improve performance. In other words, the
8481
* resolver may be called only once after which the same answer value set may be used multiple times
8582
* in the UI to populate answer options.
83+
*
84+
* @param uri The uri used to identify the questionnaire item
85+
* @param query The text input from the user
8686
*/
8787
interface ExternalAnswerValueSetResolver {
88-
suspend fun resolve(uri: String): List<Coding>
88+
suspend fun resolve(uri: String, query: String?): List<Coding>
8989
}
9090

9191
/**

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

-2
Original file line numberDiff line numberDiff line change
@@ -391,5 +391,3 @@ internal object DiffCallbacks {
391391
}
392392
}
393393
}
394-
395-
typealias CustomCallback<T> = (String, String) -> List<T>

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

+20-5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import kotlinx.coroutines.flow.stateIn
7676
import kotlinx.coroutines.flow.update
7777
import kotlinx.coroutines.flow.withIndex
7878
import kotlinx.coroutines.launch
79+
import org.hl7.fhir.r4.model.Coding
7980
import org.hl7.fhir.r4.model.DateTimeType
8081
import org.hl7.fhir.r4.model.Questionnaire
8182
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
@@ -96,10 +97,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
9697
DataCapture.getConfiguration(application).valueSetResolverExternal
9798
}
9899

99-
private val callback: CustomCallback<*>? by lazy {
100-
DataCapture.getConfiguration(application).callback
101-
}
102-
103100
/** The current questionnaire as questions are being answered. */
104101
internal val questionnaire: Questionnaire
105102

@@ -393,6 +390,23 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
393390
modificationCount.update { it + 1 }
394391
}
395392

393+
/**
394+
* Function to dynamically resolve answer options for the AutoComplete component using
395+
* [ExternalAnswerValueSetResolver.resolve]
396+
*/
397+
private val autoCompleteAnswerOptionResolver: (String, String?, (List<Coding>) -> Unit) -> Unit =
398+
{ query, answerValueSet, callback ->
399+
viewModelScope.launch {
400+
val response =
401+
if (externalValueSetResolver != null && answerValueSet != null) {
402+
externalValueSetResolver!!.resolve(query, answerValueSet)
403+
} else {
404+
emptyList()
405+
}
406+
callback(response)
407+
}
408+
}
409+
396410
private val expressionEvaluator: ExpressionEvaluator =
397411
ExpressionEvaluator(
398412
questionnaire,
@@ -954,6 +968,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
954968
validationResult = validationResult,
955969
answersChangedCallback = answersChangedCallback,
956970
enabledAnswerOptions = enabledQuestionnaireAnswerOptions,
971+
autoCompleteAnswerOptionResolver = autoCompleteAnswerOptionResolver,
957972
minAnswerValue =
958973
questionnaireItem.minValueCqfCalculatedValueExpression?.let {
959974
expressionEvaluator.evaluateExpressionValue(
@@ -989,7 +1004,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
9891004
),
9901005
isHelpCardOpen = isHelpCard && isHelpCardOpen,
9911006
helpCardStateChangedCallback = helpCardStateChangedCallback,
992-
callback = callback,
1007+
// suggestions = suggestions,
9931008
),
9941009
)
9951010
add(question)

datacapture/src/main/java/com/google/android/fhir/datacapture/expressions/EnabledAnswerOptionsEvaluator.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2024 Google LLC
2+
* Copyright 2022-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -179,7 +179,7 @@ internal class EnabledAnswerOptionsEvaluator(
179179
}
180180
} else {
181181
// Ask the client to provide the answers from an external expanded Valueset.
182-
externalValueSetResolver?.resolve(uri)?.map { coding ->
182+
externalValueSetResolver?.resolve(uri, null)?.map { coding ->
183183
Questionnaire.QuestionnaireItemAnswerOptionComponent(coding.copy())
184184
}
185185
}

datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package com.google.android.fhir.datacapture.views
1919
import android.content.Context
2020
import android.text.Spanned
2121
import androidx.recyclerview.widget.RecyclerView
22-
import com.google.android.fhir.datacapture.CustomCallback
2322
import com.google.android.fhir.datacapture.R
2423
import com.google.android.fhir.datacapture.extensions.displayString
2524
import com.google.android.fhir.datacapture.extensions.isHelpCode
@@ -31,6 +30,7 @@ import com.google.android.fhir.datacapture.validation.NotValidated
3130
import com.google.android.fhir.datacapture.validation.Valid
3231
import com.google.android.fhir.datacapture.validation.ValidationResult
3332
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
33+
import org.hl7.fhir.r4.model.Coding
3434
import org.hl7.fhir.r4.model.Questionnaire
3535
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
3636
import org.hl7.fhir.r4.model.QuestionnaireResponse
@@ -94,7 +94,9 @@ data class QuestionnaireViewItem(
9494
val helpCardStateChangedCallback: (Boolean, QuestionnaireResponseItemComponent) -> Unit =
9595
{ _, _ ->
9696
},
97-
val callback: CustomCallback<*>? = null,
97+
internal val autoCompleteAnswerOptionResolver: (String, String?, (List<Coding>) -> Unit) -> Unit =
98+
{ _, _, _ ->
99+
},
98100
) {
99101

100102
fun getQuestionnaireResponseItem(): QuestionnaireResponseItemComponent = questionnaireResponseItem

datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactory.kt

+36-25
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import androidx.appcompat.app.AppCompatActivity
2828
import androidx.core.view.children
2929
import androidx.core.view.get
3030
import androidx.core.view.isEmpty
31+
import androidx.core.widget.addTextChangedListener
3132
import androidx.lifecycle.lifecycleScope
32-
import com.google.android.fhir.datacapture.CustomCallback
3333
import com.google.android.fhir.datacapture.R
3434
import com.google.android.fhir.datacapture.extensions.displayString
3535
import com.google.android.fhir.datacapture.extensions.identifierString
@@ -58,14 +58,11 @@ internal object AutoCompleteViewHolderFactory :
5858
private lateinit var autoCompleteTextView: MaterialAutoCompleteTextView
5959
private lateinit var chipContainer: ChipGroup
6060
private lateinit var textInputLayout: TextInputLayout
61-
private lateinit var adapter: ArrayAdapter<AutoCompleteViewAnswerOption>
61+
private lateinit var adapter: AutoCompleteArrayAdapter
6262

6363
private val canHaveMultipleAnswers
6464
get() = questionnaireViewItem.questionnaireItem.repeats
6565

66-
private val callback: CustomCallback<*>?
67-
get() = questionnaireViewItem.callback
68-
6966
override lateinit var questionnaireViewItem: QuestionnaireViewItem
7067
private lateinit var errorTextView: TextView
7168

@@ -82,30 +79,50 @@ internal object AutoCompleteViewHolderFactory :
8279
override fun bind(questionnaireViewItem: QuestionnaireViewItem) {
8380
header.bind(questionnaireViewItem)
8481
header.showRequiredOrOptionalTextInHeaderView(questionnaireViewItem)
85-
val suggestions = mutableListOf<AutoCompleteViewAnswerOption>()
8682
val answerOptionValues =
8783
questionnaireViewItem.enabledAnswerOptions.map {
8884
AutoCompleteViewAnswerOption(
8985
answerId = it.value.identifierString(header.context),
9086
answerDisplay = it.value.displayString(header.context),
9187
)
9288
}
93-
suggestions.addAll(answerOptionValues)
9489
adapter =
9590
AutoCompleteArrayAdapter(
9691
context = header.context,
9792
resource = R.layout.drop_down_list_item,
9893
textViewResourceId = R.id.answer_option_textview,
9994
objects = answerOptionValues,
100-
callback = callback,
101-
answerValueSet = questionnaireViewItem.questionnaireItem.answerValueSet,
10295
)
10396
autoCompleteTextView.setAdapter(adapter)
10497
// Remove chips if any from the last bindView call on this VH.
10598
chipContainer.removeAllViews()
10699
presetValuesIfAny()
107100

108101
displayValidationResult(questionnaireViewItem.validationResult)
102+
103+
val serverSideFiltering =
104+
questionnaireViewItem.questionnaireItem.answerValueSet != null &&
105+
answerOptionValues.isEmpty()
106+
107+
autoCompleteTextView.addTextChangedListener { text ->
108+
if (serverSideFiltering) {
109+
questionnaireViewItem.autoCompleteAnswerOptionResolver(
110+
text.toString(),
111+
questionnaireViewItem.questionnaireItem.answerValueSet,
112+
) { response ->
113+
val items =
114+
response.map {
115+
AutoCompleteViewAnswerOption(
116+
answerId = it.code,
117+
answerDisplay = it.display,
118+
)
119+
}
120+
adapter.updateData(items)
121+
}
122+
} else {
123+
adapter.clientSideFilter(text.toString())
124+
}
125+
}
109126
}
110127

111128
override fun setReadOnly(isReadOnly: Boolean) {
@@ -272,7 +289,7 @@ internal object AutoCompleteViewHolderFactory :
272289
* An answer option that would show up as a dropdown item in an [AutoCompleteViewHolderFactory]
273290
* textview
274291
*/
275-
data class AutoCompleteViewAnswerOption(val answerId: String, val answerDisplay: String) {
292+
internal data class AutoCompleteViewAnswerOption(val answerId: String, val answerDisplay: String) {
276293
override fun toString(): String {
277294
return this.answerDisplay
278295
}
@@ -283,8 +300,6 @@ internal class AutoCompleteArrayAdapter(
283300
val resource: Int,
284301
val textViewResourceId: Int,
285302
private val objects: List<AutoCompleteViewAnswerOption>,
286-
private val callback: CustomCallback<*>? = null,
287-
private val answerValueSet: String? = null,
288303
) : ArrayAdapter<AutoCompleteViewAnswerOption>(context, resource, textViewResourceId, objects) {
289304

290305
private var items = listOf<AutoCompleteViewAnswerOption>()
@@ -303,6 +318,11 @@ internal class AutoCompleteArrayAdapter(
303318
notifyDataSetChanged()
304319
}
305320

321+
fun clientSideFilter(query: String) {
322+
items = objects.filter { it.answerDisplay.contains(query, ignoreCase = true) }
323+
notifyDataSetChanged()
324+
}
325+
306326
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
307327
return getView(position, convertView, parent)
308328
}
@@ -312,21 +332,12 @@ internal class AutoCompleteArrayAdapter(
312332
@Suppress("UNCHECKED_CAST")
313333
override fun getFilter(): Filter {
314334
return object : Filter() {
335+
315336
override fun performFiltering(constraint: CharSequence?): FilterResults {
316-
val query = (constraint?.toString() ?: "").trim()
317-
val filteredResults: List<AutoCompleteViewAnswerOption> =
318-
if (callback != null && answerValueSet != null && objects.isEmpty()) {
319-
(callback as? CustomCallback<AutoCompleteViewAnswerOption>)?.invoke(
320-
query,
321-
answerValueSet,
322-
)
323-
?: emptyList()
324-
} else {
325-
objects.filter { it.answerDisplay.contains(query, ignoreCase = true) }
326-
}
337+
// Prevent default filtering behaviour
327338
return FilterResults().apply {
328-
values = filteredResults
329-
count = filteredResults.size
339+
values = items
340+
count = items.size
330341
}
331342
}
332343

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -5022,7 +5022,7 @@ class QuestionnaireViewModelTest {
50225022
DataCaptureConfig(
50235023
valueSetResolverExternal =
50245024
object : ExternalAnswerValueSetResolver {
5025-
override suspend fun resolve(uri: String): List<Coding> {
5025+
override suspend fun resolve(uri: String, query: String?): List<Coding> {
50265026
return if (uri == CODE_SYSTEM_YES_NO) {
50275027
listOf(
50285028
Coding().apply {

0 commit comments

Comments
 (0)