Skip to content

Commit 1c39a33

Browse files
siddh1004Siddharth Agarwal
andauthored
Migrate OverdueScreen patient list to Jetpack Compose (#5442)
https://app.shortcut.com/simpledotorg/story/13681/migrate-overduescreen-to-compose?team_id=1&iteration_ids=15732 --------- Co-authored-by: Siddharth Agarwal <[email protected]>
1 parent 7b840f2 commit 1c39a33

15 files changed

+302
-429
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Add Compose components for Overdue screen
1212
- Migrate medicines summary view to Jetpack Compose
1313
- Migrate codeclimate config to qlty.sh config
14+
- Migrate `OverdueScreen` patient list to Jetpack Compose
1415

1516
## 2025.05.20
1617

app/src/main/java/org/simple/clinic/home/overdue/OverdueScreen.kt

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import android.view.LayoutInflater
77
import android.view.View
88
import android.view.ViewGroup
99
import androidx.appcompat.app.AppCompatActivity
10-
import androidx.recyclerview.widget.LinearLayoutManager
10+
import androidx.compose.runtime.getValue
11+
import androidx.compose.runtime.mutableStateOf
12+
import androidx.compose.runtime.setValue
13+
import androidx.compose.ui.platform.ViewCompositionStrategy
1114
import com.f2prateek.rx.preferences2.Preference
1215
import com.google.android.material.dialog.MaterialAlertDialogBuilder
1316
import com.jakewharton.rxbinding3.view.clicks
@@ -17,25 +20,23 @@ import io.reactivex.Observable
1720
import io.reactivex.disposables.CompositeDisposable
1821
import io.reactivex.rxkotlin.cast
1922
import io.reactivex.rxkotlin.ofType
23+
import io.reactivex.subjects.PublishSubject
2024
import kotlinx.parcelize.Parcelize
2125
import org.simple.clinic.R
2226
import org.simple.clinic.ReportAnalyticsEvents
2327
import org.simple.clinic.activity.permissions.RequestPermissions
2428
import org.simple.clinic.activity.permissions.RuntimePermissions
2529
import org.simple.clinic.appconfig.Country
2630
import org.simple.clinic.contactpatient.ContactPatientBottomSheet
27-
import org.simple.clinic.databinding.ListItemDividerBinding
28-
import org.simple.clinic.databinding.ListItemNoPendingPatientsBinding
29-
import org.simple.clinic.databinding.ListItemOverdueListSectionHeaderBinding
30-
import org.simple.clinic.databinding.ListItemOverduePatientBinding
31-
import org.simple.clinic.databinding.ListItemOverduePendingListFooterBinding
32-
import org.simple.clinic.databinding.ListItemSearchOverduePatientButtonBinding
3331
import org.simple.clinic.databinding.ScreenOverdueBinding
3432
import org.simple.clinic.di.injector
3533
import org.simple.clinic.feature.Feature.OverdueInstantSearch
3634
import org.simple.clinic.feature.Feature.PatientReassignment
3735
import org.simple.clinic.feature.Features
3836
import org.simple.clinic.home.HomeScreen
37+
import org.simple.clinic.home.overdue.compose.OverdueAppointmentListItem
38+
import org.simple.clinic.home.overdue.compose.OverdueUiModel
39+
import org.simple.clinic.home.overdue.compose.OverdueUiModelMapper
3940
import org.simple.clinic.home.overdue.search.OverdueSearchScreen
4041
import org.simple.clinic.navigation.v2.Router
4142
import org.simple.clinic.navigation.v2.ScreenKey
@@ -54,7 +55,6 @@ import org.simple.clinic.util.UserClock
5455
import org.simple.clinic.util.UtcClock
5556
import org.simple.clinic.util.applyInsetsBottomPadding
5657
import org.simple.clinic.util.unsafeLazy
57-
import org.simple.clinic.widgets.ItemAdapter
5858
import org.simple.clinic.widgets.UiEvent
5959
import java.time.Instant
6060
import java.time.LocalDate
@@ -115,37 +115,13 @@ class OverdueScreen : BaseScreen<
115115
@Inject
116116
lateinit var locale: Locale
117117

118-
private val overdueListAdapter = ItemAdapter(
119-
diffCallback = OverdueAppointmentListItem.DiffCallback(),
120-
bindings = mapOf(
121-
R.layout.list_item_overdue_patient to { layoutInflater, parent ->
122-
ListItemOverduePatientBinding.inflate(layoutInflater, parent, false)
123-
},
124-
R.layout.list_item_overdue_list_section_header to { layoutInflater, parent ->
125-
ListItemOverdueListSectionHeaderBinding.inflate(layoutInflater, parent, false)
126-
},
127-
R.layout.list_item_overdue_pending_list_footer to { layoutInflater, parent ->
128-
ListItemOverduePendingListFooterBinding.inflate(layoutInflater, parent, false)
129-
},
130-
R.layout.list_item_no_pending_patients to { layoutInflater, parent ->
131-
ListItemNoPendingPatientsBinding.inflate(layoutInflater, parent, false)
132-
},
133-
R.layout.list_item_divider to { layoutInflater, parent ->
134-
ListItemDividerBinding.inflate(layoutInflater, parent, false)
135-
},
136-
R.layout.list_item_search_overdue_patient_button to { layoutInflater, parent ->
137-
ListItemSearchOverduePatientButtonBinding.inflate(layoutInflater, parent, false)
138-
}
139-
)
140-
)
141-
142118
private val disposable = CompositeDisposable()
143119

144120
private val viewForEmptyList
145121
get() = binding.viewForEmptyList
146122

147-
private val overdueRecyclerView
148-
get() = binding.overdueRecyclerView
123+
private val composeView
124+
get() = binding.composeView
149125

150126
private val overdueProgressBar
151127
get() = binding.overdueProgressBar
@@ -172,18 +148,22 @@ class OverdueScreen : BaseScreen<
172148
country.isoCountryCode == Country.INDIA
173149
}
174150

