11package com.hoc.flowmvi.ui.add
22
3+ import android.content.Context
4+ import androidx.compose.animation.Crossfade
5+ import androidx.compose.animation.core.tween
6+ import androidx.compose.foundation.layout.Box
37import androidx.compose.foundation.layout.Column
8+ import androidx.compose.foundation.layout.PaddingValues
9+ import androidx.compose.foundation.layout.Spacer
10+ import androidx.compose.foundation.layout.fillMaxSize
11+ import androidx.compose.foundation.layout.fillMaxWidth
12+ import androidx.compose.foundation.layout.height
13+ import androidx.compose.foundation.layout.heightIn
14+ import androidx.compose.foundation.layout.padding
15+ import androidx.compose.foundation.layout.wrapContentHeight
16+ import androidx.compose.foundation.layout.wrapContentSize
17+ import androidx.compose.foundation.rememberScrollState
18+ import androidx.compose.foundation.text.KeyboardOptions
19+ import androidx.compose.foundation.verticalScroll
420import androidx.compose.material.icons.Icons
521import androidx.compose.material.icons.filled.ArrowBack
22+ import androidx.compose.material.icons.filled.Email
23+ import androidx.compose.material.icons.filled.Person
24+ import androidx.compose.material3.ButtonDefaults
25+ import androidx.compose.material3.ElevatedButton
626import androidx.compose.material3.ExperimentalMaterial3Api
727import androidx.compose.material3.Icon
828import androidx.compose.material3.IconButton
29+ import androidx.compose.material3.MaterialTheme
30+ import androidx.compose.material3.Text
931import androidx.compose.material3.TextField
1032import androidx.compose.material3.TopAppBarDefaults
1133import androidx.compose.runtime.Composable
@@ -14,21 +36,35 @@ import androidx.compose.runtime.getValue
1436import androidx.compose.runtime.remember
1537import androidx.compose.runtime.rememberCoroutineScope
1638import androidx.compose.runtime.rememberUpdatedState
39+ import androidx.compose.ui.Alignment
1740import androidx.compose.ui.Modifier
41+ import androidx.compose.ui.platform.LocalContext
1842import androidx.compose.ui.res.stringResource
43+ import androidx.compose.ui.text.input.ImeAction
44+ import androidx.compose.ui.text.input.KeyboardType
45+ import androidx.compose.ui.tooling.preview.Preview
46+ import androidx.compose.ui.unit.dp
1947import androidx.hilt.navigation.compose.hiltViewModel
2048import androidx.lifecycle.Lifecycle
2149import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
2250import androidx.lifecycle.compose.collectAsStateWithLifecycle
2351import com.hoc.flowmvi.core_ui.AppBarState
2452import com.hoc.flowmvi.core_ui.ConfigAppBar
53+ import com.hoc.flowmvi.core_ui.LoadingIndicator
2554import com.hoc.flowmvi.core_ui.LocalSnackbarHostState
2655import com.hoc.flowmvi.core_ui.OnLifecycleEvent
2756import com.hoc.flowmvi.core_ui.collectInLaunchedEffectWithLifecycle
57+ import com.hoc.flowmvi.domain.model.UserError
58+ import com.hoc.flowmvi.domain.model.UserValidationError
59+ import com.hoc.flowmvi.ui.theme.AppTheme
60+ import kotlinx.collections.immutable.persistentHashSetOf
61+ import kotlinx.coroutines.Dispatchers
2862import kotlinx.coroutines.channels.Channel
2963import kotlinx.coroutines.flow.collect
3064import kotlinx.coroutines.flow.consumeAsFlow
3165import kotlinx.coroutines.flow.onEach
66+ import kotlinx.coroutines.launch
67+ import kotlinx.coroutines.withContext
3268
3369@OptIn(ExperimentalMaterial3Api ::class , ExperimentalLifecycleComposeApi ::class )
3470@Composable
@@ -63,18 +99,33 @@ internal fun AddNewUserRoute(
6399
64100 val intentChannel = remember { Channel <ViewIntent >(Channel .UNLIMITED ) }
65101 LaunchedEffect (Unit ) {
66- intentChannel
67- .consumeAsFlow()
68- .onEach(viewModel::processIntent)
69- .collect()
102+ withContext(Dispatchers .Main .immediate) {
103+ intentChannel
104+ .consumeAsFlow()
105+ .onEach(viewModel::processIntent)
106+ .collect()
107+ }
70108 }
71109
72110 val snackbarHostState by rememberUpdatedState(LocalSnackbarHostState .current)
73111 val scope = rememberCoroutineScope()
112+ val context = LocalContext .current
74113 viewModel.singleEvent.collectInLaunchedEffectWithLifecycle { event ->
75114 when (event) {
76- is SingleEvent .AddUserFailure -> TODO ()
77- is SingleEvent .AddUserSuccess -> TODO ()
115+ is SingleEvent .AddUserFailure -> {
116+ scope.launch {
117+ snackbarHostState.showSnackbar(
118+ event.error.getReadableMessage(context)
119+ )
120+ }
121+ }
122+ is SingleEvent .AddUserSuccess -> {
123+ scope.launch {
124+ snackbarHostState.showSnackbar(
125+ context.getString(R .string.add_user_success)
126+ )
127+ }
128+ }
78129 }
79130 }
80131
@@ -89,6 +140,9 @@ internal fun AddNewUserRoute(
89140 modifier = modifier,
90141 viewState = viewState,
91142 onEmailChanged = { dispatch(ViewIntent .EmailChanged (it)) },
143+ onFirstNameChanged = { dispatch(ViewIntent .FirstNameChanged (it)) },
144+ onLastNameChanged = { dispatch(ViewIntent .LastNameChanged (it)) },
145+ onSubmit = { dispatch(ViewIntent .Submit ) }
92146 )
93147}
94148
@@ -97,12 +151,187 @@ internal fun AddNewUserRoute(
97151private fun AddNewUserContent (
98152 viewState : ViewState ,
99153 onEmailChanged : (String ) -> Unit ,
154+ onFirstNameChanged : (String ) -> Unit ,
155+ onLastNameChanged : (String ) -> Unit ,
156+ onSubmit : () -> Unit ,
100157 modifier : Modifier = Modifier ,
101158) {
102- Column (modifier = modifier) {
103- TextField (
104- value = viewState.email ? : " " ,
105- onValueChange = onEmailChanged
159+ val emailError =
160+ if (viewState.emailChanged && UserValidationError .INVALID_EMAIL_ADDRESS in viewState.errors) " Invalid email"
161+ else null
162+
163+ val firstNameError =
164+ if (viewState.firstNameChanged && UserValidationError .TOO_SHORT_FIRST_NAME in viewState.errors) " Too short first name"
165+ else null
166+
167+ val lastNameError =
168+ if (viewState.lastNameChanged && UserValidationError .TOO_SHORT_LAST_NAME in viewState.errors) " Too short last name"
169+ else null
170+
171+ Box (
172+ modifier = modifier
173+ .fillMaxSize()
174+ .padding(horizontal = 16 .dp)
175+ ) {
176+ Column (
177+ modifier = Modifier
178+ .fillMaxSize()
179+ .wrapContentHeight(Alignment .CenterVertically )
180+ .verticalScroll(rememberScrollState())
181+ ) {
182+ Spacer (modifier = Modifier .height(16 .dp))
183+
184+ TextField (
185+ modifier = Modifier .fillMaxWidth(),
186+ value = viewState.email ? : " " ,
187+ onValueChange = onEmailChanged,
188+ label = { Text (text = " Email" ) },
189+ leadingIcon = {
190+ Icon (
191+ imageVector = Icons .Filled .Email ,
192+ contentDescription = " Email"
193+ )
194+ },
195+ maxLines = 1 ,
196+ singleLine = true ,
197+ keyboardOptions = KeyboardOptions (
198+ keyboardType = KeyboardType .Email ,
199+ imeAction = ImeAction .Next
200+ ),
201+ isError = emailError != = null ,
202+ supportingText = {
203+ emailError?.let {
204+ Text (text = it)
205+ }
206+ }
207+ )
208+
209+ Spacer (modifier = Modifier .height(16 .dp))
210+
211+ TextField (
212+ modifier = Modifier .fillMaxWidth(),
213+ value = viewState.firstName ? : " " ,
214+ onValueChange = onFirstNameChanged,
215+ label = { Text (text = " First name" ) },
216+ leadingIcon = {
217+ Icon (
218+ imageVector = Icons .Filled .Person ,
219+ contentDescription = " First name"
220+ )
221+ },
222+ maxLines = 1 ,
223+ singleLine = true ,
224+ keyboardOptions = KeyboardOptions (
225+ keyboardType = KeyboardType .Text ,
226+ imeAction = ImeAction .Next
227+ ),
228+ isError = firstNameError != = null ,
229+ supportingText = {
230+ firstNameError?.let {
231+ Text (text = it)
232+ }
233+ }
234+ )
235+
236+ Spacer (modifier = Modifier .height(16 .dp))
237+
238+ TextField (
239+ modifier = Modifier .fillMaxWidth(),
240+ value = viewState.lastName ? : " " ,
241+ onValueChange = onLastNameChanged,
242+ label = { Text (text = " Last name" ) },
243+ leadingIcon = {
244+ Icon (
245+ imageVector = Icons .Filled .Person ,
246+ contentDescription = " Last name"
247+ )
248+ },
249+ maxLines = 1 ,
250+ singleLine = true ,
251+ keyboardOptions = KeyboardOptions (
252+ keyboardType = KeyboardType .Text ,
253+ imeAction = ImeAction .Done
254+ ),
255+ isError = lastNameError != = null ,
256+ supportingText = {
257+ lastNameError?.let {
258+ Text (text = it)
259+ }
260+ }
261+ )
262+
263+ Spacer (modifier = Modifier .height(24 .dp))
264+
265+ Crossfade (
266+ modifier = Modifier
267+ .fillMaxWidth()
268+ .heightIn(min = 64 .dp),
269+ targetState = viewState.isLoading,
270+ animationSpec = tween(durationMillis = 200 ),
271+ label = " LoadingIndicator/ElevatedButton" ,
272+ ) { isLoading ->
273+ if (isLoading) {
274+ LoadingIndicator (
275+ modifier = Modifier
276+ .fillMaxWidth(),
277+ )
278+ } else {
279+ ElevatedButton (
280+ modifier = Modifier
281+ .fillMaxWidth()
282+ .wrapContentSize(Alignment .Center ),
283+ colors = ButtonDefaults .elevatedButtonColors(
284+ containerColor = MaterialTheme .colorScheme.primary,
285+ contentColor = MaterialTheme .colorScheme.onPrimary
286+ ),
287+ onClick = onSubmit,
288+ contentPadding = PaddingValues (
289+ horizontal = 32 .dp,
290+ vertical = 16 .dp,
291+ ),
292+ ) {
293+ Text (text = " Add" )
294+ }
295+ }
296+ }
297+
298+ Spacer (modifier = Modifier .height(16 .dp))
299+ }
300+ }
301+ }
302+
303+ @Preview(
304+ showBackground = true ,
305+ showSystemUi = true ,
306+ device = " spec:shape=Normal,width=360,height=640,unit=dp,dpi=480" ,
307+ )
308+ @Composable
309+ fun PreviewAddNewUserContent () {
310+ AppTheme {
311+ AddNewUserContent (
312+ viewState = ViewState (
313+ errors = persistentHashSetOf(),
314+ isLoading = true ,
315+ emailChanged = false ,
316+ firstNameChanged = false ,
317+ lastNameChanged = false ,
318+ email = " hoc081098@gmail.com" ,
319+ firstName = " Petrus" ,
320+ lastName = " Hoc" ,
321+ ),
322+ onEmailChanged = {},
323+ onFirstNameChanged = {},
324+ onLastNameChanged = {},
325+ onSubmit = {}
106326 )
107327 }
108328}
329+
330+ private fun UserError.getReadableMessage (context : Context ): String = when (this ) {
331+ is UserError .InvalidId -> context.getString(R .string.invalid_id_error_message)
332+ UserError .NetworkError -> context.getString(R .string.network_error_error_message)
333+ UserError .ServerError -> context.getString(R .string.server_error_error_message)
334+ UserError .Unexpected -> context.getString(R .string.unexpected_error_error_message)
335+ is UserError .UserNotFound -> context.getString(R .string.user_not_found_error_message)
336+ is UserError .ValidationFailed -> context.getString(R .string.validation_failed_error_message)
337+ }
0 commit comments