Skip to content

Commit 2a7e91f

Browse files
santosh-pingleSantosh Pingle
and
Santosh Pingle
authored
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 <[email protected]>
1 parent f343e26 commit 2a7e91f

File tree

9 files changed

+847
-14
lines changed

9 files changed

+847
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/*
2+
* Copyright 2024-2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.demo
18+
19+
import android.os.Bundle
20+
import android.view.LayoutInflater
21+
import android.view.MenuItem
22+
import android.view.View
23+
import android.view.ViewGroup
24+
import android.widget.Button
25+
import android.widget.CheckBox
26+
import android.widget.EditText
27+
import android.widget.RadioGroup
28+
import android.widget.Toast
29+
import androidx.appcompat.app.AppCompatActivity
30+
import androidx.fragment.app.Fragment
31+
import androidx.fragment.app.viewModels
32+
import androidx.lifecycle.Lifecycle
33+
import androidx.lifecycle.lifecycleScope
34+
import androidx.lifecycle.repeatOnLifecycle
35+
import androidx.navigation.fragment.NavHostFragment
36+
import com.google.android.fhir.demo.helpers.PatientCreationHelper
37+
import com.google.android.material.tabs.TabLayout
38+
import kotlinx.coroutines.launch
39+
import org.hl7.fhir.r4.model.Enumerations
40+
41+
class CrudOperationFragment : Fragment() {
42+
private val crudOperationViewModel: CrudOperationViewModel by viewModels()
43+
44+
override fun onCreateView(
45+
inflater: LayoutInflater,
46+
container: ViewGroup?,
47+
savedInstanceState: Bundle?,
48+
): View {
49+
return inflater.inflate(R.layout.fragment_crud_layout, container, false)
50+
}
51+
52+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
53+
super.onViewCreated(view, savedInstanceState)
54+
setUpActionBar()
55+
setHasOptionsMenu(true)
56+
setupUiOnScreenLaunch()
57+
viewLifecycleOwner.lifecycleScope.launch {
58+
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
59+
crudOperationViewModel.patientUiState.collect { patientUiState ->
60+
patientUiState?.let {
61+
when (it.operationType) {
62+
OperationType.CREATE -> {
63+
Toast.makeText(requireContext(), "Patient is saved", Toast.LENGTH_SHORT).show()
64+
}
65+
OperationType.READ -> displayPatientDetails(it)
66+
OperationType.UPDATE -> {
67+
Toast.makeText(requireContext(), "Patient is updated", Toast.LENGTH_SHORT).show()
68+
}
69+
OperationType.DELETE -> {
70+
// Reset the page as the patient has been deleted.
71+
clearUiFieldValues()
72+
configureFieldsForOperation(OperationType.DELETE)
73+
Toast.makeText(requireContext(), "Patient is deleted", Toast.LENGTH_SHORT).show()
74+
}
75+
}
76+
}
77+
}
78+
}
79+
}
80+
}
81+
82+
override fun onOptionsItemSelected(item: MenuItem): Boolean {
83+
return when (item.itemId) {
84+
android.R.id.home -> {
85+
NavHostFragment.findNavController(this).navigateUp()
86+
true
87+
}
88+
else -> false
89+
}
90+
}
91+
92+
private fun setUpActionBar() {
93+
(requireActivity() as AppCompatActivity).supportActionBar?.apply {
94+
title = requireContext().getString(R.string.crud_operations)
95+
setDisplayHomeAsUpEnabled(true)
96+
}
97+
}
98+
99+
private fun setupUiOnScreenLaunch() {
100+
setupTabLayoutChangeListener()
101+
selectTab(TAB_CREATE)
102+
setupUiForCrudOperation(OperationType.CREATE)
103+
104+
requireView().findViewById<Button>(R.id.btnSubmit).setOnClickListener {
105+
val currentOperationType =
106+
getOperationTypeByTabPosition(
107+
requireView().findViewById<TabLayout>(R.id.tabLayoutCrud).selectedTabPosition,
108+
)
109+
when (currentOperationType) {
110+
OperationType.CREATE -> createPatient()
111+
OperationType.READ -> {
112+
isPatientCreationRequired()
113+
crudOperationViewModel.readPatientById()
114+
}
115+
OperationType.UPDATE -> updatePatient()
116+
OperationType.DELETE -> deletePatient()
117+
}
118+
}
119+
}
120+
121+
private fun setupUiForCrudOperation(operationType: OperationType) {
122+
when (operationType) {
123+
OperationType.CREATE -> {
124+
clearUiFieldValues()
125+
setPatientIdUiFieldText(PatientCreationHelper.createPatientId())
126+
updateSubmitButtonText(getString(R.string.create))
127+
}
128+
OperationType.READ -> {
129+
clearUiFieldValues()
130+
setPatientIdUiFieldText(crudOperationViewModel.currentPatientLogicalId)
131+
updateSubmitButtonText(getString(R.string.read))
132+
}
133+
OperationType.UPDATE -> {
134+
updateSubmitButtonText(getString(R.string.update))
135+
crudOperationViewModel.readPatientById()
136+
}
137+
OperationType.DELETE -> {
138+
updateSubmitButtonText(getString(R.string.delete))
139+
crudOperationViewModel.readPatientById()
140+
}
141+
}
142+
configureFieldsForOperation(operationType)
143+
}
144+
145+
private fun updateSubmitButtonText(text: String) {
146+
requireView().findViewById<Button>(R.id.btnSubmit).text = text
147+
}
148+
149+
private fun setPatientIdUiFieldText(patientId: String?) {
150+
requireView().findViewById<EditText>(R.id.etId).setText(patientId)
151+
}
152+
153+
private fun selectTab(position: Int) {
154+
val tabLayout = requireView().findViewById<TabLayout>(R.id.tabLayoutCrud)
155+
val tab = tabLayout.getTabAt(position)
156+
tab?.select()
157+
}
158+
159+
private fun setupTabLayoutChangeListener() {
160+
val tabLayout = requireView().findViewById<TabLayout>(R.id.tabLayoutCrud)
161+
tabLayout.addOnTabSelectedListener(
162+
object : TabLayout.OnTabSelectedListener {
163+
override fun onTabSelected(tab: TabLayout.Tab?) {
164+
val currentOperationType = getOperationTypeByTabPosition(tab?.position ?: TAB_CREATE)
165+
setupUiForCrudOperation(currentOperationType)
166+
}
167+
168+
override fun onTabUnselected(tab: TabLayout.Tab?) {}
169+
170+
override fun onTabReselected(tab: TabLayout.Tab?) {}
171+
},
172+
)
173+
}
174+
175+
private fun clearUiFieldValues() {
176+
val editTexts =
177+
listOf(
178+
R.id.etId,
179+
R.id.etFirstName,
180+
R.id.etLastName,
181+
R.id.etBirthDate,
182+
)
183+
editTexts.forEach { editTextId ->
184+
requireView().findViewById<EditText>(editTextId).apply {
185+
text.clear()
186+
clearFocus()
187+
}
188+
}
189+
requireView().findViewById<RadioGroup>(R.id.radioGroupGender).clearCheck()
190+
requireView().findViewById<CheckBox>(R.id.checkBoxActive).isChecked = false
191+
}
192+
193+
private fun configureFieldsForOperation(operationType: OperationType) {
194+
val isEditable =
195+
when (operationType) {
196+
OperationType.CREATE,
197+
OperationType.UPDATE, -> true
198+
else -> false
199+
}
200+
val views =
201+
listOf(
202+
R.id.etFirstName,
203+
R.id.etLastName,
204+
R.id.etBirthDate,
205+
R.id.radioGroupGender,
206+
R.id.rbMale,
207+
R.id.rbFemale,
208+
R.id.rbOther,
209+
R.id.checkBoxActive,
210+
)
211+
views.forEach { viewId -> requireView().findViewById<View>(viewId).isEnabled = isEditable }
212+
}
213+
214+
private fun displayPatientDetails(patientUiState: PatientUiState) {
215+
setPatientIdUiFieldText(patientUiState.patientId)
216+
requireView().findViewById<EditText>(R.id.etFirstName).apply {
217+
setText(patientUiState.firstName)
218+
}
219+
requireView().findViewById<EditText>(R.id.etLastName).apply {
220+
setText(patientUiState.lastName ?: "")
221+
}
222+
requireView().findViewById<EditText>(R.id.etBirthDate).apply {
223+
setText(patientUiState.birthDate ?: "")
224+
}
225+
226+
val radioGroupGender = requireView().findViewById<RadioGroup>(R.id.radioGroupGender)
227+
radioGroupGender.clearCheck()
228+
val genderRadioButtonId =
229+
when (patientUiState.gender) {
230+
Enumerations.AdministrativeGender.MALE -> R.id.rbMale
231+
Enumerations.AdministrativeGender.FEMALE -> R.id.rbFemale
232+
Enumerations.AdministrativeGender.OTHER -> R.id.rbOther
233+
else -> null
234+
}
235+
genderRadioButtonId?.let { radioGroupGender.check(it) }
236+
237+
requireView().findViewById<CheckBox>(R.id.checkBoxActive).apply {
238+
isChecked = patientUiState.isActive
239+
}
240+
}
241+
242+
private fun isPatientCreationRequired(): Boolean {
243+
if (crudOperationViewModel.currentPatientLogicalId.isNullOrEmpty()) {
244+
Toast.makeText(requireContext(), "Please create a patient first.", Toast.LENGTH_SHORT).show()
245+
return true
246+
}
247+
return false
248+
}
249+
250+
private fun getOperationTypeByTabPosition(tabPosition: Int): OperationType {
251+
return when (tabPosition) {
252+
TAB_CREATE -> {
253+
OperationType.CREATE
254+
}
255+
TAB_READ -> {
256+
OperationType.READ
257+
}
258+
TAB_UPDATE -> {
259+
OperationType.UPDATE
260+
}
261+
TAB_DELETE -> {
262+
OperationType.DELETE
263+
}
264+
else -> {
265+
error("Invalid tab selection.")
266+
}
267+
}
268+
}
269+
270+
private fun getPatientInput(): PatientInput? {
271+
val firstName = requireView().findViewById<EditText>(R.id.etFirstName).text.toString().trim()
272+
if (firstName.isBlank()) {
273+
Toast.makeText(requireContext(), "First name is required.", Toast.LENGTH_SHORT).show()
274+
return null
275+
}
276+
val lastName = requireView().findViewById<EditText>(R.id.etLastName).text.toString().trim()
277+
val birthDate = requireView().findViewById<EditText>(R.id.etBirthDate).text.toString().trim()
278+
if (birthDate.isNotEmpty() && !crudOperationViewModel.isBirthDateValid(birthDate)) {
279+
Toast.makeText(requireContext(), "Please enter a valid birth date.", Toast.LENGTH_SHORT)
280+
.show()
281+
return null
282+
}
283+
val selectedGenderId =
284+
requireView().findViewById<RadioGroup>(R.id.radioGroupGender).checkedRadioButtonId
285+
val gender =
286+
when (selectedGenderId) {
287+
R.id.rbMale -> Enumerations.AdministrativeGender.MALE
288+
R.id.rbFemale -> Enumerations.AdministrativeGender.FEMALE
289+
R.id.rbOther -> Enumerations.AdministrativeGender.OTHER
290+
else -> null
291+
}
292+
val isActive = requireView().findViewById<CheckBox>(R.id.checkBoxActive).isChecked
293+
294+
return PatientInput(firstName, lastName, birthDate, gender, isActive)
295+
}
296+
297+
private fun createPatient() {
298+
getPatientInput()?.let {
299+
crudOperationViewModel.createPatient(
300+
patientId = requireView().findViewById<EditText>(R.id.etId).text.toString(),
301+
firstName = it.firstName,
302+
lastName = it.lastName,
303+
birthDate = it.birthDate,
304+
gender = it.gender,
305+
isActive = it.isActive,
306+
)
307+
}
308+
}
309+
310+
private fun updatePatient() {
311+
if (isPatientCreationRequired()) {
312+
return
313+
}
314+
getPatientInput()?.let {
315+
crudOperationViewModel.updatePatient(
316+
firstName = it.firstName,
317+
lastName = it.lastName,
318+
birthDate = it.birthDate,
319+
gender = it.gender,
320+
isActive = it.isActive,
321+
)
322+
}
323+
}
324+
325+
private fun deletePatient() {
326+
if (isPatientCreationRequired()) {
327+
return
328+
}
329+
crudOperationViewModel.currentPatientLogicalId?.let { crudOperationViewModel.deletePatient() }
330+
}
331+
332+
companion object {
333+
private const val TAB_CREATE = 0
334+
private const val TAB_READ = 1
335+
private const val TAB_UPDATE = 2
336+
private const val TAB_DELETE = 3
337+
}
338+
}

0 commit comments

Comments
 (0)