151+
private var uiModelsState by mutableStateOf<List<OverdueUiModel>>(emptyList())
152+
175153
override fun defaultModel() = OverdueModel.create()
176154

177155
override fun bindView(layoutInflater: LayoutInflater, container: ViewGroup?) =
178156
ScreenOverdueBinding.inflate(layoutInflater, container, false)
179157

180158
override fun uiRenderer() = OverdueUiRenderer(ui = this)
181159

160+
private val composeUiEvents = PublishSubject.create<OverdueEvent>()
161+
182162
override fun events() = Observable.mergeArray(
183-
overdueListAdapter.itemEvents,
184163
downloadOverdueListClicks(),
185164
shareOverdueListClicks(),
186-
clearSelectedOverdueAppointmentClicks()
165+
clearSelectedOverdueAppointmentClicks(),
166+
composeUiEvents,
187167
)
188168
.compose(RequestPermissions(runtimePermissions, screenResults.streamResults().ofType()))
189169
.compose(runtimeNetworkStatus::apply)
@@ -218,12 +198,38 @@ class OverdueScreen : BaseScreen<
218198

219199
buttonsFrame.applyInsetsBottomPadding()
220200

221-
overdueRecyclerView.adapter = overdueListAdapter
222-
overdueRecyclerView.layoutManager = LinearLayoutManager(context)
201+
composeView.apply {
202+
setViewCompositionStrategy(
203+
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
204+
)
205+
setContent {
206+
OverdueAppointmentListItem(
207+
uiModels = uiModelsState,
208+
onCallClicked = { patientId ->
209+
composeUiEvents.onNext(CallPatientClicked(patientId))
210+
},
211+
onRowClicked = { patientId ->
212+
composeUiEvents.onNext(OverduePatientClicked(patientId))
213+
},
214+
onCheckboxClicked = { appointmentUuid ->
215+
composeUiEvents.onNext(OverdueAppointmentCheckBoxClicked(appointmentUuid))
216+
},
217+
onSearch = {
218+
composeUiEvents.onNext(OverdueSearchButtonClicked)
219+
},
220+
onSectionHeaderClick = { overdueAppointmentSectionTitle ->
221+
composeUiEvents.onNext(ChevronClicked(overdueAppointmentSectionTitle))
222+
},
223+
onSectionFooterClick = {
224+
composeUiEvents.onNext(PendingListFooterClicked)
225+
}
226+
)
227+
}
228+
}
223229
}
224230

