Skip to content

Commit

Permalink
Introduce new QuestionnaireAdapterItem type (#1767)
Browse files Browse the repository at this point in the history
This is a sealed class that currently only has one subtype, wrapping
`QuestionnaireItemViewItem`.

More subtypes will be added in the future to support (for example)
repeated group headers, allowing us to break from the current 1:1
mapping of question:row in our adapters.

`QuestionnaireItemEditAdapter` and `QuestionnaireItemReviewAdapter` were
changed to use this new type.
  • Loading branch information
kevinmost committed Jan 4, 2023
1 parent 1b08b55 commit ac1d9a2
Show file tree
Hide file tree
Showing 9 changed files with 610 additions and 364 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest {
val viewHolderFromAdapter =
questionnaireItemEditAdapter.createViewHolder(
parent,
QuestionnaireItemViewHolderType.PHONE_NUMBER.value
QuestionnaireItemEditAdapter.ViewType.from(
type = QuestionnaireItemEditAdapter.ViewType.Type.QUESTION,
subtype = QuestionnaireItemViewHolderType.PHONE_NUMBER.value,
)
.viewType,
)
assertThat(viewHolderFromAdapter).isInstanceOf(viewHolder::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2022 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:https://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 com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem

/** Various types of rows that can be used in a Questionnaire RecyclerView. */
internal sealed interface QuestionnaireAdapterItem {
/** A row for a quesion in a Questionnaire RecyclerView. */
data class Question(val item: QuestionnaireItemViewItem) : QuestionnaireAdapterItem
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ open class QuestionnaireFragment : Fragment() {
is DisplayMode.ReviewMode -> {
// Set items
questionnaireEditRecyclerView.visibility = View.GONE
questionnaireItemReviewAdapter.submitList(state.items)
questionnaireItemReviewAdapter.submitList(
state.items.filterIsInstance<QuestionnaireAdapterItem.Question>()
)
questionnaireReviewRecyclerView.visibility = View.VISIBLE

// Set button visibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,36 @@ import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType
internal class QuestionnaireItemEditAdapter(
private val questionnaireItemViewHolderMatchers:
List<QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatcher> =
emptyList()
) : ListAdapter<QuestionnaireItemViewItem, QuestionnaireItemViewHolder>(DiffCallback) {
emptyList(),
) : ListAdapter<QuestionnaireAdapterItem, QuestionnaireItemViewHolder>(DiffCallbacks.ITEMS) {
/**
* @param viewType the integer value of the [QuestionnaireItemViewHolderType] used to render the
* [QuestionnaireItemViewItem].
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionnaireItemViewHolder {
val typedViewType = ViewType.parse(viewType)
val subtype = typedViewType.subtype
return when (typedViewType.type) {
ViewType.Type.QUESTION -> onCreateViewHolderQuestion(parent = parent, subtype = subtype)
}
}

private fun onCreateViewHolderQuestion(
parent: ViewGroup,
subtype: Int,
): QuestionnaireItemViewHolder {
val numOfCanonicalWidgets = QuestionnaireItemViewHolderType.values().size
check(viewType < numOfCanonicalWidgets + questionnaireItemViewHolderMatchers.size) {
check(subtype < numOfCanonicalWidgets + questionnaireItemViewHolderMatchers.size) {
"Invalid widget type specified. Widget Int type cannot exceed the total number of supported custom and canonical widgets"
}

// Map custom widget viewTypes to their corresponding widget factories
if (viewType >= numOfCanonicalWidgets)
return questionnaireItemViewHolderMatchers[viewType - numOfCanonicalWidgets]
if (subtype >= numOfCanonicalWidgets)
return questionnaireItemViewHolderMatchers[subtype - numOfCanonicalWidgets]
.factory.create(parent)

val viewHolderFactory =
when (QuestionnaireItemViewHolderType.fromInt(viewType)) {
when (QuestionnaireItemViewHolderType.fromInt(subtype)) {
QuestionnaireItemViewHolderType.GROUP -> QuestionnaireItemGroupViewHolderFactory
QuestionnaireItemViewHolderType.BOOLEAN_TYPE_PICKER ->
QuestionnaireItemBooleanTypePickerViewHolderFactory
Expand Down Expand Up @@ -95,7 +106,56 @@ internal class QuestionnaireItemEditAdapter(
}

override fun onBindViewHolder(holder: QuestionnaireItemViewHolder, position: Int) {
holder.bind(getItem(position))
when (val item = getItem(position)) {
is QuestionnaireAdapterItem.Question -> {
holder.bind(item.item)
}
}
}

override fun getItemViewType(position: Int): Int {
// Because we have multiple Item subtypes, we will pack two ints into the item view type.

// The first 8 bits will be represented by this type, which is unique for each Item subclass.
val type: ViewType.Type
// The last 24 bits will be represented by this subtype, which will further divide each Item
// subclass into more view types.
val subtype: Int
when (val item = getItem(position)) {
is QuestionnaireAdapterItem.Question -> {
type = ViewType.Type.QUESTION
subtype = getItemViewTypeForQuestion(item.item)
}
}
return ViewType.from(type = type, subtype = subtype).viewType
}

/**
* Utility to pack two types (a "type" and "subtype") into a single "viewType" int, for use with
* [getItemViewType].
*
* [type] is contained in the first 8 bits of the int, and should be unique for each type of
* [QuestionnaireAdapterItem].
*
* [subtype] is contained in the lower 24 bits of the int, and should be used to differentiate
* between different items within the same [QuestionnaireAdapterItem] type.
*/
@JvmInline
internal value class ViewType(val viewType: Int) {
val subtype: Int
get() = viewType and 0xFFFFFF
val type: Type
get() = Type.values()[viewType shr 24]

companion object {
fun parse(viewType: Int): ViewType = ViewType(viewType)

fun from(type: Type, subtype: Int): ViewType = ViewType((type.ordinal shl 24) or subtype)
}

enum class Type {
QUESTION,
}
}

/**
Expand All @@ -105,11 +165,9 @@ internal class QuestionnaireItemEditAdapter(
* (http:https://hl7.org/fhir/R4/valueset-questionnaire-item-control.html) used in the itemControl
* extension (http:https://hl7.org/fhir/R4/extension-questionnaire-itemcontrol.html).
*/
override fun getItemViewType(position: Int): Int {
return getItemViewTypeMapping(getItem(position))
}

internal fun getItemViewTypeMapping(questionnaireItemViewItem: QuestionnaireItemViewItem): Int {
internal fun getItemViewTypeForQuestion(
questionnaireItemViewItem: QuestionnaireItemViewItem,
): Int {
val questionnaireItem = questionnaireItemViewItem.questionnaireItem
// For custom widgets, generate an int value that's greater than any int assigned to the
// canonical FHIR widgets
Expand Down Expand Up @@ -141,7 +199,7 @@ internal class QuestionnaireItemEditAdapter(
}

private fun getChoiceViewHolderType(
questionnaireItemViewItem: QuestionnaireItemViewItem
questionnaireItemViewItem: QuestionnaireItemViewItem,
): QuestionnaireItemViewHolderType {
val questionnaireItem = questionnaireItemViewItem.questionnaireItem

Expand Down Expand Up @@ -169,7 +227,7 @@ internal class QuestionnaireItemEditAdapter(
}

private fun getIntegerViewHolderType(
questionnaireItemViewItem: QuestionnaireItemViewItem
questionnaireItemViewItem: QuestionnaireItemViewItem,
): QuestionnaireItemViewHolderType {
val questionnaireItem = questionnaireItemViewItem.questionnaireItem
// Use the view type that the client wants if they specified an itemControl
Expand All @@ -178,7 +236,7 @@ internal class QuestionnaireItemEditAdapter(
}

private fun getStringViewHolderType(
questionnaireItemViewItem: QuestionnaireItemViewItem
questionnaireItemViewItem: QuestionnaireItemViewItem,
): QuestionnaireItemViewHolderType {
val questionnaireItem = questionnaireItemViewItem.questionnaireItem
// Use the view type that the client wants if they specified an itemControl
Expand All @@ -195,26 +253,54 @@ internal class QuestionnaireItemEditAdapter(
}
}

internal object DiffCallback : DiffUtil.ItemCallback<QuestionnaireItemViewItem>() {
/**
* [QuestionnaireItemViewItem] is a transient object for the UI only. Whenever the user makes any
* change via the UI, a new list of [QuestionnaireItemViewItem]s will be created, each holding
* references to the underlying [QuestionnaireItem] and [QuestionnaireResponseItem], both of which
* should be read-only, and the current answers. To help recycler view handle update and/or
* animations, we consider two [QuestionnaireItemViewItem]s to be the same if they have the same
* underlying [QuestionnaireItem] and [QuestionnaireResponseItem].
*/
override fun areItemsTheSame(
oldItem: QuestionnaireItemViewItem,
newItem: QuestionnaireItemViewItem
) = oldItem.hasTheSameItem(newItem)

override fun areContentsTheSame(
oldItem: QuestionnaireItemViewItem,
newItem: QuestionnaireItemViewItem
): Boolean {
return oldItem.hasTheSameItem(newItem) &&
oldItem.hasTheSameAnswer(newItem) &&
oldItem.hasTheSameValidationResult(newItem)
}
internal object DiffCallbacks {
val ITEMS =
object : DiffUtil.ItemCallback<QuestionnaireAdapterItem>() {
override fun areItemsTheSame(
oldItem: QuestionnaireAdapterItem,
newItem: QuestionnaireAdapterItem,
): Boolean =
when (oldItem) {
is QuestionnaireAdapterItem.Question -> {
newItem is QuestionnaireAdapterItem.Question &&
QUESTIONS.areItemsTheSame(oldItem, newItem)
}
}

override fun areContentsTheSame(
oldItem: QuestionnaireAdapterItem,
newItem: QuestionnaireAdapterItem,
): Boolean =
when (oldItem) {
is QuestionnaireAdapterItem.Question -> {
newItem is QuestionnaireAdapterItem.Question &&
QUESTIONS.areContentsTheSame(oldItem, newItem)
}
}
}

val QUESTIONS =
object : DiffUtil.ItemCallback<QuestionnaireAdapterItem.Question>() {
/**
* [QuestionnaireItemViewItem] is a transient object for the UI only. Whenever the user makes
* any change via the UI, a new list of [QuestionnaireItemViewItem]s will be created, each
* holding references to the underlying [QuestionnaireItem] and [QuestionnaireResponseItem],
* both of which should be read-only, and the current answers. To help recycler view handle
* update and/or animations, we consider two [QuestionnaireItemViewItem]s to be the same if
* they have the same underlying [QuestionnaireItem] and [QuestionnaireResponseItem].
*/
override fun areItemsTheSame(
oldItem: QuestionnaireAdapterItem.Question,
newItem: QuestionnaireAdapterItem.Question,
) = oldItem.item.hasTheSameItem(newItem.item)

override fun areContentsTheSame(
oldItem: QuestionnaireAdapterItem.Question,
newItem: QuestionnaireAdapterItem.Question,
): Boolean {
return oldItem.item.hasTheSameItem(newItem.item) &&
oldItem.item.hasTheSameAnswer(newItem.item) &&
oldItem.item.hasTheSameValidationResult(newItem.item)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import com.google.android.fhir.datacapture.views.QuestionnaireItemSimpleQuestionAnswerDisplayViewHolderFactory
import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolder
import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem

/** List Adapter used to bind answers to [QuestionnaireItemViewHolder] in review mode. */
internal class QuestionnaireItemReviewAdapter :
ListAdapter<QuestionnaireItemViewItem, QuestionnaireItemViewHolder>(DiffCallback) {
ListAdapter<QuestionnaireAdapterItem.Question, QuestionnaireItemViewHolder>(
DiffCallbacks.QUESTIONS
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionnaireItemViewHolder {
return QuestionnaireItemSimpleQuestionAnswerDisplayViewHolderFactory.create(parent)
}

override fun onBindViewHolder(holder: QuestionnaireItemViewHolder, position: Int) {
holder.bind(getItem(position))
holder.bind(getItem(position).item)
}
}
Loading

0 comments on commit ac1d9a2

Please sign in to comment.