From f343e26cc4d8625aeb7f328338bfb8b69c0947d3 Mon Sep 17 00:00:00 2001 From: santosh-pingle <86107848+santosh-pingle@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:17:38 +0530 Subject: [PATCH 1/2] Search use case in the demo app. (#2754) * Search use case. * Update ui edge cases. * spotless apply. * Address ui changes. * Code refactoring * address review comments. --------- Co-authored-by: Santosh Pingle --- .../android/fhir/demo/PatientListFragment.kt | 54 +++++++------ .../android/fhir/demo/PatientListViewModel.kt | 78 ++++++++++++++++++- .../main/res/layout/fragment_patient_list.xml | 49 ++++++++++-- demo/src/main/res/values/strings.xml | 4 +- 4 files changed, 153 insertions(+), 32 deletions(-) diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt index e61ab37648..31eeba82a8 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt @@ -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. @@ -28,6 +28,7 @@ import android.view.inputmethod.InputMethodManager import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView +import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.NavHostFragment @@ -53,6 +54,35 @@ class PatientListFragment : Fragment() { savedInstanceState: Bundle?, ): View { _binding = FragmentPatientListBinding.inflate(inflater, container, false) + + val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + + binding.givenNameEditText.apply { + addTextChangedListener( + onTextChanged = { text, _, _, _ -> + patientListViewModel.setPatientGivenName(text.toString()) + }, + ) + setOnFocusChangeListener { view, hasFocus -> + if (!hasFocus) { + imm.hideSoftInputFromWindow(view.windowToken, 0) + } + } + } + + binding.familyNameEditText.apply { + addTextChangedListener( + onTextChanged = { text, _, _, _ -> + patientListViewModel.setPatientFamilyName(text.toString()) + }, + ) + setOnFocusChangeListener { view, hasFocus -> + if (!hasFocus) { + imm.hideSoftInputFromWindow(view.windowToken, 0) + } + } + } + return binding.root } @@ -87,27 +117,6 @@ class PatientListFragment : Fragment() { binding.patientListContainer.patientCount.text = "$it Patient(s)" } - searchView = binding.search - searchView.setOnQueryTextListener( - object : SearchView.OnQueryTextListener { - override fun onQueryTextChange(newText: String): Boolean { - patientListViewModel.searchPatientsByName(newText) - return true - } - - override fun onQueryTextSubmit(query: String): Boolean { - patientListViewModel.searchPatientsByName(query) - return true - } - }, - ) - searchView.setOnQueryTextFocusChangeListener { view, focused -> - if (!focused) { - // hide soft keyboard - (requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .hideSoftInputFromWindow(view.windowToken, 0) - } - } requireActivity() .onBackPressedDispatcher .addCallback( @@ -123,7 +132,6 @@ class PatientListFragment : Fragment() { } }, ) - setHasOptionsMenu(true) } diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt index fbf7b43fae..8aa22a9b51 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-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. @@ -39,12 +39,11 @@ import org.hl7.fhir.r4.model.RiskAssessment */ class PatientListViewModel(application: Application, private val fhirEngine: FhirEngine) : AndroidViewModel(application) { - val liveSearchedPatients = MutableLiveData>() val patientCount = MutableLiveData() init { - updatePatientListAndPatientCount({ getSearchResults() }, { count() }) + updatePatientListAndPatientCount({ getSearchResults() }, { searchedPatientCount() }) } fun searchPatientsByName(nameQuery: String) { @@ -174,6 +173,79 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi throw IllegalArgumentException("Unknown ViewModel class") } } + + private var patientGivenName: String? = null + private var patientFamilyName: String? = null + + fun setPatientGivenName(givenName: String) { + patientGivenName = givenName + searchPatientsByParameter() + } + + fun setPatientFamilyName(familyName: String) { + patientFamilyName = familyName + searchPatientsByParameter() + } + + private fun searchPatientsByParameter() { + viewModelScope.launch { + liveSearchedPatients.value = searchPatients() + patientCount.value = searchedPatientCount() + } + } + + private suspend fun searchPatients(): List { + val patients = + fhirEngine + .search { + filter( + Patient.GIVEN, + { + modifier = StringFilterModifier.CONTAINS + this.value = patientGivenName ?: "" + }, + ) + filter( + Patient.FAMILY, + { + modifier = StringFilterModifier.CONTAINS + this.value = patientFamilyName ?: "" + }, + ) + sort(Patient.GIVEN, Order.ASCENDING) + count = 100 + from = 0 + } + .mapIndexed { index, fhirPatient -> fhirPatient.resource.toPatientItem(index + 1) } + .toMutableList() + + val risks = getRiskAssessments() + patients.forEach { patient -> + risks["Patient/${patient.resourceId}"]?.let { + patient.risk = it.prediction?.first()?.qualitativeRisk?.coding?.first()?.code + } + } + return patients + } + + private suspend fun searchedPatientCount(): Long { + return fhirEngine.count { + filter( + Patient.GIVEN, + { + modifier = StringFilterModifier.CONTAINS + this.value = patientGivenName ?: "" + }, + ) + filter( + Patient.FAMILY, + { + modifier = StringFilterModifier.CONTAINS + this.value = patientFamilyName ?: "" + }, + ) + } + } } internal fun Patient.toPatientItem(position: Int): PatientListViewModel.PatientItem { diff --git a/demo/src/main/res/layout/fragment_patient_list.xml b/demo/src/main/res/layout/fragment_patient_list.xml index 09cfd442fb..1fcf485a66 100644 --- a/demo/src/main/res/layout/fragment_patient_list.xml +++ b/demo/src/main/res/layout/fragment_patient_list.xml @@ -13,15 +13,54 @@ android:focusableInTouchMode="true" android:orientation="vertical" > - - + + + + + + + + + + + + + %1$s: %2$s\nEffective: %3$s - Find by Patient Name Are you sure you want to discard the answers? @@ -74,4 +73,7 @@ Last sync status: %1$s Last sync status: Not available Periodic sync + Search Patient by + Given name + Family name From 2a7e91f7e118569de896a033b1583cafc6cc1555 Mon Sep 17 00:00:00 2001 From: santosh-pingle <86107848+santosh-pingle@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:02:21 +0530 Subject: [PATCH 2/2] CRUD operation screen in the demo app. (#2746) * crud operation show case. * Tab layout, birthdate, and edge cases in tab switching. * code clean up. * code clean up * Address review comment. * Address ui changes. * clear ui state. * address review comments. * refactoring. --------- Co-authored-by: Santosh Pingle --- .../fhir/demo/CrudOperationFragment.kt | 338 ++++++++++++++++++ .../fhir/demo/CrudOperationViewModel.kt | 162 +++++++++ .../google/android/fhir/demo/HomeFragment.kt | 6 +- .../demo/helpers/PatientCreationHelper.kt | 53 ++- .../main/res/layout/fragment_crud_layout.xml | 233 ++++++++++++ demo/src/main/res/layout/fragment_home.xml | 42 ++- .../res/navigation/reference_nav_graph.xml | 10 + demo/src/main/res/values/strings.xml | 5 + demo/src/main/res/values/styles.xml | 12 + 9 files changed, 847 insertions(+), 14 deletions(-) create mode 100644 demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt create mode 100644 demo/src/main/java/com/google/android/fhir/demo/CrudOperationViewModel.kt create mode 100644 demo/src/main/res/layout/fragment_crud_layout.xml diff --git a/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt new file mode 100644 index 0000000000..2694044bc8 --- /dev/null +++ b/demo/src/main/java/com/google/android/fhir/demo/CrudOperationFragment.kt @@ -0,0 +1,338 @@ +/* + * 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.demo + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.CheckBox +import android.widget.EditText +import android.widget.RadioGroup +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.NavHostFragment +import com.google.android.fhir.demo.helpers.PatientCreationHelper +import com.google.android.material.tabs.TabLayout +import kotlinx.coroutines.launch +import org.hl7.fhir.r4.model.Enumerations + +class CrudOperationFragment : Fragment() { + private val crudOperationViewModel: CrudOperationViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return inflater.inflate(R.layout.fragment_crud_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setUpActionBar() + setHasOptionsMenu(true) + setupUiOnScreenLaunch() + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + crudOperationViewModel.patientUiState.collect { patientUiState -> + patientUiState?.let { + when (it.operationType) { + OperationType.CREATE -> { + Toast.makeText(requireContext(), "Patient is saved", Toast.LENGTH_SHORT).show() + } + OperationType.READ -> displayPatientDetails(it) + OperationType.UPDATE -> { + Toast.makeText(requireContext(), "Patient is updated", Toast.LENGTH_SHORT).show() + } + OperationType.DELETE -> { + // Reset the page as the patient has been deleted. + clearUiFieldValues() + configureFieldsForOperation(OperationType.DELETE) + Toast.makeText(requireContext(), "Patient is deleted", Toast.LENGTH_SHORT).show() + } + } + } + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + NavHostFragment.findNavController(this).navigateUp() + true + } + else -> false + } + } + + private fun setUpActionBar() { + (requireActivity() as AppCompatActivity).supportActionBar?.apply { + title = requireContext().getString(R.string.crud_operations) + setDisplayHomeAsUpEnabled(true) + } + } + + private fun setupUiOnScreenLaunch() { + setupTabLayoutChangeListener() + selectTab(TAB_CREATE) + setupUiForCrudOperation(OperationType.CREATE) + + requireView().findViewById