231+
225232
override fun onDestroyView() {
226-
overdueRecyclerView.adapter = null
227233
disposable.clear()
228234
super.onDestroyView()
229235
}
@@ -267,7 +273,7 @@ class OverdueScreen : BaseScreen<
267273
selectedOverdueAppointments: Set<UUID>,
268274
overdueListSectionStates: OverdueListSectionStates
269275
) {
270-
overdueListAdapter.submitList(OverdueAppointmentListItem.from(
276+
uiModelsState = OverdueUiModelMapper.from(
271277
overdueAppointmentSections = overdueAppointmentSections,
272278
clock = userClock,
273279
pendingListDefaultStateSize = pendingAppointmentsConfig.pendingListDefaultStateSize,
@@ -277,7 +283,8 @@ class OverdueScreen : BaseScreen<
277283
selectedOverdueAppointments = selectedOverdueAppointments,
278284
isPatientReassignmentFeatureEnabled = features.isEnabled(PatientReassignment),
279285
locale = locale,
280-
))
286+
)
287+
281288
if (isOverdueListDownloadAndShareEnabled) {
282289
buttonsFrame.visibility = View.VISIBLE
283290
}
@@ -316,11 +323,11 @@ class OverdueScreen : BaseScreen<
316323
}
317324

318325
override fun showOverdueRecyclerView() {
319-
overdueRecyclerView.visibility = View.VISIBLE
326+
composeView.visibility = View.VISIBLE
320327
}
321328

322329
override fun hideOverdueRecyclerView() {
323-
overdueRecyclerView.visibility = View.GONE
330+
composeView.visibility = View.GONE
324331
}
325332

