From e1f3762af458d9b2c229e24da895bee5313146c0 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 2 Aug 2023 18:36:36 +0300 Subject: [PATCH 1/3] refactor ui and navigation --- .../stslex/aproselection/SelectApplication.kt | 13 +++ .../controller/AuthController.kt | 11 ++ .../controller/AuthControllerImpl.kt | 46 ++++++++ .../com/stslex/aproselection/di/AppModule.kt | 14 +++ .../navigation/NavigationHost.kt | 62 +--------- .../com/stslex/aproselection/ui/InitialApp.kt | 43 ++++--- .../aproselection/ui/InitialAppViewModel.kt | 45 +++---- .../stslex/aproselection/ui/MainActivity.kt | 32 +++-- .../stslex/aproselection/DiKoinModuleTest.kt | 2 +- .../core/ui/components/ErrorSnackbar.kt | 102 ++++++++++++++++ .../aproselection/core/ui/core/Logger.kt | 21 ++++ .../{ => destination}/AppArguments.kt | 2 +- .../{ => destination}/AppDestination.kt | 13 ++- .../{ => destination}/NavigationScreen.kt | 7 +- .../core/ui/navigation/{ => ext}/NavExt.kt | 3 +- .../core/ui/navigation/navigator/Navigator.kt | 9 ++ .../ui/navigation/navigator/NavigatorImpl.kt | 31 +++++ .../aproselection/core/ui/theme/AppDimens.kt | 20 ++++ .../feature/auth/ui/AuthScreen.kt | 110 ++++++------------ .../feature/auth/ui/AuthViewModel.kt | 7 +- .../auth/ui/components/AuthBottomText.kt | 34 ++++++ .../auth/ui/components/AuthFieldsColumn.kt | 93 +++++++++++++++ .../auth/ui/components/AuthSubmitButton.kt | 59 ++++++++++ .../feature/auth/ui/components/AuthTitle.kt | 53 +++++++++ .../feature/auth/ui/model/AuthFieldsState.kt | 13 ++- .../auth/ui/model/screen/AuthScreenState.kt | 9 +- .../feature/auth/ui/navigation/AuthRouter.kt | 9 +- feature/auth/src/main/res/values/strings.xml | 6 +- .../feature/home/ui/HomeViewModel.kt | 2 - .../feature/home/ui/navigation/HomeRouter.kt | 9 +- 30 files changed, 653 insertions(+), 227 deletions(-) create mode 100644 app/src/main/java/com/stslex/aproselection/controller/AuthController.kt create mode 100644 app/src/main/java/com/stslex/aproselection/controller/AuthControllerImpl.kt create mode 100644 core/ui/src/main/java/com/stslex/aproselection/core/ui/components/ErrorSnackbar.kt create mode 100644 core/ui/src/main/java/com/stslex/aproselection/core/ui/core/Logger.kt rename core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/{ => destination}/AppArguments.kt (85%) rename core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/{ => destination}/AppDestination.kt (72%) rename core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/{ => destination}/NavigationScreen.kt (78%) rename core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/{ => ext}/NavExt.kt (80%) create mode 100644 core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/navigator/Navigator.kt create mode 100644 core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/navigator/NavigatorImpl.kt create mode 100644 core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/AppDimens.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthBottomText.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthFieldsColumn.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthSubmitButton.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthTitle.kt diff --git a/app/src/main/java/com/stslex/aproselection/SelectApplication.kt b/app/src/main/java/com/stslex/aproselection/SelectApplication.kt index 4422154..2f4dc44 100644 --- a/app/src/main/java/com/stslex/aproselection/SelectApplication.kt +++ b/app/src/main/java/com/stslex/aproselection/SelectApplication.kt @@ -1,19 +1,32 @@ package com.stslex.aproselection import android.app.Application +import com.stslex.aproselection.controller.AuthController import com.stslex.aproselection.core.datastore.coreDataStoreModule import com.stslex.aproselection.core.network.di.ModuleCoreNetwork.moduleCoreNetwork import com.stslex.aproselection.di.appModule import com.stslex.aproselection.feature.auth.di.ModuleFeatureAuth.moduleFeatureAuth import com.stslex.aproselection.feature.home.di.moduleFeatureHome +import org.koin.android.ext.android.inject import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin class SelectApplication : Application() { + private val appController: AuthController by inject() + override fun onCreate() { super.onCreate() + setupDependencies() + initControllers() + } + + private fun initControllers() { + appController.init() + } + + private fun setupDependencies() { startKoin { androidLogger() androidContext(applicationContext) diff --git a/app/src/main/java/com/stslex/aproselection/controller/AuthController.kt b/app/src/main/java/com/stslex/aproselection/controller/AuthController.kt new file mode 100644 index 0000000..a9269d5 --- /dev/null +++ b/app/src/main/java/com/stslex/aproselection/controller/AuthController.kt @@ -0,0 +1,11 @@ +package com.stslex.aproselection.controller + +import kotlinx.coroutines.flow.StateFlow + +interface AuthController { + + val isAuth: StateFlow + + fun init() +} + diff --git a/app/src/main/java/com/stslex/aproselection/controller/AuthControllerImpl.kt b/app/src/main/java/com/stslex/aproselection/controller/AuthControllerImpl.kt new file mode 100644 index 0000000..5067b85 --- /dev/null +++ b/app/src/main/java/com/stslex/aproselection/controller/AuthControllerImpl.kt @@ -0,0 +1,46 @@ +package com.stslex.aproselection.controller + +import com.stslex.aproselection.core.datastore.AppDataStore +import com.stslex.aproselection.core.network.client.NetworkClient +import com.stslex.aproselection.core.ui.core.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class AuthControllerImpl( + private val dataStore: AppDataStore, + private val networkClient: NetworkClient, +) : AuthController { + + private val scope = CoroutineScope(SupervisorJob()) + private val _isAuth = MutableStateFlow(null) + override val isAuth: StateFlow + get() = _isAuth.asStateFlow() + + override fun init() { + dataStore.token + .catch { error -> + Logger.exception(error) + } + .onEach { token -> + if (token.isBlank()) { + networkClient.regenerateToken() + } + } + .launchIn(scope) + + dataStore.uuid + .catch { error -> + Logger.exception(error) + } + .onEach { uuid -> + _isAuth.emit(uuid.isNotBlank()) + } + .launchIn(scope) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/stslex/aproselection/di/AppModule.kt b/app/src/main/java/com/stslex/aproselection/di/AppModule.kt index c6e6376..782b023 100644 --- a/app/src/main/java/com/stslex/aproselection/di/AppModule.kt +++ b/app/src/main/java/com/stslex/aproselection/di/AppModule.kt @@ -1,9 +1,23 @@ package com.stslex.aproselection.di +import androidx.navigation.NavHostController +import com.stslex.aproselection.controller.AuthController +import com.stslex.aproselection.controller.AuthControllerImpl +import com.stslex.aproselection.core.ui.navigation.navigator.Navigator +import com.stslex.aproselection.core.ui.navigation.navigator.NavigatorImpl import com.stslex.aproselection.ui.InitialAppViewModel import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val appModule = module { + singleOf(::AuthControllerImpl) { bind() } +} + +fun navigationModule( + navHostController: NavHostController +) = module { + single { NavigatorImpl(navHostController) } viewModelOf(::InitialAppViewModel) } \ No newline at end of file diff --git a/app/src/main/java/com/stslex/aproselection/navigation/NavigationHost.kt b/app/src/main/java/com/stslex/aproselection/navigation/NavigationHost.kt index c41fecb..c423e36 100644 --- a/app/src/main/java/com/stslex/aproselection/navigation/NavigationHost.kt +++ b/app/src/main/java/com/stslex/aproselection/navigation/NavigationHost.kt @@ -1,78 +1,24 @@ package com.stslex.aproselection.navigation -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import com.stslex.aproselection.core.ui.navigation.AppDestination -import com.stslex.aproselection.core.ui.navigation.NavigationScreen +import com.stslex.aproselection.core.ui.navigation.destination.AppDestination import com.stslex.aproselection.feature.auth.ui.navigation.authRouter import com.stslex.aproselection.feature.home.ui.navigation.homeRouter -import com.stslex.aproselection.ui.InitialAppViewModel -import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Composable fun NavigationHost( navController: NavHostController, + startDestination: AppDestination, modifier: Modifier = Modifier, - startDestination: AppDestination = AppDestination.SPLASH ) { - val navigator: (NavigationScreen) -> Unit = { screen -> - when (screen) { - is NavigationScreen.PopBackStack -> navController.popBackStack() - else -> navController.navigateScreen(screen) - } - } - - val viewModel = koinViewModel( - parameters = { parametersOf(navigator) } - ) - val isAuth by remember { - viewModel.isAuth - }.collectAsState() - NavHost( navController = navController, startDestination = startDestination.route ) { - composable(NavigationScreen.Splash.screenRoute) { - Box( - contentAlignment = Alignment.Center - ) { - Text(text = "splash") // TODO replace with norm navigator - } - val screen = when (isAuth) { - true -> NavigationScreen.Home - false -> NavigationScreen.Auth - null -> null - } - screen?.let(navigator) - } - authRouter(modifier, navigator) - homeRouter(modifier, navigator) + authRouter(modifier) + homeRouter(modifier) } } - -fun NavHostController.navigateScreen(screen: NavigationScreen) { - val currentRoute = this.currentDestination?.route - if (currentRoute == screen.screenRoute) return - navigate(screen.screenRoute) { - if (screen.isSingleTop.not()) return@navigate - currentRoute?.let { route -> - popUpTo(route) { - inclusive = true - saveState = true - } - } - - launchSingleTop = true - } -} \ No newline at end of file diff --git a/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt b/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt index 204e964..9db8cf1 100644 --- a/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt +++ b/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt @@ -1,27 +1,34 @@ package com.stslex.aproselection.ui -import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import androidx.navigation.compose.rememberNavController -import com.stslex.aproselection.core.ui.theme.AppTheme +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import com.stslex.aproselection.core.ui.navigation.destination.AppDestination import com.stslex.aproselection.navigation.NavigationHost +import org.koin.androidx.compose.koinViewModel @Composable -fun InitialApp() { - val navController = rememberNavController() +fun InitialApp( + navController: NavHostController +) { + val viewModel: InitialAppViewModel = koinViewModel() + val isInitialAuth by remember { + viewModel.isInitialAuth + }.collectAsState() - Box { - NavigationHost( - navController = navController, - ) + LaunchedEffect(Unit) { + viewModel.init() } -} -@Preview(showBackground = true) -@Composable -fun InitialAppPreview() { - AppTheme { - InitialApp() - } -} \ No newline at end of file + AppDestination + .getStartDestination(isInitialAuth) + ?.let { destination -> + NavigationHost( + navController = navController, + startDestination = destination + ) + } +} diff --git a/app/src/main/java/com/stslex/aproselection/ui/InitialAppViewModel.kt b/app/src/main/java/com/stslex/aproselection/ui/InitialAppViewModel.kt index 1a784c2..3ac651b 100644 --- a/app/src/main/java/com/stslex/aproselection/ui/InitialAppViewModel.kt +++ b/app/src/main/java/com/stslex/aproselection/ui/InitialAppViewModel.kt @@ -1,48 +1,33 @@ package com.stslex.aproselection.ui import androidx.lifecycle.viewModelScope -import com.stslex.aproselection.core.datastore.AppDataStore -import com.stslex.aproselection.core.network.client.NetworkClient +import com.stslex.aproselection.controller.AuthController import com.stslex.aproselection.core.ui.base.BaseViewModel -import com.stslex.aproselection.core.ui.navigation.NavigationScreen +import com.stslex.aproselection.core.ui.navigation.destination.NavigationScreen +import com.stslex.aproselection.core.ui.navigation.navigator.Navigator import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach class InitialAppViewModel( - dataStore: AppDataStore, - navigate: (NavigationScreen) -> Unit, - networkClient: NetworkClient, + private val navigator: Navigator, + private val controller: AuthController ) : BaseViewModel() { - private val _isAuth = MutableStateFlow(null) - val isAuth: StateFlow - get() = _isAuth.asStateFlow() + private val _isInitialAuth = MutableStateFlow(null) + val isInitialAuth: StateFlow + get() = _isInitialAuth.asStateFlow() - // TODO (вынести из init блока) - init { - dataStore.token - .catch { error -> - handleError(error) - } - .onEach { token -> - if (token.isBlank()) { - networkClient.regenerateToken() + fun init() { + controller.isAuth + .onEach { isAuth -> + if (isInitialAuth.value == null) { + _isInitialAuth.emit(isAuth) } - } - .launchIn(viewModelScope) - - dataStore.uuid - .catch { error -> - handleError(error) - } - .onEach { uuid -> - _isAuth.emit(uuid.isNotBlank()) - if (uuid.isBlank()) { - navigate(NavigationScreen.Auth) + if (isAuth == false) { + navigator.navigate(NavigationScreen.Auth) } } .launchIn(viewModelScope) diff --git a/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt b/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt index 7f226d1..d95a918 100644 --- a/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt +++ b/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt @@ -1,30 +1,38 @@ package com.stslex.aproselection.ui import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.lifecycle.lifecycleScope -import com.stslex.aproselection.core.datastore.AppDataStore -import com.stslex.aproselection.core.network.client.NetworkClient +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController import com.stslex.aproselection.core.ui.theme.AppTheme -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.koin.android.ext.android.inject +import com.stslex.aproselection.di.navigationModule +import org.koin.androidx.compose.getKoin class MainActivity : ComponentActivity() { - private val dataStore by inject() - private val networkClient by inject() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + val navHostController = rememberNavController() + SetupComposeDependencies(navHostController) AppTheme { - InitialApp() + InitialApp( + navController = navHostController + ) } } } + + @Composable + private fun SetupComposeDependencies( + navController: NavHostController + ) { + val navModule = navigationModule(navController) + getKoin().loadModules( + listOf(navModule) + ) + } } diff --git a/app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt b/app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt index df90d9c..767eda5 100644 --- a/app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt +++ b/app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt @@ -3,7 +3,7 @@ package com.stslex.aproselection import android.content.Context import com.stslex.aproselection.core.datastore.coreDataStoreModule import com.stslex.aproselection.core.network.di.ModuleCoreNetwork.moduleCoreNetwork -import com.stslex.aproselection.core.ui.navigation.NavigationScreen +import com.stslex.aproselection.core.ui.navigation.destination.NavigationScreen import com.stslex.aproselection.di.appModule import com.stslex.aproselection.feature.auth.di.ModuleFeatureAuth.moduleFeatureAuth import com.stslex.aproselection.feature.home.di.moduleFeatureHome diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/components/ErrorSnackbar.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/components/ErrorSnackbar.kt new file mode 100644 index 0000000..656c5cd --- /dev/null +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/components/ErrorSnackbar.kt @@ -0,0 +1,102 @@ +package com.stslex.aproselection.core.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarVisuals +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.stslex.aproselection.core.ui.theme.AppDimens +import com.stslex.aproselection.core.ui.theme.AppTheme + +@Composable +fun ErrorSnackbar( + snackbarData: SnackbarData, + modifier: Modifier = Modifier, +) { + val isDarkTheme = isSystemInDarkTheme() + val containerColor = if (isDarkTheme) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.errorContainer + } + val contentColor = if (isDarkTheme) { + MaterialTheme.colorScheme.onError + } else { + MaterialTheme.colorScheme.onErrorContainer + } + Snackbar( + modifier = modifier + .padding(AppDimens.Padding.medium) + .clip(RoundedCornerShape(AppDimens.Corners.big)), + containerColor = containerColor, + contentColor = contentColor + ) { + Row { + Icon( + Icons.Default.Warning, + contentDescription = null, + ) + Spacer(modifier = Modifier.width(AppDimens.Padding.medium)) + Text( + text = snackbarData.visuals.message, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Preview( + device = "id:pixel_6a", showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL +) +@Composable +fun ErrorSnackbarPreview() { + val data = object : SnackbarData { + override val visuals: SnackbarVisuals + get() = object : SnackbarVisuals { + override val actionLabel: String = "action label" + override val duration: SnackbarDuration = SnackbarDuration.Short + override val message: String = "message message message message message message" + override val withDismissAction: Boolean = false + } + + override fun dismiss() = Unit + + override fun performAction() = Unit + } + AppTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + ErrorSnackbar( + modifier = Modifier.align(Alignment.BottomCenter), + snackbarData = data + ) + } + } +} + diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/core/Logger.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/core/Logger.kt new file mode 100644 index 0000000..785c443 --- /dev/null +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/core/Logger.kt @@ -0,0 +1,21 @@ +package com.stslex.aproselection.core.ui.core + +import android.util.Log + +object Logger { + + private const val DEFAULT_TAG = "PRO_SELECTION" + + fun exception( + throwable: Throwable, + tag: String? = null, + message: String? = null + ) { + val currentTag = "$DEFAULT_TAG:${tag.orEmpty()}" + Log.e( + currentTag, + message ?: throwable.message.orEmpty(), + throwable, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/AppArguments.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/AppArguments.kt similarity index 85% rename from core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/AppArguments.kt rename to core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/AppArguments.kt index b030ad8..d63c38e 100644 --- a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/AppArguments.kt +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/AppArguments.kt @@ -1,4 +1,4 @@ -package com.stslex.aproselection.core.ui.navigation +package com.stslex.aproselection.core.ui.navigation.destination sealed class AppArguments { diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/AppDestination.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/AppDestination.kt similarity index 72% rename from core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/AppDestination.kt rename to core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/AppDestination.kt index d9d8d3b..0e3db8a 100644 --- a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/AppDestination.kt +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/AppDestination.kt @@ -1,11 +1,10 @@ -package com.stslex.aproselection.core.ui.navigation +package com.stslex.aproselection.core.ui.navigation.destination enum class AppDestination( vararg val argsNames: String ) { AUTH, - HOME, - SPLASH; + HOME; val route: String get() = StringBuilder() @@ -26,5 +25,13 @@ enum class AppDestination( companion object { private const val SEPARATOR_ROUTE = "_" private const val TAG_ROUTE = "route" + + fun getStartDestination( + isAuth: Boolean? + ) = when (isAuth) { + true -> HOME + false -> AUTH + null -> null + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/NavigationScreen.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/NavigationScreen.kt similarity index 78% rename from core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/NavigationScreen.kt rename to core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/NavigationScreen.kt index 2520e83..c9ac2be 100644 --- a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/NavigationScreen.kt +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/destination/NavigationScreen.kt @@ -1,4 +1,4 @@ -package com.stslex.aproselection.core.ui.navigation +package com.stslex.aproselection.core.ui.navigation.destination sealed interface NavigationScreen { @@ -23,11 +23,6 @@ sealed interface NavigationScreen { override val isSingleTop: Boolean = true } - data object Splash: NavigationScreen{ - override val screen: AppDestination = AppDestination.SPLASH - override val isSingleTop: Boolean = true - } - data object PopBackStack : NavigationScreen { override val screen: AppDestination = throw Exception("PopBackStack") override val appArgs: AppArguments = throw Exception("PopBackStack") diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/NavExt.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/ext/NavExt.kt similarity index 80% rename from core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/NavExt.kt rename to core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/ext/NavExt.kt index c920fb4..4dfbda9 100644 --- a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/NavExt.kt +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/ext/NavExt.kt @@ -1,9 +1,10 @@ -package com.stslex.aproselection.core.ui.navigation +package com.stslex.aproselection.core.ui.navigation.ext import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry import androidx.navigation.NavType import androidx.navigation.navArgument +import com.stslex.aproselection.core.ui.navigation.destination.AppDestination object NavExt { diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/navigator/Navigator.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/navigator/Navigator.kt new file mode 100644 index 0000000..f52061e --- /dev/null +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/navigator/Navigator.kt @@ -0,0 +1,9 @@ +package com.stslex.aproselection.core.ui.navigation.navigator + +import com.stslex.aproselection.core.ui.navigation.destination.NavigationScreen + +interface Navigator { + + fun navigate(screen: NavigationScreen) +} + diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/navigator/NavigatorImpl.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/navigator/NavigatorImpl.kt new file mode 100644 index 0000000..adf649e --- /dev/null +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/navigation/navigator/NavigatorImpl.kt @@ -0,0 +1,31 @@ +package com.stslex.aproselection.core.ui.navigation.navigator + +import androidx.navigation.NavHostController +import com.stslex.aproselection.core.ui.navigation.destination.NavigationScreen + +class NavigatorImpl( + private val navController: NavHostController +) : Navigator { + + override fun navigate(screen: NavigationScreen) { + when (screen) { + is NavigationScreen.PopBackStack -> navController.popBackStack() + else -> navigateScreen(screen) + } + } + + private fun navigateScreen(screen: NavigationScreen) { + val currentRoute = navController.currentDestination?.route ?: return + if (currentRoute == screen.screenRoute) return + + navController.navigate(screen.screenRoute) { + if (screen.isSingleTop.not()) return@navigate + + popUpTo(currentRoute) { + inclusive = true + saveState = true + } + launchSingleTop = true + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/AppDimens.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/AppDimens.kt new file mode 100644 index 0000000..d0b5af4 --- /dev/null +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/AppDimens.kt @@ -0,0 +1,20 @@ +package com.stslex.aproselection.core.ui.theme + +import androidx.compose.ui.unit.dp + +object AppDimens { + + object Padding { + + val small = 8.dp + val medium = 16.dp + val big = 32.dp + } + + object Corners { + + val small = 5.dp + val medium = 10.dp + val big = 15.dp + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt index 85689cd..318a48c 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt @@ -1,32 +1,27 @@ package com.stslex.aproselection.feature.auth.ui -import androidx.compose.animation.AnimatedVisibility +import android.annotation.SuppressLint +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Divider -import androidx.compose.material3.ElevatedButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp +import com.stslex.aproselection.core.ui.components.ErrorSnackbar +import com.stslex.aproselection.core.ui.theme.AppDimens +import com.stslex.aproselection.core.ui.theme.AppTheme +import com.stslex.aproselection.feature.auth.ui.components.AuthBottomText +import com.stslex.aproselection.feature.auth.ui.components.AuthFieldsColumn +import com.stslex.aproselection.feature.auth.ui.components.AuthTitle import com.stslex.aproselection.feature.auth.ui.model.ScreenLoadingState import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenState import com.stslex.aproselection.feature.auth.ui.model.screen.AuthScreenState @@ -34,6 +29,7 @@ import com.stslex.aproselection.feature.auth.ui.model.screen.rememberAuthScreenS import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +@SuppressLint("CoroutineCreationDuringComposition") @Composable fun AuthScreen( state: AuthScreenState, @@ -47,14 +43,10 @@ fun AuthScreen( ) { AuthScreenContent(state) SnackbarHost( - modifier = Modifier - .padding(16.dp) - .align(Alignment.BottomCenter), + modifier = Modifier.align(Alignment.BottomCenter), hostState = state.snackbarHostState ) { snackbarData -> - Snackbar { - Text(text = snackbarData.visuals.message) - } + ErrorSnackbar(snackbarData) } } if (state.screenLoadingState == ScreenLoadingState.Loading) { @@ -74,56 +66,25 @@ fun AuthScreenContent( state: AuthScreenState, modifier: Modifier = Modifier ) { - - val buttonRes by remember { - derivedStateOf { state.authFieldsState.inverse.buttonResId } - } - - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally + Box( + modifier = modifier + .fillMaxSize() + .padding(AppDimens.Padding.big) ) { - AuthUsernameTextField( - inputUsername = state.username, - onTextChange = state::onUsernameChange + AuthTitle( + modifier = Modifier.align(Alignment.TopCenter), + state = state.authFieldsState ) - Divider(Modifier.padding(16.dp)) - AuthPasswordTextField( - inputPassword = state.password, - onTextChange = state::onPasswordChange + AuthFieldsColumn( + modifier = Modifier.align(Alignment.Center), + state = state ) - AnimatedVisibility( - visible = state.isRegisterState - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(8.dp)) - AuthPasswordTextField( - inputPassword = state.passwordSubmit, - onTextChange = state::onPasswordSubmitChange - ) - } - } - Divider(Modifier.padding(16.dp)) - ElevatedButton( - onClick = state::onSubmitClicked, - enabled = state.isFieldsValid - ) { - Text( - text = "submit", - style = MaterialTheme.typography.headlineMedium - ) - } - Divider(Modifier.padding(16.dp)) - TextButton( + AuthBottomText( + modifier = Modifier + .align(Alignment.BottomCenter), + authFieldsState = state.authFieldsState, onClick = state::onAuthFieldChange - ) { - Text( - text = stringResource(id = buttonRes), - style = MaterialTheme.typography.titleMedium - ) - } + ) } } @@ -173,14 +134,19 @@ fun AuthPasswordTextField( ) } -@Preview(device = "id:pixel_6", showSystemUi = true, showBackground = true) +@Preview( + device = "id:pixel_6", showSystemUi = true, showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL +) @Composable fun AuthScreenPreview() { - AuthScreen( - state = rememberAuthScreenState( - screenStateFlow = { MutableStateFlow(ScreenState()) }, - screenEventFlow = { MutableSharedFlow() }, - processAction = {} + AppTheme { + AuthScreen( + state = rememberAuthScreenState( + screenStateFlow = { MutableStateFlow(ScreenState()) }, + screenEventFlow = { MutableSharedFlow() }, + processAction = {} + ) ) - ) + } } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt index 11a54d5..690ca6f 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt @@ -2,7 +2,8 @@ package com.stslex.aproselection.feature.auth.ui import androidx.lifecycle.viewModelScope import com.stslex.aproselection.core.ui.base.BaseViewModel -import com.stslex.aproselection.core.ui.navigation.NavigationScreen +import com.stslex.aproselection.core.ui.navigation.destination.NavigationScreen +import com.stslex.aproselection.core.ui.navigation.navigator.Navigator import com.stslex.aproselection.feature.auth.domain.interactor.AuthInteractor import com.stslex.aproselection.feature.auth.ui.model.AuthFieldsState import com.stslex.aproselection.feature.auth.ui.model.ErrorType @@ -23,7 +24,7 @@ import kotlinx.coroutines.flow.update class AuthViewModel( private val interactor: AuthInteractor, - private val navigate: (NavigationScreen) -> Unit + private val navigator: Navigator ) : BaseViewModel() { private val _screenState = MutableStateFlow(ScreenState()) @@ -119,7 +120,7 @@ class AuthViewModel( } .onEach { setLoadingState(ScreenLoadingState.Content) - navigate(NavigationScreen.Home) + navigator.navigate(NavigationScreen.Home) } .launchIn(viewModelScope) } diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthBottomText.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthBottomText.kt new file mode 100644 index 0000000..2893b7b --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthBottomText.kt @@ -0,0 +1,34 @@ +package com.stslex.aproselection.feature.auth.ui.components + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import com.stslex.aproselection.feature.auth.ui.model.AuthFieldsState + +@Composable +fun AuthBottomText( + authFieldsState: AuthFieldsState, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val haptic = LocalHapticFeedback.current + TextButton( + modifier = modifier, + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + onClick() + } + ) { + Text( + text = stringResource( + id = authFieldsState.inverse.buttonResId + ), + style = MaterialTheme.typography.titleMedium + ) + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthFieldsColumn.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthFieldsColumn.kt new file mode 100644 index 0000000..e6aeadb --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthFieldsColumn.kt @@ -0,0 +1,93 @@ +package com.stslex.aproselection.feature.auth.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.stslex.aproselection.core.ui.theme.AppDimens +import com.stslex.aproselection.core.ui.theme.AppTheme +import com.stslex.aproselection.feature.auth.ui.AuthPasswordTextField +import com.stslex.aproselection.feature.auth.ui.AuthUsernameTextField +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenState +import com.stslex.aproselection.feature.auth.ui.model.screen.AuthScreenState +import com.stslex.aproselection.feature.auth.ui.model.screen.rememberAuthScreenState +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow + +@Composable +fun AuthFieldsColumn( + state: AuthScreenState, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + AuthUsernameTextField( + inputUsername = state.username, + onTextChange = state::onUsernameChange + ) + Spacer(Modifier.height(AppDimens.Padding.medium)) + AuthPasswordTextField( + inputPassword = state.password, + onTextChange = state::onPasswordChange + ) + AnimatedVisibility( + visible = state.isRegisterState + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(AppDimens.Padding.small)) + AuthPasswordTextField( + inputPassword = state.passwordSubmit, + onTextChange = state::onPasswordSubmitChange + ) + } + } + Spacer(Modifier.height(AppDimens.Padding.big)) + AuthSubmitButton( + isValid = state.isFieldsValid, + onClick = state::onSubmitClicked, + ) + } +} + +@Preview( + device = "id:pixel_6a", showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL +) +@Composable +fun AuthFieldsColumnPreview() { + AppTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background( + MaterialTheme.colorScheme.background + ) + ) { + AuthFieldsColumn( + modifier = Modifier.align(Alignment.Center), + state = rememberAuthScreenState( + screenStateFlow = { + MutableStateFlow(ScreenState()) + }, + screenEventFlow = { + MutableSharedFlow() + }, + processAction = {} + ) + ) + } + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthSubmitButton.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthSubmitButton.kt new file mode 100644 index 0000000..2d685bb --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthSubmitButton.kt @@ -0,0 +1,59 @@ +package com.stslex.aproselection.feature.auth.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.tooling.preview.Preview +import com.stslex.aproselection.core.ui.theme.AppTheme + +@Composable +fun AuthSubmitButton( + isValid: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val haptic = LocalHapticFeedback.current + FilledTonalButton( + modifier = modifier, + onClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onClick() + }, + enabled = isValid + ) { + Text( + text = "submit", + style = MaterialTheme.typography.headlineSmall + ) + } +} + +@Preview( + device = "id:pixel_6a", showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL +) +@Composable +fun AuthSubmitButtonPreview() { + AppTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + AuthSubmitButton( + isValid = true, + onClick = { /*TODO*/ } + ) + } + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthTitle.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthTitle.kt new file mode 100644 index 0000000..723badf --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthTitle.kt @@ -0,0 +1,53 @@ +package com.stslex.aproselection.feature.auth.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.stslex.aproselection.core.ui.theme.AppDimens +import com.stslex.aproselection.core.ui.theme.AppTheme +import com.stslex.aproselection.feature.auth.ui.model.AuthFieldsState + +@Composable +fun AuthTitle( + state: AuthFieldsState, + modifier: Modifier = Modifier +) { + Text( + modifier = modifier.padding( + top = AppDimens.Padding.big + ), + text = stringResource(id = state.titleResId), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1 + ) +} + +@Preview( + device = "id:pixel_6", showSystemUi = true, showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL +) +@Composable +fun AuthScreenPreview() { + AppTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + AuthTitle( + modifier = Modifier.align(Alignment.TopCenter), + state = AuthFieldsState.AUTH + ) + } + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/AuthFieldsState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/AuthFieldsState.kt index 934331a..fb5f35a 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/AuthFieldsState.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/AuthFieldsState.kt @@ -4,10 +4,17 @@ import androidx.annotation.StringRes import com.stslex.aproselection.feature.auth.R enum class AuthFieldsState( - @StringRes val buttonResId: Int + @StringRes val buttonResId: Int, + @StringRes val titleResId: Int ) { - AUTH(R.string.auth), - REGISTER(R.string.register); + AUTH( + buttonResId = R.string.auth_button_choose_log_in, + titleResId = R.string.auth_title_auth + ), + REGISTER( + buttonResId = R.string.auth_button_choose_register, + titleResId = R.string.auth_title_register + ); val inverse: AuthFieldsState get() = when (this) { diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt index 7c63abe..ccacfe2 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt @@ -31,7 +31,14 @@ data class AuthScreenState( private val processAction: (ScreenAction) -> Unit, private val keyboardController: SoftwareKeyboardController? = null ) { - val isFieldsValid = username.length >= 4 && password.length >= 4 + val isFieldsValid: Boolean + get() { + val isCorrectLength = username.length >= 4 && password.length >= 4 + val isEqualsPasswords = password == passwordSubmit + val isRegisterPassword = authFieldsState == AuthFieldsState.AUTH || isEqualsPasswords + return isCorrectLength && isRegisterPassword + } + val isRegisterState = authFieldsState == AuthFieldsState.REGISTER fun onUsernameChange(username: String) { diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/navigation/AuthRouter.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/navigation/AuthRouter.kt index ab7605c..7a52558 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/navigation/AuthRouter.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/navigation/AuthRouter.kt @@ -3,24 +3,19 @@ package com.stslex.aproselection.feature.auth.ui.navigation import androidx.compose.ui.Modifier import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.stslex.aproselection.core.ui.navigation.AppDestination -import com.stslex.aproselection.core.ui.navigation.NavigationScreen +import com.stslex.aproselection.core.ui.navigation.destination.AppDestination import com.stslex.aproselection.feature.auth.ui.AuthScreen import com.stslex.aproselection.feature.auth.ui.AuthViewModel import com.stslex.aproselection.feature.auth.ui.model.screen.rememberAuthScreenState import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf fun NavGraphBuilder.authRouter( modifier: Modifier = Modifier, - navigate: (NavigationScreen) -> Unit ) { composable( route = AppDestination.AUTH.navigationRoute ) { - val viewModel: AuthViewModel = koinViewModel( - parameters = { parametersOf(navigate) } - ) + val viewModel: AuthViewModel = koinViewModel() val authScreenState = rememberAuthScreenState( screenStateFlow = viewModel::screenState, diff --git a/feature/auth/src/main/res/values/strings.xml b/feature/auth/src/main/res/values/strings.xml index 2a54413..c73d54a 100644 --- a/feature/auth/src/main/res/values/strings.xml +++ b/feature/auth/src/main/res/values/strings.xml @@ -1,5 +1,7 @@ - log in - register (if not have account) + log in (if not have account) + register (if not have account) + Authentication + Registration \ No newline at end of file diff --git a/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/HomeViewModel.kt b/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/HomeViewModel.kt index 09cb017..2459951 100644 --- a/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/HomeViewModel.kt +++ b/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/HomeViewModel.kt @@ -3,12 +3,10 @@ package com.stslex.aproselection.feature.home.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.stslex.aproselection.core.datastore.AppDataStore -import com.stslex.aproselection.core.ui.navigation.NavigationScreen import kotlinx.coroutines.launch class HomeViewModel( private val appDataStore: AppDataStore, - private val navigation: (NavigationScreen) -> Unit, ) : ViewModel() { fun logOut() { diff --git a/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/navigation/HomeRouter.kt b/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/navigation/HomeRouter.kt index 02df7fd..0dc2923 100644 --- a/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/navigation/HomeRouter.kt +++ b/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/navigation/HomeRouter.kt @@ -3,23 +3,18 @@ package com.stslex.aproselection.feature.home.ui.navigation import androidx.compose.ui.Modifier import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.stslex.aproselection.core.ui.navigation.AppDestination -import com.stslex.aproselection.core.ui.navigation.NavigationScreen +import com.stslex.aproselection.core.ui.navigation.destination.AppDestination import com.stslex.aproselection.feature.home.ui.HomeScreen import com.stslex.aproselection.feature.home.ui.HomeViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf fun NavGraphBuilder.homeRouter( modifier: Modifier = Modifier, - navigate: (NavigationScreen) -> Unit ) { composable( route = AppDestination.HOME.navigationRoute ) { - val viewModel: HomeViewModel = koinViewModel( - parameters = { parametersOf(navigate) } - ) + val viewModel: HomeViewModel = koinViewModel() HomeScreen( modifier = modifier, From 8191b9be77938a294fcae5a7464eb685f5317239 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 2 Aug 2023 19:52:57 +0300 Subject: [PATCH 2/3] refactor auth screen ui - password and username input --- .../core/ui/components/ErrorSnackbar.kt | 1 + .../aproselection/core/ui/theme/AppDimens.kt | 1 + .../feature/auth/ui/AuthScreen.kt | 59 +++++------ .../feature/auth/ui/AuthViewModel.kt | 17 ++-- .../auth/ui/components/AuthFieldsColumn.kt | 23 ++--- .../ui/components/AuthPasswordTextField.kt | 99 +++++++++++++++++++ .../feature/auth/ui/model/mvi/ScreenAction.kt | 27 ++--- .../auth/ui/model/screen/AuthScreenState.kt | 61 +++++++----- .../model/screen/text_field/AuthTextField.kt | 20 ++++ .../text_field/PasswordInputTextFieldState.kt | 34 +++++++ .../PasswordSubmitTextFieldState.kt | 34 +++++++ .../text_field/PasswordTextFieldState.kt | 41 ++++++++ .../text_field/UsernameTextFieldState.kt | 30 ++++++ .../res/drawable/baseline_visibility_24.xml | 5 + .../drawable/baseline_visibility_off_24.xml | 5 + feature/auth/src/main/res/values/strings.xml | 4 + 16 files changed, 370 insertions(+), 91 deletions(-) create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthPasswordTextField.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/AuthTextField.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordTextFieldState.kt create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt create mode 100644 feature/auth/src/main/res/drawable/baseline_visibility_24.xml create mode 100644 feature/auth/src/main/res/drawable/baseline_visibility_off_24.xml diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/components/ErrorSnackbar.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/components/ErrorSnackbar.kt index 656c5cd..40952c8 100644 --- a/core/ui/src/main/java/com/stslex/aproselection/core/ui/components/ErrorSnackbar.kt +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/components/ErrorSnackbar.kt @@ -44,6 +44,7 @@ fun ErrorSnackbar( } else { MaterialTheme.colorScheme.onErrorContainer } + Snackbar( modifier = modifier .padding(AppDimens.Padding.medium) diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/AppDimens.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/AppDimens.kt index d0b5af4..2a967b9 100644 --- a/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/AppDimens.kt +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/theme/AppDimens.kt @@ -6,6 +6,7 @@ object AppDimens { object Padding { + val smallest = 4.dp val small = 8.dp val medium = 16.dp val big = 32.dp diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt index 318a48c..cbce505 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthScreen.kt @@ -5,7 +5,9 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost @@ -14,11 +16,14 @@ import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import com.stslex.aproselection.core.ui.components.ErrorSnackbar import com.stslex.aproselection.core.ui.theme.AppDimens import com.stslex.aproselection.core.ui.theme.AppTheme +import com.stslex.aproselection.feature.auth.R import com.stslex.aproselection.feature.auth.ui.components.AuthBottomText import com.stslex.aproselection.feature.auth.ui.components.AuthFieldsColumn import com.stslex.aproselection.feature.auth.ui.components.AuthTitle @@ -26,6 +31,7 @@ import com.stslex.aproselection.feature.auth.ui.model.ScreenLoadingState import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenState import com.stslex.aproselection.feature.auth.ui.model.screen.AuthScreenState import com.stslex.aproselection.feature.auth.ui.model.screen.rememberAuthScreenState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.UsernameTextFieldState import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -90,47 +96,32 @@ fun AuthScreenContent( @Composable fun AuthUsernameTextField( - inputUsername: String, - onTextChange: (String) -> Unit, + state: UsernameTextFieldState, modifier: Modifier = Modifier ) { TextField( - modifier = modifier, - value = inputUsername, - onValueChange = { value -> - if (inputUsername != value) { - onTextChange(value) - } - }, + modifier = modifier + .fillMaxWidth(), + value = state.text, + onValueChange = state::onTextChange, singleLine = true, label = { - Text(text = "username") - } - ) -} - -@Composable -fun AuthPasswordTextField( - inputPassword: String, - onTextChange: (String) -> Unit, - modifier: Modifier = Modifier -) { - TextField( - modifier = modifier, - value = inputPassword, - onValueChange = { value -> - if (inputPassword != value) { - onTextChange(value) - } + Text( + text = stringResource(id = R.string.auth_username_text) + ) }, - singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), supportingText = { - Text(text = "enter password") + Box(modifier = Modifier.fillMaxWidth()) { + Text( + modifier = Modifier.align(Alignment.CenterEnd), + text = state.supportingEndText + ) + } }, - visualTransformation = PasswordVisualTransformation(), - label = { - Text(text = "password") - } ) } diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt index 690ca6f..9664ed4 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/AuthViewModel.kt @@ -9,6 +9,9 @@ import com.stslex.aproselection.feature.auth.ui.model.AuthFieldsState import com.stslex.aproselection.feature.auth.ui.model.ErrorType import com.stslex.aproselection.feature.auth.ui.model.ScreenLoadingState import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.PasswordInput +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.PasswordSubmitInput +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.UsernameInput import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenEvent import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenState import kotlinx.coroutines.flow.MutableSharedFlow @@ -37,15 +40,15 @@ class AuthViewModel( fun process(action: ScreenAction) { when (action) { - is ScreenAction.UsernameInput -> processUsernameInput(action) + is UsernameInput -> processUsernameInput(action) is ScreenAction.OnSubmitClicked -> processSubmitClicked() - is ScreenAction.PasswordInput -> processPasswordInput(action) - is ScreenAction.PasswordSubmitInput -> processPasswordSubmitInput(action) + is PasswordInput -> processPasswordInput(action) + is PasswordSubmitInput -> processPasswordSubmitInput(action) is ScreenAction.OnAuthFieldChange -> processAuthFieldChange() } } - private fun processUsernameInput(action: ScreenAction.UsernameInput) { + private fun processUsernameInput(action: UsernameInput) { _screenState.update { currentValue -> currentValue.copy( username = action.value @@ -53,7 +56,7 @@ class AuthViewModel( } } - private fun processPasswordInput(action: ScreenAction.PasswordInput) { + private fun processPasswordInput(action: PasswordInput) { _screenState.update { currentValue -> currentValue.copy( password = action.value @@ -61,10 +64,10 @@ class AuthViewModel( } } - private fun processPasswordSubmitInput(action: ScreenAction.PasswordSubmitInput) { + private fun processPasswordSubmitInput(action: PasswordSubmitInput) { _screenState.update { currentValue -> currentValue.copy( - passwordSubmit = action.value + passwordSubmit = action.value, ) } } diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthFieldsColumn.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthFieldsColumn.kt index e6aeadb..a3b6695 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthFieldsColumn.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthFieldsColumn.kt @@ -8,14 +8,16 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.stslex.aproselection.core.ui.theme.AppDimens import com.stslex.aproselection.core.ui.theme.AppTheme -import com.stslex.aproselection.feature.auth.ui.AuthPasswordTextField +import com.stslex.aproselection.feature.auth.R import com.stslex.aproselection.feature.auth.ui.AuthUsernameTextField import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenState import com.stslex.aproselection.feature.auth.ui.model.screen.AuthScreenState @@ -29,18 +31,14 @@ fun AuthFieldsColumn( modifier: Modifier = Modifier ) { Column( - modifier = modifier, + modifier = modifier.padding( + horizontal = AppDimens.Padding.big + ), horizontalAlignment = Alignment.CenterHorizontally ) { - AuthUsernameTextField( - inputUsername = state.username, - onTextChange = state::onUsernameChange - ) + AuthUsernameTextField(state.usernameState) Spacer(Modifier.height(AppDimens.Padding.medium)) - AuthPasswordTextField( - inputPassword = state.password, - onTextChange = state::onPasswordChange - ) + AuthPasswordTextField(state.passwordEnterState) AnimatedVisibility( visible = state.isRegisterState ) { @@ -48,10 +46,7 @@ fun AuthFieldsColumn( horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.height(AppDimens.Padding.small)) - AuthPasswordTextField( - inputPassword = state.passwordSubmit, - onTextChange = state::onPasswordSubmitChange - ) + AuthPasswordTextField(state.passwordSubmitState) } } Spacer(Modifier.height(AppDimens.Padding.big)) diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthPasswordTextField.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthPasswordTextField.kt new file mode 100644 index 0000000..d81e398 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthPasswordTextField.kt @@ -0,0 +1,99 @@ +package com.stslex.aproselection.feature.auth.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import com.stslex.aproselection.core.ui.theme.AppTheme +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.PasswordTextFieldState + +@Composable +fun AuthPasswordTextField( + state: PasswordTextFieldState, + modifier: Modifier = Modifier +) { + TextField( + modifier = modifier + .fillMaxWidth(), + value = state.text, + onValueChange = state::onTextChange, + singleLine = true, + supportingText = { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + modifier = Modifier.align(Alignment.CenterStart), + text = stringResource(id = state.hint) + ) + Text( + modifier = Modifier.align(Alignment.CenterEnd), + text = state.supportingEndText + ) + } + }, + visualTransformation = state.visualTransformation.value, + label = { + Text(text = stringResource(state.label)) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Next + ), + trailingIcon = { + IconButton( + onClick = state::onPasswordHideClicked + ) { + Icon( + painter = painterResource(id = state.trailingIconRes.value), + contentDescription = null + ) + } + }, + ) +} + +@Preview( + device = "id:pixel_6", showSystemUi = true, showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL +) +@Composable +fun AuthPasswordTextFieldPreview() { + AppTheme { + var text by remember { + mutableStateOf("text") + } + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { +// AuthPasswordTextField( +// modifier = Modifier.align(Alignment.Center), +// inputPassword = text, +// onTextChange = { value -> +// text = value +// }, +// hint = "enterPassword", +// supportingEndText = "4/10" +// ) + } + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenAction.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenAction.kt index cd09784..c64c1e8 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenAction.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenAction.kt @@ -2,19 +2,24 @@ package com.stslex.aproselection.feature.auth.ui.model.mvi sealed interface ScreenAction { - data class UsernameInput( - val value: String - ) : ScreenAction + data object OnSubmitClicked : ScreenAction - data class PasswordInput( - val value: String - ) : ScreenAction + data object OnAuthFieldChange : ScreenAction - data class PasswordSubmitInput( - val value: String - ) : ScreenAction + sealed class InputAction( + open val value: String + ) : ScreenAction { - data object OnSubmitClicked : ScreenAction + data class UsernameInput( + override val value: String + ) : InputAction(value) - data object OnAuthFieldChange : ScreenAction + data class PasswordInput( + override val value: String + ) : InputAction(value) + + data class PasswordSubmitInput( + override val value: String + ) : InputAction(value) + } } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt index ccacfe2..220299c 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController import com.stslex.aproselection.core.ui.ext.CollectAsEvent @@ -16,6 +17,12 @@ import com.stslex.aproselection.feature.auth.ui.model.ScreenLoadingState import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenEvent import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.PasswordInputTextFieldState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.PasswordSubmitTextFieldState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.UsernameTextFieldState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.rememberPasswordInputTextFieldState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.rememberPasswordSubmitTextFieldState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.rememberUsernameTextFieldState import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -23,39 +30,26 @@ import kotlinx.coroutines.flow.StateFlow @Stable data class AuthScreenState( val screenLoadingState: ScreenLoadingState = ScreenLoadingState.Content, - val username: String = "", - val password: String = "", - val passwordSubmit: String = "", + val usernameState: UsernameTextFieldState, + val passwordEnterState: PasswordInputTextFieldState, + val passwordSubmitState: PasswordSubmitTextFieldState, val authFieldsState: AuthFieldsState = AuthFieldsState.AUTH, val snackbarHostState: SnackbarHostState, private val processAction: (ScreenAction) -> Unit, private val keyboardController: SoftwareKeyboardController? = null ) { + val isFieldsValid: Boolean get() { - val isCorrectLength = username.length >= 4 && password.length >= 4 - val isEqualsPasswords = password == passwordSubmit + val isCorrectLength = usernameState.text.length >= 4 && + passwordEnterState.text.length >= 4 + val isEqualsPasswords = passwordEnterState.text == passwordSubmitState.text val isRegisterPassword = authFieldsState == AuthFieldsState.AUTH || isEqualsPasswords return isCorrectLength && isRegisterPassword } val isRegisterState = authFieldsState == AuthFieldsState.REGISTER - fun onUsernameChange(username: String) { - if (this.username == username) return - processAction(ScreenAction.UsernameInput(username)) - } - - fun onPasswordChange(password: String) { - if (this.password == password) return - processAction(ScreenAction.PasswordInput(password)) - } - - fun onPasswordSubmitChange(passwordSubmit: String) { - if (this.passwordSubmit == passwordSubmit) return - processAction(ScreenAction.PasswordSubmitInput(passwordSubmit)) - } - fun onSubmitClicked() { keyboardController?.hide() processAction(ScreenAction.OnSubmitClicked) @@ -73,6 +67,8 @@ fun rememberAuthScreenState( screenEventFlow: () -> SharedFlow, processAction: (ScreenAction) -> Unit, ): AuthScreenState { + val keyboardController = LocalSoftwareKeyboardController.current + val hapticFeedback = LocalHapticFeedback.current val screenState by remember { screenStateFlow() @@ -94,18 +90,33 @@ fun rememberAuthScreenState( } } - val keyboardController = LocalSoftwareKeyboardController.current + val usernameTextFieldState = rememberUsernameTextFieldState( + text = screenState.username, + processAction = processAction + ) + + val passwordEnterState = rememberPasswordInputTextFieldState( + hapticFeedback = hapticFeedback, + processAction = processAction, + text = screenState.password + ) + + val passwordSubmitState = rememberPasswordSubmitTextFieldState( + hapticFeedback = hapticFeedback, + processAction = processAction, + text = screenState.passwordSubmit + ) return remember(screenState) { AuthScreenState( screenLoadingState = screenState.screenLoadingState, - username = screenState.username, - password = screenState.password, - passwordSubmit = screenState.passwordSubmit, + passwordEnterState = passwordEnterState, + passwordSubmitState = passwordSubmitState, authFieldsState = screenState.authFieldsState, snackbarHostState = snackbarHostState, processAction = processAction, - keyboardController = keyboardController + keyboardController = keyboardController, + usernameState = usernameTextFieldState ) } } diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/AuthTextField.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/AuthTextField.kt new file mode 100644 index 0000000..03c1502 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/AuthTextField.kt @@ -0,0 +1,20 @@ +package com.stslex.aproselection.feature.auth.ui.model.screen.text_field + +abstract class AuthTextField { + + abstract val text: String + abstract val label: Int + abstract val sendAction: (text: String) -> Unit + + val supportingEndText: String + get() = "${text.length}/$MAX_SYMBOL_COUNT" + + fun onTextChange(value: String) { + if (text == value || value.length > MAX_SYMBOL_COUNT) return + sendAction(value) + } + + companion object { + private const val MAX_SYMBOL_COUNT = 10 + } +} diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt new file mode 100644 index 0000000..4e60dca --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt @@ -0,0 +1,34 @@ +package com.stslex.aproselection.feature.auth.ui.model.screen.text_field + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.hapticfeedback.HapticFeedback +import com.stslex.aproselection.feature.auth.R +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.PasswordInput + +data class PasswordInputTextFieldState( + private val hapticFeedback: HapticFeedback, + private val processAction: (PasswordInput) -> Unit, + override val text: String, +) : PasswordTextFieldState(hapticFeedback) { + + override val hint: Int = R.string.auth_password_enter_hint_text + + override val sendAction: (text: String) -> Unit + get() = { value -> + processAction(PasswordInput(value)) + } +} + +@Composable +fun rememberPasswordInputTextFieldState( + hapticFeedback: HapticFeedback, + processAction: (PasswordInput) -> Unit, + text: String, +) = remember(text) { + PasswordInputTextFieldState( + hapticFeedback = hapticFeedback, + processAction = processAction, + text = text + ) +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt new file mode 100644 index 0000000..925afab --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt @@ -0,0 +1,34 @@ +package com.stslex.aproselection.feature.auth.ui.model.screen.text_field + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.hapticfeedback.HapticFeedback +import com.stslex.aproselection.feature.auth.R +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.PasswordSubmitInput + +data class PasswordSubmitTextFieldState( + private val hapticFeedback: HapticFeedback, + private val processAction: (PasswordSubmitInput) -> Unit, + override val text: String, +) : PasswordTextFieldState(hapticFeedback) { + + override val hint: Int = R.string.auth_password_submit_hint_text + + override val sendAction: (text: String) -> Unit + get() = { value -> + processAction(PasswordSubmitInput(value)) + } +} + +@Composable +fun rememberPasswordSubmitTextFieldState( + hapticFeedback: HapticFeedback, + processAction: (PasswordSubmitInput) -> Unit, + text: String, +) = remember(text) { + PasswordSubmitTextFieldState( + hapticFeedback = hapticFeedback, + processAction = processAction, + text = text + ) +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordTextFieldState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordTextFieldState.kt new file mode 100644 index 0000000..71a4519 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordTextFieldState.kt @@ -0,0 +1,41 @@ +package com.stslex.aproselection.feature.auth.ui.model.screen.text_field + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import com.stslex.aproselection.feature.auth.R + +abstract class PasswordTextFieldState( + private val hapticFeedback: HapticFeedback +) : AuthTextField() { + + private val isPasswordHidden = mutableStateOf(true) + + override val label: Int = R.string.auth_password_text + abstract val hint: Int + + val visualTransformation = derivedStateOf { + if (isPasswordHidden.value) { + PasswordVisualTransformation() + } else { + VisualTransformation.None + } + } + + val trailingIconRes = derivedStateOf { + if (isPasswordHidden.value) { + R.drawable.baseline_visibility_off_24 + } else { + R.drawable.baseline_visibility_24 + } + } + + fun onPasswordHideClicked() { + hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove) + isPasswordHidden.value = isPasswordHidden.value.not() + } +} + diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt new file mode 100644 index 0000000..752a0fc --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt @@ -0,0 +1,30 @@ +package com.stslex.aproselection.feature.auth.ui.model.screen.text_field + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.stslex.aproselection.feature.auth.R +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.UsernameInput + +data class UsernameTextFieldState( + private val processAction: (UsernameInput) -> Unit, + override val text: String, +) : AuthTextField() { + + override val sendAction: (text: String) -> Unit + get() = { value -> + processAction(UsernameInput(value)) + } + + override val label: Int = R.string.auth_username_text +} + +@Composable +fun rememberUsernameTextFieldState( + text: String, + processAction: (UsernameInput) -> Unit +): UsernameTextFieldState = remember(text) { + UsernameTextFieldState( + text = text, + processAction = processAction + ) +} diff --git a/feature/auth/src/main/res/drawable/baseline_visibility_24.xml b/feature/auth/src/main/res/drawable/baseline_visibility_24.xml new file mode 100644 index 0000000..b923c39 --- /dev/null +++ b/feature/auth/src/main/res/drawable/baseline_visibility_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/feature/auth/src/main/res/drawable/baseline_visibility_off_24.xml b/feature/auth/src/main/res/drawable/baseline_visibility_off_24.xml new file mode 100644 index 0000000..00c8a20 --- /dev/null +++ b/feature/auth/src/main/res/drawable/baseline_visibility_off_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/feature/auth/src/main/res/values/strings.xml b/feature/auth/src/main/res/values/strings.xml index c73d54a..ddc7bec 100644 --- a/feature/auth/src/main/res/values/strings.xml +++ b/feature/auth/src/main/res/values/strings.xml @@ -4,4 +4,8 @@ register (if not have account) Authentication Registration + username + password + enter password + repeat password \ No newline at end of file From 4ca24dd1d886fdd9e3fd8ccea25909d297ced7c9 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 2 Aug 2023 21:29:28 +0300 Subject: [PATCH 3/3] fix di and auth password fields --- .../ui/components/AuthPasswordTextField.kt | 2 +- .../text_field/PasswordInputTextFieldState.kt | 26 ++++++++++++----- .../PasswordSubmitTextFieldState.kt | 28 ++++++++++++++----- .../text_field/UsernameTextFieldState.kt | 3 ++ .../text_field/{ => base}/AuthTextField.kt | 10 +++++-- .../{ => base}/PasswordTextFieldState.kt | 17 +++++------ .../screen/text_field/hidden/HiddenState.kt | 15 ++++++++++ 7 files changed, 75 insertions(+), 26 deletions(-) rename feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/{ => base}/AuthTextField.kt (65%) rename feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/{ => base}/PasswordTextFieldState.kt (73%) create mode 100644 feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/hidden/HiddenState.kt diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthPasswordTextField.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthPasswordTextField.kt index d81e398..69069c0 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthPasswordTextField.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/components/AuthPasswordTextField.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import com.stslex.aproselection.core.ui.theme.AppTheme -import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.PasswordTextFieldState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.base.PasswordTextFieldState @Composable fun AuthPasswordTextField( diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt index 4e60dca..9688807 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordInputTextFieldState.kt @@ -1,16 +1,24 @@ package com.stslex.aproselection.feature.auth.ui.model.screen.text_field import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.hapticfeedback.HapticFeedback import com.stslex.aproselection.feature.auth.R import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.PasswordInput +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.base.PasswordTextFieldState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.hidden.HiddenState +@Stable data class PasswordInputTextFieldState( private val hapticFeedback: HapticFeedback, private val processAction: (PasswordInput) -> Unit, + private val hiddenState: HiddenState, override val text: String, -) : PasswordTextFieldState(hapticFeedback) { +) : PasswordTextFieldState( + hapticFeedback = hapticFeedback, + hiddenState = hiddenState +) { override val hint: Int = R.string.auth_password_enter_hint_text @@ -25,10 +33,14 @@ fun rememberPasswordInputTextFieldState( hapticFeedback: HapticFeedback, processAction: (PasswordInput) -> Unit, text: String, -) = remember(text) { - PasswordInputTextFieldState( - hapticFeedback = hapticFeedback, - processAction = processAction, - text = text - ) +): PasswordInputTextFieldState { + val hiddenState = remember { HiddenState() } + return remember(text) { + PasswordInputTextFieldState( + hapticFeedback = hapticFeedback, + processAction = processAction, + hiddenState = hiddenState, + text = text + ) + } } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt index 925afab..8aaea8b 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordSubmitTextFieldState.kt @@ -1,16 +1,24 @@ package com.stslex.aproselection.feature.auth.ui.model.screen.text_field import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.hapticfeedback.HapticFeedback import com.stslex.aproselection.feature.auth.R import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.PasswordSubmitInput +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.base.PasswordTextFieldState +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.hidden.HiddenState +@Stable data class PasswordSubmitTextFieldState( private val hapticFeedback: HapticFeedback, private val processAction: (PasswordSubmitInput) -> Unit, + private val hiddenState: HiddenState, override val text: String, -) : PasswordTextFieldState(hapticFeedback) { +) : PasswordTextFieldState( + hapticFeedback = hapticFeedback, + hiddenState = hiddenState +) { override val hint: Int = R.string.auth_password_submit_hint_text @@ -25,10 +33,16 @@ fun rememberPasswordSubmitTextFieldState( hapticFeedback: HapticFeedback, processAction: (PasswordSubmitInput) -> Unit, text: String, -) = remember(text) { - PasswordSubmitTextFieldState( - hapticFeedback = hapticFeedback, - processAction = processAction, - text = text - ) +): PasswordSubmitTextFieldState { + val hiddenState = remember { + HiddenState() + } + return remember(text) { + PasswordSubmitTextFieldState( + hapticFeedback = hapticFeedback, + processAction = processAction, + hiddenState = hiddenState, + text = text + ) + } } \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt index 752a0fc..b5dcc67 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/UsernameTextFieldState.kt @@ -1,10 +1,13 @@ package com.stslex.aproselection.feature.auth.ui.model.screen.text_field import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import com.stslex.aproselection.feature.auth.R import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenAction.InputAction.UsernameInput +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.base.AuthTextField +@Stable data class UsernameTextFieldState( private val processAction: (UsernameInput) -> Unit, override val text: String, diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/AuthTextField.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/base/AuthTextField.kt similarity index 65% rename from feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/AuthTextField.kt rename to feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/base/AuthTextField.kt index 03c1502..d45c967 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/AuthTextField.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/base/AuthTextField.kt @@ -1,5 +1,8 @@ -package com.stslex.aproselection.feature.auth.ui.model.screen.text_field +package com.stslex.aproselection.feature.auth.ui.model.screen.text_field.base +import androidx.compose.runtime.Stable + +@Stable abstract class AuthTextField { abstract val text: String @@ -10,8 +13,9 @@ abstract class AuthTextField { get() = "${text.length}/$MAX_SYMBOL_COUNT" fun onTextChange(value: String) { - if (text == value || value.length > MAX_SYMBOL_COUNT) return - sendAction(value) + val currentValue = value.trim() + if (text == currentValue || currentValue.length > MAX_SYMBOL_COUNT) return + sendAction(currentValue) } companion object { diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordTextFieldState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/base/PasswordTextFieldState.kt similarity index 73% rename from feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordTextFieldState.kt rename to feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/base/PasswordTextFieldState.kt index 71a4519..fd4add6 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/PasswordTextFieldState.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/base/PasswordTextFieldState.kt @@ -1,24 +1,25 @@ -package com.stslex.aproselection.feature.auth.ui.model.screen.text_field +package com.stslex.aproselection.feature.auth.ui.model.screen.text_field.base +import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import com.stslex.aproselection.feature.auth.R +import com.stslex.aproselection.feature.auth.ui.model.screen.text_field.hidden.HiddenState +@Stable abstract class PasswordTextFieldState( - private val hapticFeedback: HapticFeedback + private val hapticFeedback: HapticFeedback, + private val hiddenState: HiddenState ) : AuthTextField() { - private val isPasswordHidden = mutableStateOf(true) - override val label: Int = R.string.auth_password_text abstract val hint: Int val visualTransformation = derivedStateOf { - if (isPasswordHidden.value) { + if (hiddenState.isHidden) { PasswordVisualTransformation() } else { VisualTransformation.None @@ -26,7 +27,7 @@ abstract class PasswordTextFieldState( } val trailingIconRes = derivedStateOf { - if (isPasswordHidden.value) { + if (hiddenState.isHidden) { R.drawable.baseline_visibility_off_24 } else { R.drawable.baseline_visibility_24 @@ -35,7 +36,7 @@ abstract class PasswordTextFieldState( fun onPasswordHideClicked() { hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove) - isPasswordHidden.value = isPasswordHidden.value.not() + hiddenState.isHidden = hiddenState.isHidden.not() } } diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/hidden/HiddenState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/hidden/HiddenState.kt new file mode 100644 index 0000000..d34abb5 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/text_field/hidden/HiddenState.kt @@ -0,0 +1,15 @@ +package com.stslex.aproselection.feature.auth.ui.model.screen.text_field.hidden + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf + +@Stable +class HiddenState { + + private val _isHidden = mutableStateOf(true) + var isHidden: Boolean + get() = _isHidden.value + set(value) { + _isHidden.value = value + } +} \ No newline at end of file