diff --git a/reference/build.gradle.kts b/reference/build.gradle.kts index be26c232eb..9284053742 100644 --- a/reference/build.gradle.kts +++ b/reference/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id(Plugins.BuildPlugins.application) id(Plugins.BuildPlugins.kotlinAndroid) id(Plugins.BuildPlugins.kotlinKapt) + id(Plugins.BuildPlugins.navSafeArgs) } android { diff --git a/reference/src/main/AndroidManifest.xml b/reference/src/main/AndroidManifest.xml index c4c8dbbc61..36881c7cd0 100644 --- a/reference/src/main/AndroidManifest.xml +++ b/reference/src/main/AndroidManifest.xml @@ -30,7 +30,7 @@ android:theme="@style/AppTheme" > @@ -40,17 +40,6 @@ - - - diff --git a/reference/src/main/java/com/google/android/fhir/reference/MainActivity.kt b/reference/src/main/java/com/google/android/fhir/reference/MainActivity.kt new file mode 100644 index 0000000000..1ec37cb6e0 --- /dev/null +++ b/reference/src/main/java/com/google/android/fhir/reference/MainActivity.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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.reference + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.work.Constraints +import com.google.android.fhir.reference.data.FhirPeriodicSyncWorker +import com.google.android.fhir.sync.PeriodicSyncConfiguration +import com.google.android.fhir.sync.RepeatInterval +import com.google.android.fhir.sync.Sync +import java.util.concurrent.TimeUnit + +const val MAX_RESOURCE_COUNT = 20 + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + toolbar.title = title + + Sync.periodicSync( + this, + PeriodicSyncConfiguration( + syncConstraints = Constraints.Builder().build(), + repeat = RepeatInterval(interval = 1, timeUnit = TimeUnit.MINUTES) + ) + ) + } +} diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientDetailActivity.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientDetailActivity.kt deleted file mode 100644 index f5f021f944..0000000000 --- a/reference/src/main/java/com/google/android/fhir/reference/PatientDetailActivity.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020 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.reference - -import android.content.Intent -import android.os.Bundle -import android.view.MenuItem -import androidx.appcompat.app.AppCompatActivity - -/** An activity representing a single Patient detail screen. */ -class PatientDetailActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_patient_detail) - setSupportActionBar(findViewById(R.id.detail_toolbar)) - - // Show the Up button in the action bar. - supportActionBar?.setDisplayHomeAsUpEnabled(true) - - if (savedInstanceState == null) { - // Create the detail fragment and add it to the activity - // using a fragment transaction. - val fragment = - PatientDetailFragment().apply { - arguments = - Bundle().apply { - putString( - PatientDetailFragment.ARG_ITEM_ID, - intent.getStringExtra(PatientDetailFragment.ARG_ITEM_ID) - ) - } - } - - supportFragmentManager - .beginTransaction() - .add(R.id.patient_detail_container, fragment) - .commit() - } - } - - override fun onOptionsItemSelected(item: MenuItem) = - when (item.itemId) { - android.R.id.home -> { - navigateUpTo(Intent(this, PatientListActivity::class.java)) - true - } - else -> super.onOptionsItemSelected(item) - } -} diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientDetailFragment.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientDetailFragment.kt deleted file mode 100644 index 4cceb83fa2..0000000000 --- a/reference/src/main/java/com/google/android/fhir/reference/PatientDetailFragment.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 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.reference - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.core.text.HtmlCompat -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.RecyclerView - -/** - * A fragment representing a single Patient detail screen. This fragment is contained in a - * [PatientDetailActivity]. - */ -class PatientDetailFragment : Fragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val rootView = inflater.inflate(R.layout.patient_detail, container, false) - - val recyclerView: RecyclerView = rootView.findViewById(R.id.observation_list) - val adapter = ObservationItemRecyclerViewAdapter() - recyclerView.adapter = adapter - - var patient: PatientListViewModel.PatientItem? = null - setupPatientData(rootView, patient) - - return rootView - } - - private fun setupPatientData(view: View, patient: PatientListViewModel.PatientItem?) { - if (patient != null) { - view.findViewById(R.id.patient_detail).text = - HtmlCompat.fromHtml(patient.html, HtmlCompat.FROM_HTML_MODE_LEGACY) - view.findViewById(R.id.name).text = patient.name - view.findViewById(R.id.dob).text = patient.dob - view.findViewById(R.id.gender).text = patient.phone - } - } - - companion object { - /** The fragment argument representing the patient item ID that this fragment represents. */ - const val ARG_ITEM_ID = "patient_item_id" - } -} diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsFragment.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsFragment.kt new file mode 100644 index 0000000000..132e11109e --- /dev/null +++ b/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsFragment.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2020 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.reference + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.text.HtmlCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.RecyclerView +import com.google.android.fhir.FhirEngine + +/** + * A fragment representing a single Patient detail screen. This fragment is contained in a + * [MainActivity]. + */ +class PatientDetailsFragment : Fragment() { + private lateinit var fhirEngine: FhirEngine + private lateinit var patientDetailsViewModel: PatientDetailsViewModel + private val args: PatientDetailsFragmentArgs by navArgs() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.patient_detail, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val recyclerView: RecyclerView = view.findViewById(R.id.observation_list) + val adapter = ObservationItemRecyclerViewAdapter() + recyclerView.adapter = adapter + fhirEngine = FhirApplication.fhirEngine(requireContext()) + patientDetailsViewModel = + ViewModelProvider( + this, + PatientDetailsViewModelFactory(requireActivity().application, fhirEngine, args.patientId) + ) + .get(PatientDetailsViewModel::class.java) + patientDetailsViewModel.livePatientData.observe(viewLifecycleOwner) { + setupPatientData(view, it) + } + patientDetailsViewModel.livePatientObservation.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + } + + private fun setupPatientData(view: View, patient: PatientListViewModel.PatientItem?) { + patient?.let { + view.findViewById(R.id.patient_detail).text = + HtmlCompat.fromHtml(it.html, HtmlCompat.FROM_HTML_MODE_LEGACY) + view.findViewById(R.id.name).text = it.name + view.findViewById(R.id.dob).text = it.dob + view.findViewById(R.id.gender).text = it.phone + + (requireActivity() as AppCompatActivity).supportActionBar?.apply { + title = it.name + setDisplayHomeAsUpEnabled(true) + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + NavHostFragment.findNavController(this).navigateUp() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + companion object { + /** The fragment argument representing the patient item ID that this fragment represents. */ + const val ARG_ITEM_ID = "patient_item_id" + } +} diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsViewModel.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsViewModel.kt new file mode 100644 index 0000000000..4bc6b37b8a --- /dev/null +++ b/reference/src/main/java/com/google/android/fhir/reference/PatientDetailsViewModel.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2020 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.reference + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.liveData +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.search.search +import org.hl7.fhir.r4.model.Observation +import org.hl7.fhir.r4.model.Patient + +/** + * The ViewModel helper class for PatientItemRecyclerViewAdapter, that is responsible for preparing + * data for UI. + */ +class PatientDetailsViewModel( + application: Application, + private val fhirEngine: FhirEngine, + private val patientId: String +) : AndroidViewModel(application) { + + val livePatientData = liveData { emit(getPatient()) } + val livePatientObservation = liveData { emit(getPatientObservations()) } + + private suspend fun getPatient(): PatientListViewModel.PatientItem { + val patient = fhirEngine.load(Patient::class.java, patientId) + return patient.toPatientItem(0) + } + + private suspend fun getPatientObservations(): List { + val observations: MutableList = mutableListOf() + fhirEngine + .search { filter(Observation.SUBJECT) { value = "Patient/$patientId" } } + .take(MAX_RESOURCE_COUNT) + .mapIndexed { index, fhirPatient -> createObservationItem(index + 1, fhirPatient) } + .let { observations.addAll(it) } + return observations + } + + /** Creates ObservationItem objects with displayable values from the Fhir Observation objects. */ + private fun createObservationItem( + position: Int, + observation: Observation + ): PatientListViewModel.ObservationItem { + // val observation: Observation = getObservationDetails(position) + val observationCode = observation.code.text + + // Show nothing if no values available for datetime and value quantity. + val dateTimeStr = + if (observation.hasEffectiveDateTimeType()) observation.effectiveDateTimeType.asStringValue() + else "No effective DateTime" + val value = + if (observation.hasValueQuantity()) observation.valueQuantity.value.toString() + else "No ValueQuantity" + val valueUnit = if (observation.hasValueQuantity()) observation.valueQuantity.unit else "" + val valueStr = "$value $valueUnit" + + return PatientListViewModel.ObservationItem( + position.toString(), + observationCode, + dateTimeStr, + valueStr + ) + } +} + +class PatientDetailsViewModelFactory( + private val application: Application, + private val fhirEngine: FhirEngine, + private val patientId: String +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + require(modelClass.isAssignableFrom(PatientDetailsViewModel::class.java)) { + "Unknown ViewModel class" + } + return PatientDetailsViewModel(application, fhirEngine, patientId) as T + } +} diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientListActivity.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientListActivity.kt deleted file mode 100644 index bfea362939..0000000000 --- a/reference/src/main/java/com/google/android/fhir/reference/PatientListActivity.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2020 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.reference - -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuInflater -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.menu.MenuBuilder -import androidx.appcompat.widget.SearchView -import androidx.appcompat.widget.Toolbar -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView -import androidx.work.Constraints -import com.google.android.fhir.FhirEngine -import com.google.android.fhir.reference.FhirApplication.Companion.fhirEngine -import com.google.android.fhir.reference.data.FhirPeriodicSyncWorker -import com.google.android.fhir.sync.PeriodicSyncConfiguration -import com.google.android.fhir.sync.RepeatInterval -import com.google.android.fhir.sync.Sync -import java.util.concurrent.TimeUnit - -/** An activity representing a list of Patients. */ -class PatientListActivity() : AppCompatActivity() { - private lateinit var fhirEngine: FhirEngine - private lateinit var patientListViewModel: PatientListViewModel - private lateinit var searchView: SearchView - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Log.d("PatientListActivity", "onCreate() called") - setContentView(R.layout.activity_patient_list) - - Sync.periodicSync( - this, - PeriodicSyncConfiguration( - syncConstraints = Constraints.Builder().build(), - repeat = RepeatInterval(interval = 1, timeUnit = TimeUnit.MINUTES) - ) - ) - - val toolbar = findViewById(R.id.toolbar) - setSupportActionBar(toolbar) - toolbar.title = title - - fhirEngine = fhirEngine(this) - - patientListViewModel = - ViewModelProvider( - this, - PatientListViewModel.PatientListViewModelFactory(this.application, fhirEngine) - ) - .get(PatientListViewModel::class.java) - val recyclerView: RecyclerView = findViewById(R.id.patient_list) - - val adapter = PatientItemRecyclerViewAdapter(this::onPatientItemClicked) - recyclerView.adapter = adapter - - patientListViewModel.liveSearchedPatients.observe( - this, - { - Log.d("PatientListActivity", "Submitting ${it.count()} patient records") - adapter.submitList(it) - } - ) - - patientListViewModel.patientCount.observe(this, { Log.d("PatientListActivity", "$it Patient") }) - - patientListViewModel.patientCount.observe( - this, - { findViewById(R.id.patient_count).text = " Patient(s)" } - ) - - searchView = findViewById(R.id.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 - } - } - ) - } - - // Click handler to help display the details about the patients from the list. - private fun onPatientItemClicked(patientItem: PatientListViewModel.PatientItem) { - val intent = - Intent(this.applicationContext, PatientDetailActivity::class.java).apply { - putExtra(PatientDetailFragment.ARG_ITEM_ID, patientItem.id) - } - this.startActivity(intent) - } - - // To suppress the warning. Seems to be an issue with androidx library. - // "MenuBuilder.setOptionalIconsVisible can only be called from within the same library group - // prefix (referenced groupId=androidx.appcompat with prefix androidx from groupId=fhir-engine" - @SuppressLint("RestrictedApi") - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - val inflater: MenuInflater = menuInflater - inflater.inflate(R.menu.list_options_menu, menu) - // To ensure that icons show up in the overflow options menu. Icons go missing without this. - if (menu is MenuBuilder) { - menu.setOptionalIconsVisible(true) - } - return true - } - - override fun onBackPressed() { - if (searchView.query.isNotEmpty()) searchView.setQuery("", true) else super.onBackPressed() - } -} diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientListFragment.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientListFragment.kt new file mode 100644 index 0000000000..03b51be33f --- /dev/null +++ b/reference/src/main/java/com/google/android/fhir/reference/PatientListFragment.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2020 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.reference + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.RecyclerView +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.reference.PatientListViewModel.PatientListViewModelFactory + +class PatientListFragment : Fragment() { + private lateinit var fhirEngine: FhirEngine + private lateinit var patientListViewModel: PatientListViewModel + private lateinit var searchView: SearchView + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_patient_list, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (requireActivity() as AppCompatActivity).supportActionBar?.apply { + title = requireActivity().title + setDisplayHomeAsUpEnabled(false) + } + fhirEngine = FhirApplication.fhirEngine(requireContext()) + patientListViewModel = + ViewModelProvider( + this, + PatientListViewModelFactory(requireActivity().application, fhirEngine) + ) + .get(PatientListViewModel::class.java) + val recyclerView: RecyclerView = view.findViewById(R.id.patient_list) + val adapter = PatientItemRecyclerViewAdapter(this::onPatientItemClicked) + recyclerView.adapter = adapter + + patientListViewModel.liveSearchedPatients.observe( + viewLifecycleOwner, + { + Log.d("PatientListActivity", "Submitting ${it.count()} patient records") + adapter.submitList(it) + } + ) + patientListViewModel.patientCount.observe( + viewLifecycleOwner, + { Log.d("PatientListActivity", "$it Patient") } + ) + + patientListViewModel.patientCount.observe( + viewLifecycleOwner, + { view.findViewById(R.id.patient_count).text = "$it Patient(s)" } + ) + searchView = view.findViewById(R.id.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 + } + } + ) + requireActivity() + .onBackPressedDispatcher + .addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (searchView.query.isNotEmpty()) { + searchView.setQuery("", true) + } else { + isEnabled = false + activity?.onBackPressed() + } + } + } + ) + } + + private fun onPatientItemClicked(patientItem: PatientListViewModel.PatientItem) { + findNavController() + .navigate(PatientListFragmentDirections.navigateToProductDetail(patientItem.resourceId)) + } +} diff --git a/reference/src/main/java/com/google/android/fhir/reference/PatientListViewModel.kt b/reference/src/main/java/com/google/android/fhir/reference/PatientListViewModel.kt index b8fd8f3757..61ef34a83e 100644 --- a/reference/src/main/java/com/google/android/fhir/reference/PatientListViewModel.kt +++ b/reference/src/main/java/com/google/android/fhir/reference/PatientListViewModel.kt @@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import com.google.android.fhir.FhirEngine -import com.google.android.fhir.reference.data.SamplePatients import com.google.android.fhir.search.Order import com.google.android.fhir.search.StringFilterModifier import com.google.android.fhir.search.count @@ -39,7 +38,6 @@ import org.hl7.fhir.r4.model.Patient class PatientListViewModel(application: Application, private val fhirEngine: FhirEngine) : AndroidViewModel(application) { - private val samplePatients = SamplePatients() val liveSearchedPatients = MutableLiveData>() val patientCount = liveData { emit(count()) } @@ -65,8 +63,9 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi } private suspend fun getSearchResults(nameQuery: String = ""): List { - val searchResults: List = - fhirEngine.search { + val patients: MutableList = mutableListOf() + fhirEngine + .search { if (nameQuery.isNotEmpty()) filter(Patient.NAME) { modifier = StringFilterModifier.CONTAINS @@ -76,7 +75,10 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi count = 100 from = 0 } - return samplePatients.getPatientItems(searchResults) + .take(MAX_RESOURCE_COUNT) + .mapIndexed { index, fhirPatient -> fhirPatient.toPatientItem(index + 1) } + .let { patients.addAll(it) } + return patients } /** The Patient's details for display purposes. */ @@ -114,3 +116,23 @@ class PatientListViewModel(application: Application, private val fhirEngine: Fhi } } } + +internal fun Patient.toPatientItem(position: Int): PatientListViewModel.PatientItem { + val name = name[0].nameAsSingleString + + // Show nothing if no values available for gender and date of birth. + val gender = if (hasGenderElement()) genderElement.valueAsString else "" + val dob = if (hasBirthDateElement()) birthDateElement.valueAsString else "" + val html: String = if (hasText()) text.div.valueAsString else "" + val phone: String = if (hasTelecom()) telecom[0].value else "" + + return PatientListViewModel.PatientItem( + position.toString(), + name, + gender, + dob, + html, + phone, + idElement.idPart + ) +} diff --git a/reference/src/main/java/com/google/android/fhir/reference/data/SamplePatients.kt b/reference/src/main/java/com/google/android/fhir/reference/data/SamplePatients.kt deleted file mode 100644 index ffe43c2c88..0000000000 --- a/reference/src/main/java/com/google/android/fhir/reference/data/SamplePatients.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2020 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.reference.data - -import ca.uhn.fhir.context.FhirContext -import ca.uhn.fhir.parser.IParser -import com.google.android.fhir.reference.PatientListViewModel -import org.hl7.fhir.r4.model.Bundle -import org.hl7.fhir.r4.model.Observation -import org.hl7.fhir.r4.model.Patient - -private const val MAX_RESOURCE_COUNT = 20 - -/** - * Helper class for loading a list of sample Fhir Patient Resource objects. - * - * Parses and loads Patient data from the passed JSON String into items that could be used by - * PatientListViewModel. - */ -class SamplePatients { - // The resource bundle with Patient objects. - private var fhirBundle: Bundle? = null - - companion object { - val tag = "SamplePatient" - - val fhirJsonParser: IParser = FhirContext.forR4().newJsonParser() - } - /** Returns list of PatientItem objects based on patients from the json string. */ - fun getPatientItems(jsonString: String): List { - val patients: MutableList = mutableListOf() - - fhirBundle = fhirJsonParser.parseResource(Bundle::class.java, jsonString) as Bundle - - // Create a list of PatientItems from fhirPatients. The display index is 1 based. - fhirBundle - ?.entry - ?.take(MAX_RESOURCE_COUNT) - ?.mapIndexed { index, entry -> createPatientItem(index + 1, entry.resource as Patient) } - ?.let { patients.addAll(it) } - - return patients - } - - fun getPatientItems(fhirPatients: List): List { - val patients: MutableList = mutableListOf() - - // Create a list of PatientItems from fhirPatients. The display index is 1 based. - fhirPatients - .take(MAX_RESOURCE_COUNT) - ?.mapIndexed { index, fhirPatient -> createPatientItem(index + 1, fhirPatient) } - ?.let { patients.addAll(it) } - - // Return a cloned List - return patients - } - - /** Creates PatientItem objects with displayable values from the Fhir Patient objects. */ - private fun createPatientItem(position: Int, patient: Patient): PatientListViewModel.PatientItem { - val name = patient.name[0].nameAsSingleString - - // Show nothing if no values available for gender and date of birth. - val gender = if (patient.hasGenderElement()) patient.genderElement.valueAsString else "" - val dob = if (patient.hasBirthDateElement()) patient.birthDateElement.valueAsString else "" - val html: String = if (patient.hasText()) patient.text.div.valueAsString else "" - val phone: String = if (patient.hasTelecom()) patient.telecom[0].value else "" - - return PatientListViewModel.PatientItem( - position.toString(), - name, - gender, - dob, - html, - phone, - patient.idElement.idPart - ) - } - - /** Returns list of ObservationItem objects based on observations from the json string. */ - fun getObservationItems(jsonString: String): MutableList { - val observations = ArrayList() - - fhirBundle = fhirJsonParser.parseResource(Bundle::class.java, jsonString) as Bundle - - // Create a list of ObservationItems from fhirObservations. The display index is 1 based. - fhirBundle - ?.entry - ?.take(MAX_RESOURCE_COUNT) - ?.mapIndexed { index, entry -> - createObservationItem(index + 1, entry.resource as Observation) - } - ?.let { observations.addAll(it) } - - return observations - } - - /** Creates ObservationItem objects with displayable values from the Fhir Observation objects. */ - private fun createObservationItem( - position: Int, - observation: Observation - ): PatientListViewModel.ObservationItem { - // val observation: Observation = getObservationDetails(position) - val observationCode = observation.code.text - - // Show nothing if no values available for datetime and value quantity. - val dateTimeStr = - if (observation.hasEffectiveDateTimeType()) observation.effectiveDateTimeType.asStringValue() - else "No effective DateTime" - val value = - if (observation.hasValueQuantity()) observation.valueQuantity.value.toString() - else "No ValueQuantity" - val valueUnit = if (observation.hasValueQuantity()) observation.valueQuantity.unit else "" - val valueStr = "$value $valueUnit" - - return PatientListViewModel.ObservationItem( - position.toString(), - observationCode, - dateTimeStr, - valueStr - ) - } -} diff --git a/reference/src/main/res/layout/activity_patient_list.xml b/reference/src/main/res/layout/activity_main.xml similarity index 66% rename from reference/src/main/res/layout/activity_patient_list.xml rename to reference/src/main/res/layout/activity_main.xml index f157a17730..3b34cc8dcf 100644 --- a/reference/src/main/res/layout/activity_patient_list.xml +++ b/reference/src/main/res/layout/activity_main.xml @@ -21,7 +21,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" - tools:context=".PatientListActivity" + tools:context=".MainActivity" > - - - - - - diff --git a/reference/src/main/res/layout/activity_patient_detail.xml b/reference/src/main/res/layout/activity_patient_detail.xml deleted file mode 100644 index f1edc07b9c..0000000000 --- a/reference/src/main/res/layout/activity_patient_detail.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/reference/src/main/res/layout/fragment_patient_list.xml b/reference/src/main/res/layout/fragment_patient_list.xml new file mode 100644 index 0000000000..34499ff288 --- /dev/null +++ b/reference/src/main/res/layout/fragment_patient_list.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/reference/src/main/res/layout/observation_list.xml b/reference/src/main/res/layout/observation_list.xml index 9e7e21a16d..2eb2207bcf 100644 --- a/reference/src/main/res/layout/observation_list.xml +++ b/reference/src/main/res/layout/observation_list.xml @@ -25,6 +25,6 @@ android:layout_marginLeft="16dp" android:layout_marginRight="16dp" app:layoutManager="LinearLayoutManager" - tools:context=".PatientListActivity" + tools:context=".MainActivity" tools:listitem="@layout/observation_list_item" /> diff --git a/reference/src/main/res/layout/patient_detail.xml b/reference/src/main/res/layout/patient_detail.xml index 72fa105d0c..be15d3a725 100644 --- a/reference/src/main/res/layout/patient_detail.xml +++ b/reference/src/main/res/layout/patient_detail.xml @@ -32,10 +32,8 @@ android:padding="16dp" > diff --git a/reference/src/main/res/layout/patient_list_item.xml b/reference/src/main/res/layout/patient_list_item.xml index 9304cee35e..25e94665b2 100644 --- a/reference/src/main/res/layout/patient_list_item.xml +++ b/reference/src/main/res/layout/patient_list_item.xml @@ -24,9 +24,8 @@ android:id="@+id/id_patient_number" style="@android:style/TextAppearance.Large" android:layout_width="wrap_content" - android:layout_height="fill_parent" - android:layout_alignParentTop="true" - android:layout_alignParentBottom="true" + android:layout_height="wrap_content" + android:layout_centerVertical="true" android:layout_marginStart="@dimen/text_margin" android:layout_marginTop="@dimen/text_margin" android:layout_marginEnd="@dimen/text_margin" @@ -55,7 +54,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/name" - android:layout_alignParentBottom="true" android:layout_marginStart="@dimen/small_text_margin" android:layout_marginTop="@dimen/small_text_margin" android:layout_marginEnd="@dimen/text_margin" @@ -72,7 +70,6 @@ android:layout_height="wrap_content" android:layout_below="@id/name" android:layout_alignParentEnd="true" - android:layout_alignParentBottom="true" android:layout_marginStart="@dimen/text_margin" android:layout_marginTop="@dimen/small_text_margin" android:layout_marginEnd="@dimen/small_text_margin" diff --git a/reference/src/main/res/navigation/reference_nav_graph.xml b/reference/src/main/res/navigation/reference_nav_graph.xml new file mode 100644 index 0000000000..8a1ed35f25 --- /dev/null +++ b/reference/src/main/res/navigation/reference_nav_graph.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/reference/src/main/res/values/strings.xml b/reference/src/main/res/values/strings.xml index a27660396c..821115494b 100644 --- a/reference/src/main/res/values/strings.xml +++ b/reference/src/main/res/values/strings.xml @@ -23,4 +23,5 @@ %1$s: %2$s\nEffective: %3$s + Find by Patient Name