326333
override fun openOverdueSearch() {

app/src/main/java/org/simple/clinic/home/overdue/compose/NoPendingPatients.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.material.Text
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.ui.Alignment
1111
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.res.dimensionResource
1213
import androidx.compose.ui.res.painterResource
1314
import androidx.compose.ui.res.stringResource
1415
import androidx.compose.ui.tooling.preview.Preview
@@ -21,11 +22,15 @@ fun NoPendingPatients(modifier: Modifier = Modifier) {
2122
Card(
2223
modifier = modifier
2324
.fillMaxWidth()
25+
.padding(8.dp)
2426
) {
2527
Column(
2628
modifier = Modifier
2729
.fillMaxWidth()
28-
.padding(vertical = 40.dp, horizontal = 16.dp),
30+
.padding(
31+
vertical = dimensionResource(R.dimen.spacing_40),
32+
horizontal = dimensionResource(R.dimen.spacing_16)
33+
),
2934
horizontalAlignment = Alignment.CenterHorizontally
3035
) {
3136
Image(
@@ -34,7 +39,7 @@ fun NoPendingPatients(modifier: Modifier = Modifier) {
3439
)
3540

3641
Text(
37-
modifier = Modifier.padding(top = 16.dp),
42+
modifier = Modifier.padding(top = dimensionResource(R.dimen.spacing_16)),
3843
text = stringResource(id = R.string.overdue_no_pending_patients),
3944
style = SimpleTheme.typography.body0Medium,
4045
color = SimpleTheme.colors.material.secondary
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package org.simple.clinic.home.overdue.compose
2+
3+
import androidx.compose.foundation.layout.fillMaxWidth
4+
import androidx.compose.foundation.layout.padding
5+
import androidx.compose.foundation.lazy.LazyColumn
6+
import androidx.compose.foundation.lazy.items
7+
import androidx.compose.material.Divider
8+
import androidx.compose.material.Icon
9+
import androidx.compose.material.ProvideTextStyle
10+
import androidx.compose.material.Text
11+
import androidx.compose.material.icons.Icons
12+
import androidx.compose.material.icons.outlined.Search
13+
import androidx.compose.runtime.Composable
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.res.colorResource
16+
import androidx.compose.ui.res.dimensionResource
17+
import androidx.compose.ui.res.stringResource
18+
import org.simple.clinic.R
19+
import org.simple.clinic.common.ui.components.ButtonSize
20+
import org.simple.clinic.common.ui.components.OutlinedButton
21+
import org.simple.clinic.common.ui.theme.SimpleTheme
22+
import org.simple.clinic.home.overdue.OverdueAppointmentSectionTitle
23+
import java.util.UUID
24+
25+
@Composable
26+
fun OverdueAppointmentListItem(
27+
uiModels: List<OverdueUiModel>,
28+
onCallClicked: (UUID) -> Unit,
29+
onRowClicked: (UUID) -> Unit,
30+
onCheckboxClicked: (UUID) -> Unit,
31+
onSectionHeaderClick: (OverdueAppointmentSectionTitle) -> Unit,
32+
onSearch: () -> Unit,
33+
onSectionFooterClick: () -> Unit,
34+
35+
) {
36+
SimpleTheme {
37+
LazyColumn(
38+
modifier = Modifier.padding(
39+
start = dimensionResource(R.dimen.spacing_8),
40+
end = dimensionResource(R.dimen.spacing_8),
41+
top = dimensionResource(R.dimen.spacing_8),
42+
bottom = dimensionResource(R.dimen.spacing_128)
43+
)
44+
) {
45+
items(uiModels) { model ->
46+
when (model) {
47+
is OverdueUiModel.Patient -> {
48+
OverduePatientListItem(
49+
modifier = Modifier.padding(bottom = dimensionResource(R.dimen.spacing_8)),
50+
appointmentUuid = model.appointmentUuid,
51+
patientUuid = model.patientUuid,
52+
name = model.name,
53+
gender = model.gender,
54+
age = model.age,
55+
phoneNumber = model.phoneNumber,
56+
overdueDays = model.overdueDays,
57+
villageName = model.villageName,
58+
isOverdueSelectAndDownloadEnabled = model.isOverdueSelectAndDownloadEnabled,
59+
isAppointmentSelected = model.isAppointmentSelected,
60+
isEligibleForReassignment = model.isEligibleForReassignment,
61+
onCallClicked = onCallClicked,
62+
onRowClicked = onRowClicked,
63+
onCheckboxClicked = onCheckboxClicked
64+
)
65+
}
66+
67+
is OverdueUiModel.Header -> {
68+
OverdueSectionHeader(
69+
headerTextRes = model.headerTextRes,
70+
count = model.count,
71+
isExpanded = model.isOverdueSectionHeaderExpanded,
72+
overdueAppointmentSectionTitle = model.overdueAppointmentSectionTitle,
73+
locale = model.locale,
74+
onClick = onSectionHeaderClick
75+
)
76+
}
77+
78+
is OverdueUiModel.Footer -> {
79+
OverdueSectionFooter(
80+
pendingListState = model.pendingListState,
81+
onClick = onSectionFooterClick
82+
)
83+
}
84+
85+
is OverdueUiModel.Divider -> {
86+
Divider(
87+
color = colorResource(R.color.color_on_surface_11),
88+
modifier = Modifier.padding(dimensionResource(R.dimen.spacing_8))
89+
)
90+
}
91+
92+
is OverdueUiModel.NoPendingPatients -> {
93+
NoPendingPatients()
94+
}
95+
96+
is OverdueUiModel.SearchButton -> {
97+
OutlinedButton(
98+
modifier = Modifier
99+
.fillMaxWidth(),
100+
buttonSize = ButtonSize.Big,
101+
icon = {
102+
Icon(
103+
imageVector = Icons.Outlined.Search,
104+
contentDescription = "Search Overdue Patient"
105+
)
106+
},
107+
onClick = onSearch,
108+
) {
109+
ProvideTextStyle(value = SimpleTheme.typography.body0) {
110+
Text(text = stringResource(id = R.string.overdue_search_patient_name_or_village))
111+
}
112+
}
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)