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

Large diffs are not rendered by default.

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 @@ -22,10 +22,13 @@ 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
@@ -0,0 +1,173 @@
/*
* Copyright 2024-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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.datacapture

import android.view.ViewGroup
import com.google.android.fhir.datacapture.QuestionnaireEditAdapter.Companion.MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DIALOG
import com.google.android.fhir.datacapture.QuestionnaireEditAdapter.Companion.MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DROP_DOWN
import com.google.android.fhir.datacapture.contrib.views.PhoneNumberViewHolderFactory
import com.google.android.fhir.datacapture.extensions.itemControl
import com.google.android.fhir.datacapture.extensions.shouldUseDialog
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.BooleanChoiceViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.CheckBoxGroupViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.DatePickerViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.DateTimePickerViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.DisplayViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.DropDownViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.EditTextDecimalViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.EditTextIntegerViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.EditTextMultiLineViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.EditTextSingleLineViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.GroupViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.QuantityViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemDialogSelectViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.RadioGroupViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.SliderViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.TimePickerViewHolderFactory
import org.hl7.fhir.r4.model.Questionnaire

fun getQuestionnaireItemViewHolder(
parent: ViewGroup,
questionnaireViewItem: QuestionnaireViewItem,
questionnaireItemViewHolderMatchers:
List<QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatcher>,
): QuestionnaireItemViewHolder {
// Find a matching custom widget
val questionnaireViewHolderFactory =
questionnaireItemViewHolderMatchers
.find { it.matches(questionnaireViewItem.questionnaireItem) }
?.factory
?: getQuestionnaireItemViewHolderFactory(getItemViewTypeForQuestion(questionnaireViewItem))
return questionnaireViewHolderFactory.create(parent)
}

private fun getQuestionnaireItemViewHolderFactory(
questionnaireViewHolderType: QuestionnaireViewHolderType,
): QuestionnaireItemViewHolderFactory {
val viewHolderFactory =
when (questionnaireViewHolderType) {
QuestionnaireViewHolderType.GROUP -> GroupViewHolderFactory
QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER -> BooleanChoiceViewHolderFactory
QuestionnaireViewHolderType.DATE_PICKER -> DatePickerViewHolderFactory
QuestionnaireViewHolderType.TIME_PICKER -> TimePickerViewHolderFactory
QuestionnaireViewHolderType.DATE_TIME_PICKER -> DateTimePickerViewHolderFactory
QuestionnaireViewHolderType.EDIT_TEXT_SINGLE_LINE -> EditTextSingleLineViewHolderFactory
QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE -> EditTextMultiLineViewHolderFactory
QuestionnaireViewHolderType.EDIT_TEXT_INTEGER -> EditTextIntegerViewHolderFactory
QuestionnaireViewHolderType.EDIT_TEXT_DECIMAL -> EditTextDecimalViewHolderFactory
QuestionnaireViewHolderType.RADIO_GROUP -> RadioGroupViewHolderFactory
QuestionnaireViewHolderType.DROP_DOWN -> DropDownViewHolderFactory
QuestionnaireViewHolderType.DISPLAY -> DisplayViewHolderFactory
QuestionnaireViewHolderType.QUANTITY -> QuantityViewHolderFactory
QuestionnaireViewHolderType.CHECK_BOX_GROUP -> CheckBoxGroupViewHolderFactory
QuestionnaireViewHolderType.AUTO_COMPLETE -> AutoCompleteViewHolderFactory
QuestionnaireViewHolderType.DIALOG_SELECT -> QuestionnaireItemDialogSelectViewHolderFactory
QuestionnaireViewHolderType.SLIDER -> SliderViewHolderFactory
QuestionnaireViewHolderType.PHONE_NUMBER -> PhoneNumberViewHolderFactory
QuestionnaireViewHolderType.ATTACHMENT -> AttachmentViewHolderFactory
}
return viewHolderFactory
}

/**
* Returns the [QuestionnaireViewHolderType] that will be used to render the
* [QuestionnaireViewItem]. This is determined by a combination of the data type of the question and
* any additional Questionnaire Item UI Control Codes
* (http://hl7.org/fhir/R4/valueset-questionnaire-item-control.html) used in the itemControl
* extension (http://hl7.org/fhir/R4/extension-questionnaire-itemcontrol.html).
*/
private fun getItemViewTypeForQuestion(
questionnaireViewItem: QuestionnaireViewItem,
): QuestionnaireViewHolderType {
val questionnaireItem = questionnaireViewItem.questionnaireItem

if (questionnaireViewItem.enabledAnswerOptions.isNotEmpty()) {
return getChoiceViewHolderType(questionnaireViewItem)
}

return when (val type = questionnaireItem.type) {
Questionnaire.QuestionnaireItemType.GROUP -> QuestionnaireViewHolderType.GROUP
Questionnaire.QuestionnaireItemType.BOOLEAN -> QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER
Questionnaire.QuestionnaireItemType.DATE -> QuestionnaireViewHolderType.DATE_PICKER
Questionnaire.QuestionnaireItemType.TIME -> QuestionnaireViewHolderType.TIME_PICKER
Questionnaire.QuestionnaireItemType.DATETIME -> QuestionnaireViewHolderType.DATE_TIME_PICKER
Questionnaire.QuestionnaireItemType.STRING -> getStringViewHolderType(questionnaireViewItem)
Questionnaire.QuestionnaireItemType.TEXT -> QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE
Questionnaire.QuestionnaireItemType.INTEGER -> getIntegerViewHolderType(questionnaireViewItem)
Questionnaire.QuestionnaireItemType.DECIMAL -> QuestionnaireViewHolderType.EDIT_TEXT_DECIMAL
Questionnaire.QuestionnaireItemType.CHOICE,
Questionnaire.QuestionnaireItemType.REFERENCE, -> getChoiceViewHolderType(questionnaireViewItem)
Questionnaire.QuestionnaireItemType.DISPLAY -> QuestionnaireViewHolderType.DISPLAY
Questionnaire.QuestionnaireItemType.QUANTITY -> QuestionnaireViewHolderType.QUANTITY
Questionnaire.QuestionnaireItemType.ATTACHMENT -> QuestionnaireViewHolderType.ATTACHMENT
else -> throw NotImplementedError("Question type $type not supported.")
}
}

