Skip to content

feat(search) #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .idea/deploymentTargetDropDown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ dependencies {
implementation(coreUi)
implementation(featureMain)
implementation(featureAdd)
implementation(featureSearch)

implementation(deps.coroutines.android)
implementation(deps.timber)
Expand Down
12 changes: 6 additions & 6 deletions app/src/main/java/com/hoc/flowmvi/AppState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import androidx.navigation.NavDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.hoc.flowmvi.Screen.AddNewUser
import com.hoc.flowmvi.Screen.UsersList
import com.hoc.flowmvi.ui.add.navigation.AddNewUserNavigationRoute
import com.hoc.flowmvi.ui.main.navigation.UsersListNavigationRoute
import com.hoc.flowmvi.ui.search.navigation.SearchUserNavigationRoute

@Composable
fun rememberJetpackComposeMVICoroutinesFlowApp(
Expand All @@ -28,7 +27,7 @@ enum class Screen {
get() = when (this) {
UsersList -> UsersListNavigationRoute
AddNewUser -> AddNewUserNavigationRoute
SearchUsers -> TODO()
SearchUsers -> SearchUserNavigationRoute
}

companion object {
Expand All @@ -50,9 +49,10 @@ class JetpackComposeMVICoroutinesFlowAppState(

val currentScreen: Screen?
@Composable get() = when (currentDestination?.route) {
UsersListNavigationRoute -> UsersList
AddNewUserNavigationRoute -> AddNewUser
else -> TODO()
UsersListNavigationRoute -> Screen.UsersList
AddNewUserNavigationRoute -> Screen.AddNewUser
SearchUserNavigationRoute -> Screen.SearchUsers
else -> null
}

fun onNavigateUp() {
Expand Down
19 changes: 11 additions & 8 deletions app/src/main/java/com/hoc/flowmvi/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
Expand All @@ -24,6 +23,8 @@ import com.hoc.flowmvi.core_ui.ProvideSnackbarHostState
import com.hoc.flowmvi.ui.add.navigation.addNewUserScreen
import com.hoc.flowmvi.ui.add.navigation.navigateToAddNewUser
import com.hoc.flowmvi.ui.main.navigation.usersListScreen
import com.hoc.flowmvi.ui.search.navigation.navigateToSearchUser
import com.hoc.flowmvi.ui.search.navigation.searchUserScreen
import com.hoc.flowmvi.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint

Expand All @@ -43,18 +44,14 @@ class MainActivity : AppCompatActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JetpackComposeMVICoroutinesFlowAppBar(
title: String?,
title: @Composable () -> Unit,
navigationIcon: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit,
colors: TopAppBarColors,
modifier: Modifier = Modifier
) {
CenterAlignedTopAppBar(
title = {
if (title != null) {
Text(text = title)
}
},
title = title,
modifier = modifier,
navigationIcon = navigationIcon,
actions = actions,
Expand Down Expand Up @@ -94,13 +91,19 @@ private fun JetpackComposeMVICoroutinesFlowApp(
) {
usersListScreen(
configAppBar = { appBarState = it },
navigateToAddUser = { navController.navigateToAddNewUser() }
navigateToAddUser = navController::navigateToAddNewUser,
navigateToSearchUser = navController::navigateToSearchUser
)

addNewUserScreen(
configAppBar = { appBarState = it },
onBackClick = appState::onBackClick
)

searchUserScreen(
configAppBar = { appBarState = it },
onBackClick = appState::onBackClick
)
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ buildscript {
classpath("com.diffplug.spotless:spotless-plugin-gradle:6.12.0")
classpath("com.google.dagger:hilt-android-gradle-plugin:${deps.daggerHilt.version}")
classpath("com.github.ben-manes:gradle-versions-plugin:0.44.0")
classpath("org.jacoco:org.jacoco.core:0.8.8")
classpath("com.vanniktech:gradle-android-junit-jacoco-plugin:0.17.0-SNAPSHOT")
classpath("dev.ahmedmourad.nocopy:nocopy-gradle-plugin:1.4.0")
}
}

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/deps.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ object deps {

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

const val viewBindingDelegate = "com.github.hoc081098:ViewBindingDelegate:1.2.0"
const val flowExt = "io.github.hoc081098:FlowExt:0.5.0"
const val timber = "com.jakewharton.timber:timber:5.0.1"

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

inline val DependencyHandler.domain get() = project(":domain")
inline val DependencyHandler.core get() = project(":core")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import androidx.compose.runtime.Stable
@OptIn(ExperimentalMaterial3Api::class)
@Stable
data class AppBarState(
val title: String?,
val title: @Composable () -> Unit,
val actions: @Composable RowScope.() -> Unit,
val navigationIcon: @Composable () -> Unit,
val colors: TopAppBarColors,
Expand Down
128 changes: 128 additions & 0 deletions core-ui/src/main/java/com/hoc/flowmvi/core_ui/AppBarTextField.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.hoc.flowmvi.core_ui

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBarTextField(
value: String,
onValueChange: (String) -> Unit,
hint: String,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
val colors = TextFieldDefaults.textFieldColors(containerColor = Color.Unspecified)

val textStyle = LocalTextStyle.current
// If color is not provided via the text style, use content color as a default
val textColor = textStyle.color.takeOrElse { MaterialTheme.colorScheme.onSurface }
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor, lineHeight = 50.sp))

val interactionSource = remember { MutableInteractionSource() }

// Holds the latest internal TextFieldValue state. We need to keep it to have the correct value
// of the composition.
// Set the correct cursor position when this composable is first initialized
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }

// Holds the latest TextFieldValue that BasicTextField was recomposed with. We couldn't simply
// pass `TextFieldValue(text = value)` to the CoreTextField because we need to preserve the
// composition.
val textFieldValue = textFieldValueState.copy(text = value)

SideEffect {
if (textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition
) {
textFieldValueState = textFieldValue
}
}
// Last String value that either text field was recomposed with or updated in the onValueChange
// callback. We keep track of it to prevent calling onValueChange(String) for same String when
// CoreTextField's onValueChange is called multiple times without recomposition in between.
var lastTextValue by remember(value) { mutableStateOf(value) }

// request focus when this composable is first initialized
val focusRequester = remember { FocusRequester() }
SideEffect { focusRequester.requestFocus() }

CompositionLocalProvider(LocalTextSelectionColors provides LocalTextSelectionColors.current) {
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState

val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text

if (stringChangedSinceLastInvocation) {
// remove newlines to avoid strange layout issues, and also because singleLine=true
onValueChange(newTextFieldValueState.text.replace("\n", ""))
}
},
modifier = modifier
.fillMaxWidth()
.heightIn(32.dp)
.indicatorLine(
enabled = true,
isError = false,
interactionSource = interactionSource,
colors = colors,
)
.focusRequester(focusRequester),
textStyle = mergedTextStyle,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
singleLine = true,
maxLines = 1,
decorationBox = @Composable { innerTextField ->
// places text field with placeholder and appropriate bottom padding
TextFieldDefaults.TextFieldDecorationBox(
value = value,
visualTransformation = VisualTransformation.None,
innerTextField = innerTextField,
placeholder = { Text(text = hint) },
singleLine = true,
enabled = true,
interactionSource = interactionSource,
colors = colors,
contentPadding = PaddingValues(bottom = 4.dp)
)
}
)
}
}
8 changes: 8 additions & 0 deletions core-ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<resources>
<string name="app_name">Compose MVI Coroutines Flow</string>
<string name="retry">RETRY</string>

<string name="invalid_id_error_message">Invalid id</string>
<string name="network_error_error_message">Network error</string>
<string name="server_error_error_message">Server error</string>
<string name="unexpected_error_error_message">Unexpected error</string>
<string name="user_not_found_error_message">User not found</string>
<string name="validation_failed_error_message">Validation failed</string>
<string name="add_user_success">Added user successfully</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ private fun ConfigAppBar(
) {
val title = stringResource(id = R.string.add_new_user)
val colors = TopAppBarDefaults.centerAlignedTopAppBarColors()

val appBarState = remember(colors, onBackClickState) {
AppBarState(
title = title,
title = { Text(title) },
actions = {},
navigationIcon = {
IconButton(onClick = { onBackClickState.value() }) {
Expand All @@ -159,6 +160,7 @@ private fun ConfigAppBar(
colors = colors
)
}

OnLifecycleEvent(configAppBar, appBarState) { _, event ->
if (event == Lifecycle.Event.ON_START) {
configAppBar(appBarState)
Expand Down
6 changes: 3 additions & 3 deletions feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ class AddVM @Inject constructor(
?: ViewState.initial()
Timber.tag(logTag).d("[ADD_VM] initialVS: $initialVS")

viewState = intentFlow
viewState = intentSharedFlow
.toPartialStateChangeFlow(initialVS)
.log("PartialStateChange")
.debugLog("PartialStateChange")
.sendSingleEvent()
.scan(initialVS) { state, change -> change.reduce(state) }
.onEach { savedStateHandle[VIEW_STATE] = it }
.log("ViewState")
.debugLog("ViewState")
.stateIn(viewModelScope, SharingStarted.Eagerly, initialVS)
}

Expand Down
8 changes: 0 additions & 8 deletions feature-add/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@
<resources>
<string name="add_new_user">Add new user</string>

<string name="invalid_id_error_message">Invalid id</string>
<string name="network_error_error_message">Network error</string>
<string name="server_error_error_message">Server error</string>
<string name="unexpected_error_error_message">Unexpected error</string>
<string name="user_not_found_error_message">User not found</string>
<string name="validation_failed_error_message">Validation failed</string>
<string name="add_user_success">Added user successfully</string>

<string name="invalid_email">Invalid email</string>
<string name="too_short_first_name">Too short first name</string>
<string name="too_short_last_name">Too short last name</string>
Expand Down
25 changes: 17 additions & 8 deletions feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,25 @@ internal sealed interface PartialStateChange {

override fun reduce(viewState: ViewState) = when (this) {
is Failure -> {
viewState.copy(
userItems = viewState.userItems.mutate { userItems ->
userItems.forEachIndexed { index, userItem ->
if (userItem.id == user.id) {
userItems[index] = userItem.copy(isDeleting = false)
return@mutate
// if the user is not found, remove it from the current list.
if (error is UserError.UserNotFound && error.id == user.id) {
viewState.copy(
userItems = viewState
.userItems
.removeAll { it.id == user.id }
)
} else {
viewState.copy(
userItems = viewState.userItems.mutate { userItems ->
userItems.forEachIndexed { index, userItem ->
if (userItem.id == user.id) {
userItems[index] = userItem.copy(isDeleting = false)
return@mutate
}
}
}
}
)
)
}
}
is Loading -> viewState.copy(
userItems = viewState.userItems.mutate { userItems ->
Expand Down
Loading