Skip to content

Commit ff2a33d

Browse files
authored
feat(search) (#57)
* feat(search): wip * feat(search): setup navigation * feat(search): immutable * feat(search): add ui * feat(search): refactor * update main * fix(main): fix state change * refactor
1 parent b16abdd commit ff2a33d

File tree

40 files changed

+1759
-61
lines changed

40 files changed

+1759
-61
lines changed

.idea/deploymentTargetDropDown.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ dependencies {
8989
implementation(coreUi)
9090
implementation(featureMain)
9191
implementation(featureAdd)
92+
implementation(featureSearch)
9293

9394
implementation(deps.coroutines.android)
9495
implementation(deps.timber)

app/src/main/java/com/hoc/flowmvi/AppState.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import androidx.navigation.NavDestination
77
import androidx.navigation.NavHostController
88
import androidx.navigation.compose.currentBackStackEntryAsState
99
import androidx.navigation.compose.rememberNavController
10-
import com.hoc.flowmvi.Screen.AddNewUser
11-
import com.hoc.flowmvi.Screen.UsersList
1210
import com.hoc.flowmvi.ui.add.navigation.AddNewUserNavigationRoute
1311
import com.hoc.flowmvi.ui.main.navigation.UsersListNavigationRoute
12+
import com.hoc.flowmvi.ui.search.navigation.SearchUserNavigationRoute
1413

1514
@Composable
1615
fun rememberJetpackComposeMVICoroutinesFlowApp(
@@ -28,7 +27,7 @@ enum class Screen {
2827
get() = when (this) {
2928
UsersList -> UsersListNavigationRoute
3029
AddNewUser -> AddNewUserNavigationRoute
31-
SearchUsers -> TODO()
30+
SearchUsers -> SearchUserNavigationRoute
3231
}
3332

3433
companion object {
@@ -50,9 +49,10 @@ class JetpackComposeMVICoroutinesFlowAppState(
5049

5150
val currentScreen: Screen?
5251
@Composable get() = when (currentDestination?.route) {
53-
UsersListNavigationRoute -> UsersList
54-
AddNewUserNavigationRoute -> AddNewUser
55-
else -> TODO()
52+
UsersListNavigationRoute -> Screen.UsersList
53+
AddNewUserNavigationRoute -> Screen.AddNewUser
54+
SearchUserNavigationRoute -> Screen.SearchUsers
55+
else -> null
5656
}
5757

5858
fun onNavigateUp() {

app/src/main/java/com/hoc/flowmvi/MainActivity.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
1010
import androidx.compose.material3.Scaffold
1111
import androidx.compose.material3.SnackbarHost
1212
import androidx.compose.material3.SnackbarHostState
13-
import androidx.compose.material3.Text
1413
import androidx.compose.material3.TopAppBarColors
1514
import androidx.compose.runtime.Composable
1615
import androidx.compose.runtime.getValue
@@ -24,6 +23,8 @@ import com.hoc.flowmvi.core_ui.ProvideSnackbarHostState
2423
import com.hoc.flowmvi.ui.add.navigation.addNewUserScreen
2524
import com.hoc.flowmvi.ui.add.navigation.navigateToAddNewUser
2625
import com.hoc.flowmvi.ui.main.navigation.usersListScreen
26+
import com.hoc.flowmvi.ui.search.navigation.navigateToSearchUser
27+
import com.hoc.flowmvi.ui.search.navigation.searchUserScreen
2728
import com.hoc.flowmvi.ui.theme.AppTheme
2829
import dagger.hilt.android.AndroidEntryPoint
2930

@@ -43,18 +44,14 @@ class MainActivity : AppCompatActivity() {
4344
@OptIn(ExperimentalMaterial3Api::class)
4445
@Composable
4546
fun JetpackComposeMVICoroutinesFlowAppBar(
46-
title: String?,
47+
title: @Composable () -> Unit,
4748
navigationIcon: @Composable () -> Unit,
4849
actions: @Composable RowScope.() -> Unit,
4950
colors: TopAppBarColors,
5051
modifier: Modifier = Modifier
5152
) {
5253
CenterAlignedTopAppBar(
53-
title = {
54-
if (title != null) {
55-
Text(text = title)
56-
}
57-
},
54+
title = title,
5855
modifier = modifier,
5956
navigationIcon = navigationIcon,
6057
actions = actions,
@@ -94,13 +91,19 @@ private fun JetpackComposeMVICoroutinesFlowApp(
9491
) {
9592
usersListScreen(
9693
configAppBar = { appBarState = it },
97-
navigateToAddUser = { navController.navigateToAddNewUser() }
94+
navigateToAddUser = navController::navigateToAddNewUser,
95+
navigateToSearchUser = navController::navigateToSearchUser
9896
)
9997

10098
addNewUserScreen(
10199
configAppBar = { appBarState = it },
102100
onBackClick = appState::onBackClick
103101
)
102+
103+
searchUserScreen(
104+
configAppBar = { appBarState = it },
105+
onBackClick = appState::onBackClick
106+
)
104107
}
105108
}
106109
}

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ buildscript {
1616
classpath("com.diffplug.spotless:spotless-plugin-gradle:6.12.0")
1717
classpath("com.google.dagger:hilt-android-gradle-plugin:${deps.daggerHilt.version}")
1818
classpath("com.github.ben-manes:gradle-versions-plugin:0.44.0")
19+
classpath("org.jacoco:org.jacoco.core:0.8.8")
20+
classpath("com.vanniktech:gradle-android-junit-jacoco-plugin:0.17.0-SNAPSHOT")
21+
classpath("dev.ahmedmourad.nocopy:nocopy-gradle-plugin:1.4.0")
1922
}
2023
}
2124

buildSrc/src/main/kotlin/deps.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ object deps {
108108

109109
const val immutableCollections = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5"
110110

111-
const val viewBindingDelegate = "com.github.hoc081098:ViewBindingDelegate:1.2.0"
112111
const val flowExt = "io.github.hoc081098:FlowExt:0.5.0"
113112
const val timber = "com.jakewharton.timber:timber:5.0.1"
114113

@@ -132,6 +131,7 @@ inline val PDsS.kotlin: PDS get() = kotlin("jvm")
132131
inline val PDsS.kotlinKapt: PDS get() = kotlin("kapt")
133132
inline val PDsS.kotlinParcelize: PDS get() = id("kotlin-parcelize")
134133
inline val PDsS.daggerHiltAndroid: PDS get() = id("dagger.hilt.android.plugin")
134+
inline val PDsS.nocopyPlugin: PDS get() = id("dev.ahmedmourad.nocopy.nocopy-gradle-plugin")
135135

136136
inline val DependencyHandler.domain get() = project(":domain")
137137
inline val DependencyHandler.core get() = project(":core")

core-ui/src/main/java/com/hoc/flowmvi/core_ui/AppBarState.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import androidx.compose.runtime.Stable
99
@OptIn(ExperimentalMaterial3Api::class)
1010
@Stable
1111
data class AppBarState(
12-
val title: String?,
12+
val title: @Composable () -> Unit,
1313
val actions: @Composable RowScope.() -> Unit,
1414
val navigationIcon: @Composable () -> Unit,
1515
val colors: TopAppBarColors,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.hoc.flowmvi.core_ui
2+
3+
import androidx.compose.foundation.interaction.MutableInteractionSource
4+
import androidx.compose.foundation.layout.PaddingValues
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.heightIn
7+
import androidx.compose.foundation.text.BasicTextField
8+
import androidx.compose.foundation.text.KeyboardActions
9+
import androidx.compose.foundation.text.KeyboardOptions
10+
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
11+
import androidx.compose.material3.ExperimentalMaterial3Api
12+
import androidx.compose.material3.LocalTextStyle
13+
import androidx.compose.material3.MaterialTheme
14+
import androidx.compose.material3.Text
15+
import androidx.compose.material3.TextFieldDefaults
16+
import androidx.compose.material3.TextFieldDefaults.indicatorLine
17+
import androidx.compose.runtime.Composable
18+
import androidx.compose.runtime.CompositionLocalProvider
19+
import androidx.compose.runtime.SideEffect
20+
import androidx.compose.runtime.getValue
21+
import androidx.compose.runtime.mutableStateOf
22+
import androidx.compose.runtime.remember
23+
import androidx.compose.runtime.setValue
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.focus.FocusRequester
26+
import androidx.compose.ui.focus.focusRequester
27+
import androidx.compose.ui.graphics.Color
28+
import androidx.compose.ui.graphics.SolidColor
29+
import androidx.compose.ui.graphics.takeOrElse
30+
import androidx.compose.ui.text.TextStyle
31+
import androidx.compose.ui.text.input.TextFieldValue
32+
import androidx.compose.ui.text.input.VisualTransformation
33+
import androidx.compose.ui.unit.dp
34+
import androidx.compose.ui.unit.sp
35+
36+
@OptIn(ExperimentalMaterial3Api::class)
37+
@Composable
38+
fun AppBarTextField(
39+
value: String,
40+
onValueChange: (String) -> Unit,
41+
hint: String,
42+
modifier: Modifier = Modifier,
43+
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
44+
keyboardActions: KeyboardActions = KeyboardActions.Default,
45+
) {
46+
val colors = TextFieldDefaults.textFieldColors(containerColor = Color.Unspecified)
47+
48+
val textStyle = LocalTextStyle.current
49+
// If color is not provided via the text style, use content color as a default
50+
val textColor = textStyle.color.takeOrElse { MaterialTheme.colorScheme.onSurface }
51+
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor, lineHeight = 50.sp))
52+
53+
val interactionSource = remember { MutableInteractionSource() }
54+
55+
// Holds the latest internal TextFieldValue state. We need to keep it to have the correct value
56+
// of the composition.
57+
// Set the correct cursor position when this composable is first initialized
58+
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
59+
60+
// Holds the latest TextFieldValue that BasicTextField was recomposed with. We couldn't simply
61+
// pass `TextFieldValue(text = value)` to the CoreTextField because we need to preserve the
62+
// composition.
63+
val textFieldValue = textFieldValueState.copy(text = value)
64+
65+
SideEffect {
66+
if (textFieldValue.selection != textFieldValueState.selection ||
67+
textFieldValue.composition != textFieldValueState.composition
68+
) {
69+
textFieldValueState = textFieldValue
70+
}
71+
}
72+
// Last String value that either text field was recomposed with or updated in the onValueChange
73+
// callback. We keep track of it to prevent calling onValueChange(String) for same String when
74+
// CoreTextField's onValueChange is called multiple times without recomposition in between.
75+
var lastTextValue by remember(value) { mutableStateOf(value) }
76+
77+
// request focus when this composable is first initialized
78+
val focusRequester = remember { FocusRequester() }
79+
SideEffect { focusRequester.requestFocus() }
80+
81+
CompositionLocalProvider(LocalTextSelectionColors provides LocalTextSelectionColors.current) {
82+
BasicTextField(
83+
value = textFieldValue,
84+
onValueChange = { newTextFieldValueState ->
85+
textFieldValueState = newTextFieldValueState
86+
87+
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
88+
lastTextValue = newTextFieldValueState.text
89+
90+
if (stringChangedSinceLastInvocation) {
91+
// remove newlines to avoid strange layout issues, and also because singleLine=true
92+
onValueChange(newTextFieldValueState.text.replace("\n", ""))
93+
}
94+
},
95+
modifier = modifier
96+
.fillMaxWidth()
97+
.heightIn(32.dp)
98+
.indicatorLine(
99+
enabled = true,
100+
isError = false,
101+
interactionSource = interactionSource,
102+
colors = colors,
103+
)
104+
.focusRequester(focusRequester),
105+
textStyle = mergedTextStyle,
106+
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
107+
keyboardOptions = keyboardOptions,
108+
keyboardActions = keyboardActions,
109+
interactionSource = interactionSource,
110+
singleLine = true,
111+
maxLines = 1,
112+
decorationBox = @Composable { innerTextField ->
113+
// places text field with placeholder and appropriate bottom padding
114+
TextFieldDefaults.TextFieldDecorationBox(
115+
value = value,
116+
visualTransformation = VisualTransformation.None,
117+
innerTextField = innerTextField,
118+
placeholder = { Text(text = hint) },
119+
singleLine = true,
120+
enabled = true,
121+
interactionSource = interactionSource,
122+
colors = colors,
123+
contentPadding = PaddingValues(bottom = 4.dp)
124+
)
125+
}
126+
)
127+
}
128+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
<resources>
22
<string name="app_name">Compose MVI Coroutines Flow</string>
33
<string name="retry">RETRY</string>
4+
5+
<string name="invalid_id_error_message">Invalid id</string>
6+
<string name="network_error_error_message">Network error</string>
7+
<string name="server_error_error_message">Server error</string>
8+
<string name="unexpected_error_error_message">Unexpected error</string>
9+
<string name="user_not_found_error_message">User not found</string>
10+
<string name="validation_failed_error_message">Validation failed</string>
11+
<string name="add_user_success">Added user successfully</string>
412
</resources>

0 commit comments

Comments
 (0)