private fun getChoiceViewHolderType(
questionnaireViewItem: QuestionnaireViewItem,
): QuestionnaireViewHolderType {
val questionnaireItem = questionnaireViewItem.questionnaireItem

// Use the view type that the client wants if they specified an itemControl or dialog extension
return when {
questionnaireItem.shouldUseDialog -> QuestionnaireViewHolderType.DIALOG_SELECT
else -> questionnaireItem.itemControl?.viewHolderType
}
// Otherwise, choose a sensible UI element automatically
?: run {
val numOptions = questionnaireViewItem.enabledAnswerOptions.size
when {
// Always use a dialog for questions with a large number of options
numOptions >= MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DIALOG ->
QuestionnaireViewHolderType.DIALOG_SELECT

// Use a check box group if repeated answers are permitted
questionnaireItem.repeats -> QuestionnaireViewHolderType.CHECK_BOX_GROUP

// Use a dropdown if there are a medium number of options
numOptions >= MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DROP_DOWN ->
QuestionnaireViewHolderType.DROP_DOWN

// Use a radio group only if there are a small number of options
else -> QuestionnaireViewHolderType.RADIO_GROUP
}
}
}

private fun getIntegerViewHolderType(
questionnaireViewItem: QuestionnaireViewItem,
): QuestionnaireViewHolderType {
val questionnaireItem = questionnaireViewItem.questionnaireItem
// Use the view type that the client wants if they specified an itemControl
return questionnaireItem.itemControl?.viewHolderType
?: QuestionnaireViewHolderType.EDIT_TEXT_INTEGER
}

private fun getStringViewHolderType(
questionnaireViewItem: QuestionnaireViewItem,
): QuestionnaireViewHolderType {
val questionnaireItem = questionnaireViewItem.questionnaireItem
// Use the view type that the client wants if they specified an itemControl
return questionnaireItem.itemControl?.viewHolderType
?: QuestionnaireViewHolderType.EDIT_TEXT_SINGLE_LINE
}
Loading
Loading