Skip to content

Commit 66559b1

Browse files
committed
Better error handling
1 parent 45ad60c commit 66559b1

File tree

6 files changed

+248
-82
lines changed

6 files changed

+248
-82
lines changed

android/src/main/kotlin/br/com/dillmann/fireflycompanion/android/accounts/AccountForm.kt

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
package br.com.dillmann.fireflycompanion.android.accounts
22

3-
import androidx.compose.foundation.layout.*
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.fillMaxWidth
5+
import androidx.compose.foundation.layout.imePadding
6+
import androidx.compose.foundation.layout.padding
47
import androidx.compose.material3.*
58
import androidx.compose.runtime.Composable
69
import androidx.compose.runtime.LaunchedEffect
710
import androidx.compose.runtime.getValue
811
import androidx.compose.runtime.setValue
9-
import androidx.compose.ui.Alignment
1012
import androidx.compose.ui.Modifier
1113
import androidx.compose.ui.text.style.TextAlign
1214
import androidx.compose.ui.unit.dp
13-
import androidx.compose.ui.window.Dialog
1415
import br.com.dillmann.fireflycompanion.android.R
16+
import br.com.dillmann.fireflycompanion.android.core.components.action.AsyncAction
17+
import br.com.dillmann.fireflycompanion.android.core.components.action.AsyncActionSink
1518
import br.com.dillmann.fireflycompanion.android.core.components.section.Section
1619
import br.com.dillmann.fireflycompanion.android.core.components.textfield.AppMoneyTextField
20+
import br.com.dillmann.fireflycompanion.android.core.components.textfield.AppTextFieldDefaults
1721
import br.com.dillmann.fireflycompanion.android.core.components.transactions.TransactionList
18-
import br.com.dillmann.fireflycompanion.android.core.compose.persistent
1922
import br.com.dillmann.fireflycompanion.android.core.compose.volatile
2023
import br.com.dillmann.fireflycompanion.android.core.i18n.i18n
21-
import br.com.dillmann.fireflycompanion.android.core.queue.ActionQueue
24+
import br.com.dillmann.fireflycompanion.android.core.koin.get
2225
import br.com.dillmann.fireflycompanion.android.core.refresh.OnRefreshEvent
2326
import br.com.dillmann.fireflycompanion.android.core.refresh.RefreshDispatcher
2427
import br.com.dillmann.fireflycompanion.android.core.router.NavigationContext
25-
import br.com.dillmann.fireflycompanion.android.core.components.textfield.AppTextFieldDefaults
26-
import br.com.dillmann.fireflycompanion.android.core.koin.get
2728
import br.com.dillmann.fireflycompanion.business.account.Account
2829
import br.com.dillmann.fireflycompanion.business.account.usecase.GetAccountUseCase
2930
import br.com.dillmann.fireflycompanion.business.account.usecase.UpdateAccountBalanceUseCase
@@ -32,28 +33,23 @@ import br.com.dillmann.fireflycompanion.business.transaction.usecase.ListTransac
3233
@Composable
3334
@OptIn(ExperimentalMaterial3Api::class)
3435
fun NavigationContext.AccountForm() {
35-
val queue by persistent(ActionQueue())
36+
val actionSink by volatile(AsyncActionSink())
3637
var account by volatile(requireBagValue<Account>())
3738
var balance by volatile(account.currentBalance)
38-
var showLoading by volatile(false)
3939
val listTransactionsUseCase = get<ListTransactionsUseCase>()
4040
val updateBalanceUseCase = get<UpdateAccountBalanceUseCase>()
4141
val getAccountUseCase = get<GetAccountUseCase>()
4242

4343
fun updateBalance() {
44-
showLoading = true
45-
46-
queue.add {
44+
actionSink.push {
4745
updateBalanceUseCase.updateBalance(account.id, balance)
4846
RefreshDispatcher.notify()
4947
}
5048
}
5149

5250
OnRefreshEvent("AccountForm") {
53-
queue.add {
54-
showLoading = true
51+
actionSink.push {
5552
account = getAccountUseCase.getAccount(account.id)!!
56-
showLoading = false
5753
}
5854
}
5955

@@ -109,19 +105,7 @@ fun NavigationContext.AccountForm() {
109105
}
110106
}
111107

112-
if (showLoading) {
113-
Dialog(onDismissRequest = {}) {
114-
Column(
115-
modifier = Modifier
116-
.padding(16.dp)
117-
.fillMaxWidth(),
118-
horizontalAlignment = Alignment.CenterHorizontally,
119-
verticalArrangement = Arrangement.Center
120-
) {
121-
CircularProgressIndicator()
122-
Spacer(modifier = Modifier.height(16.dp))
123-
Text(text = i18n(R.string.loading))
124-
}
125-
}
126-
}
108+
AsyncAction(
109+
sink = actionSink
110+
)
127111
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package br.com.dillmann.fireflycompanion.android.core.components.action
2+
3+
import androidx.compose.foundation.layout.*
4+
import androidx.compose.foundation.rememberScrollState
5+
import androidx.compose.foundation.verticalScroll
6+
import androidx.compose.material3.*
7+
import androidx.compose.runtime.*
8+
import androidx.compose.ui.Alignment
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.unit.dp
11+
import androidx.compose.ui.window.Dialog
12+
import br.com.dillmann.fireflycompanion.android.R
13+
import br.com.dillmann.fireflycompanion.android.core.compose.async
14+
import br.com.dillmann.fireflycompanion.android.core.compose.emptyVolatile
15+
import br.com.dillmann.fireflycompanion.android.core.compose.persistent
16+
import br.com.dillmann.fireflycompanion.android.core.compose.volatile
17+
import br.com.dillmann.fireflycompanion.android.core.i18n.i18n
18+
import br.com.dillmann.fireflycompanion.android.core.queue.ActionQueue
19+
import br.com.dillmann.fireflycompanion.core.validation.ConsistencyException
20+
import br.com.dillmann.fireflycompanion.thirdparty.firefly.infrastructure.ClientError
21+
import br.com.dillmann.fireflycompanion.thirdparty.firefly.infrastructure.ClientException
22+
import br.com.dillmann.fireflycompanion.thirdparty.firefly.infrastructure.ServerError
23+
24+
typealias OnComplete = () -> Unit
25+
typealias OnError = (Exception) -> Unit
26+
typealias OnViolation = (ConsistencyException) -> Unit
27+
typealias AsyncActionTask = suspend () -> Unit
28+
29+
@Composable
30+
fun AsyncAction(
31+
sink: AsyncActionSink,
32+
waitMessage: String = i18n(R.string.loading),
33+
onComplete: OnComplete? = null,
34+
onError: OnError? = null,
35+
onViolation: OnViolation? = null,
36+
) {
37+
val queue by persistent(ActionQueue())
38+
var loading by volatile(false)
39+
var exception by emptyVolatile<Exception>()
40+
var lastTask by emptyVolatile<AsyncActionTask>()
41+
42+
suspend fun launch(task: AsyncActionTask) {
43+
try {
44+
task()
45+
onComplete?.invoke()
46+
} catch (e: ConsistencyException) {
47+
onViolation?.invoke(e)
48+
} catch (ex: Exception) {
49+
onError?.invoke(ex)
50+
exception = ex
51+
} finally {
52+
loading = false
53+
}
54+
}
55+
56+
LaunchedEffect(Unit) {
57+
sink.attach { task ->
58+
loading = true
59+
lastTask = task
60+
61+
queue.add {
62+
launch(task)
63+
}
64+
}
65+
}
66+
67+
DisposableEffect(Unit) {
68+
onDispose {
69+
sink.detach()
70+
}
71+
}
72+
73+
if (loading) {
74+
Dialog(onDismissRequest = {}) {
75+
Column(
76+
modifier = Modifier
77+
.padding(16.dp)
78+
.fillMaxWidth(),
79+
horizontalAlignment = Alignment.CenterHorizontally,
80+
verticalArrangement = Arrangement.Center
81+
) {
82+
CircularProgressIndicator()
83+
Spacer(modifier = Modifier.height(16.dp))
84+
Text(text = waitMessage)
85+
}
86+
}
87+
}
88+
89+
if (exception != null) {
90+
var showDetails by volatile(false)
91+
92+
AlertDialog(
93+
onDismissRequest = { exception = null },
94+
title = {
95+
Text(text = i18n(R.string.error_action_failed))
96+
},
97+
text = {
98+
ErrorDetails(showDetails, exception!!)
99+
},
100+
confirmButton = {
101+
Button(
102+
onClick = {
103+
exception = null
104+
loading = true
105+
async { launch(lastTask!!) }
106+
}
107+
) {
108+
Text(
109+
text = i18n(R.string.try_again),
110+
)
111+
}
112+
},
113+
dismissButton = {
114+
TextButton(
115+
onClick = { showDetails = !showDetails },
116+
) {
117+
Text(
118+
text =
119+
if (showDetails) i18n(R.string.hide_details)
120+
else i18n(R.string.show_details),
121+
)
122+
}
123+
}
124+
)
125+
}
126+
}
127+
128+
@Composable
129+
private fun ErrorDetails(
130+
techDetailsVisible: Boolean,
131+
exception: Exception,
132+
) {
133+
Column(modifier = Modifier.fillMaxWidth()) {
134+
Text(text = exception.message ?: i18n(R.string.error_unexpected))
135+
136+
if (!techDetailsVisible)
137+
return@Column
138+
139+
val scroll = rememberScrollState()
140+
141+
Column(
142+
modifier = Modifier
143+
.fillMaxHeight()
144+
.verticalScroll(scroll),
145+
) {
146+
if (exception is ClientException) {
147+
val response = exception.response
148+
val responseBody =
149+
when (response) {
150+
is ServerError<*> -> response.body?.toString()
151+
is ClientError<*> -> response.body?.toString()
152+
else -> null
153+
}
154+
155+
if (responseBody != null) {
156+
Spacer(modifier = Modifier.height(12.dp))
157+
158+
Text(
159+
text = responseBody,
160+
)
161+
}
162+
}
163+
164+
Spacer(modifier = Modifier.height(12.dp))
165+
166+
Text(
167+
text = exception.stackTraceToString(),
168+
)
169+
}
170+
}
171+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package br.com.dillmann.fireflycompanion.android.core.components.action
2+
3+
class AsyncActionSink {
4+
private val queue = mutableListOf<AsyncActionTask>()
5+
private lateinit var delegate: (AsyncActionTask) -> Unit
6+
7+
fun push(task: AsyncActionTask) {
8+
if (!::delegate.isInitialized)
9+
queue += task
10+
else
11+
delegate(task)
12+
}
13+
14+
fun attach(executor: (AsyncActionTask) -> Unit) {
15+
delegate = executor
16+
17+
queue.forEach(executor)
18+
queue.clear()
19+
}
20+
21+
fun detach() {
22+
delegate = {}
23+
}
24+
}

0 commit comments

Comments
 (0)