diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 489e6a0..65afc47 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,3 @@ -@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed plugins { id("aproselection.android.application") id("aproselection.android.application.compose") @@ -9,4 +8,5 @@ dependencies { implementation(project(":core:datastore")) implementation(project(":core:network")) implementation(project(":feature:auth")) + implementation(project(":feature:home")) } \ No newline at end of file diff --git a/app/src/main/java/com/stslex/aproselection/SelectApplication.kt b/app/src/main/java/com/stslex/aproselection/SelectApplication.kt index c5f0446..4422154 100644 --- a/app/src/main/java/com/stslex/aproselection/SelectApplication.kt +++ b/app/src/main/java/com/stslex/aproselection/SelectApplication.kt @@ -3,7 +3,9 @@ package com.stslex.aproselection import android.app.Application 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.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin @@ -16,9 +18,11 @@ class SelectApplication : Application() { androidLogger() androidContext(applicationContext) modules( - moduleFeatureAuth, + appModule, moduleCoreNetwork, - coreDataStoreModule + coreDataStoreModule, + moduleFeatureAuth, + moduleFeatureHome, ) } } diff --git a/app/src/main/java/com/stslex/aproselection/di/AppModule.kt b/app/src/main/java/com/stslex/aproselection/di/AppModule.kt new file mode 100644 index 0000000..c6e6376 --- /dev/null +++ b/app/src/main/java/com/stslex/aproselection/di/AppModule.kt @@ -0,0 +1,9 @@ +package com.stslex.aproselection.di + +import com.stslex.aproselection.ui.InitialAppViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val appModule = module { + 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 6b41c81..c41fecb 100644 --- a/app/src/main/java/com/stslex/aproselection/navigation/NavigationHost.kt +++ b/app/src/main/java/com/stslex/aproselection/navigation/NavigationHost.kt @@ -1,18 +1,29 @@ 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.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, modifier: Modifier = Modifier, - startDestination: AppDestination = AppDestination.AUTH + startDestination: AppDestination = AppDestination.SPLASH ) { val navigator: (NavigationScreen) -> Unit = { screen -> when (screen) { @@ -20,24 +31,48 @@ fun NavigationHost( 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) } } fun NavHostController.navigateScreen(screen: NavigationScreen) { + val currentRoute = this.currentDestination?.route + if (currentRoute == screen.screenRoute) return navigate(screen.screenRoute) { if (screen.isSingleTop.not()) return@navigate - graph.startDestinationRoute?.let { route -> + currentRoute?.let { route -> popUpTo(route) { inclusive = true saveState = true } } + launchSingleTop = true - restoreState = false } } \ 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 bbb3867..204e964 100644 --- a/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt +++ b/app/src/main/java/com/stslex/aproselection/ui/InitialApp.kt @@ -1,5 +1,6 @@ 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 @@ -10,7 +11,11 @@ import com.stslex.aproselection.navigation.NavigationHost fun InitialApp() { val navController = rememberNavController() - NavigationHost(navController = navController) + Box { + NavigationHost( + navController = navController, + ) + } } @Preview(showBackground = true) diff --git a/app/src/main/java/com/stslex/aproselection/ui/InitialAppViewModel.kt b/app/src/main/java/com/stslex/aproselection/ui/InitialAppViewModel.kt new file mode 100644 index 0000000..1a784c2 --- /dev/null +++ b/app/src/main/java/com/stslex/aproselection/ui/InitialAppViewModel.kt @@ -0,0 +1,50 @@ +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.core.ui.base.BaseViewModel +import com.stslex.aproselection.core.ui.navigation.NavigationScreen +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, +) : BaseViewModel() { + + private val _isAuth = MutableStateFlow(null) + val isAuth: StateFlow + get() = _isAuth.asStateFlow() + + // TODO (вынести из init блока) + init { + dataStore.token + .catch { error -> + handleError(error) + } + .onEach { token -> + if (token.isBlank()) { + networkClient.regenerateToken() + } + } + .launchIn(viewModelScope) + + dataStore.uuid + .catch { error -> + handleError(error) + } + .onEach { uuid -> + _isAuth.emit(uuid.isNotBlank()) + if (uuid.isBlank()) { + navigate(NavigationScreen.Auth) + } + } + .launchIn(viewModelScope) + } +} \ No newline at end of file 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 9a3d5cb..7f226d1 100644 --- a/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt +++ b/app/src/main/java/com/stslex/aproselection/ui/MainActivity.kt @@ -15,21 +15,12 @@ import org.koin.android.ext.android.inject class MainActivity : ComponentActivity() { - val dataStore by inject() - val networkClient by inject() + private val dataStore by inject() + private val networkClient by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - dataStore.token - .catch { - Log.e(it.message, javaClass.simpleName, it) - } - .onEach { - if (it.isBlank()) { - networkClient.regenerateToken() - } - } - .launchIn(lifecycleScope) + setContent { AppTheme { InitialApp() diff --git a/app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt b/app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt index 2194669..df90d9c 100644 --- a/app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt +++ b/app/src/test/java/com/stslex/aproselection/DiKoinModuleTest.kt @@ -2,12 +2,15 @@ package com.stslex.aproselection import android.content.Context import com.stslex.aproselection.core.datastore.coreDataStoreModule -import com.stslex.aproselection.core.network.di.ModuleCoreNetwork import com.stslex.aproselection.core.network.di.ModuleCoreNetwork.moduleCoreNetwork +import com.stslex.aproselection.core.ui.navigation.NavigationScreen +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.junit.Test import org.koin.android.ext.koin.androidContext import org.koin.dsl.koinApplication +import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.check.checkModules import org.mockito.Mockito @@ -16,12 +19,22 @@ class DiKoinModuleTest : KoinTest { @Test fun checkKoinModules() { + val navigator: (screen: NavigationScreen) -> Unit = {} + koinApplication { + androidContext(Mockito.mock(Context::class.java)) modules( - moduleFeatureAuth, + module { + single { + navigator + } + }, + appModule, moduleCoreNetwork, - coreDataStoreModule + coreDataStoreModule, + moduleFeatureAuth, + moduleFeatureHome, ) checkModules() } diff --git a/build-logic/dependencies/src/main/kotlin/com.stslex.aproselection/KotlinAndroid.kt b/build-logic/dependencies/src/main/kotlin/com.stslex.aproselection/KotlinAndroid.kt index d7d1da6..e268996 100644 --- a/build-logic/dependencies/src/main/kotlin/com.stslex.aproselection/KotlinAndroid.kt +++ b/build-logic/dependencies/src/main/kotlin/com.stslex.aproselection/KotlinAndroid.kt @@ -5,8 +5,11 @@ import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.getValue import org.gradle.kotlin.dsl.provideDelegate import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions @@ -34,9 +37,9 @@ internal fun Project.configureKotlinAndroid( targetCompatibility = JavaVersion.VERSION_11 isCoreLibraryDesugaringEnabled = true } - } - configureKotlin() + configureKotlinJvm() + } val libs = extensions.getByType().named("libs") @@ -65,20 +68,20 @@ private fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() (this as ExtensionAware).extensions.configure("kotlinOptions", block) } -// TODO check -///** -// * Configure base Kotlin options for JVM (non-Android) -// */ -//internal fun Project.configureKotlinJvm() { -// extensions.configure { -// // Up to Java 11 APIs are available through desugaring -// // https://developer.android.com/studio/write/java11-minimal-support-table -// sourceCompatibility = JavaVersion.VERSION_11 -// targetCompatibility = JavaVersion.VERSION_11 -// } -// -// configureKotlin() -//} +/** + * Configure base Kotlin options for JVM (non-Android) + */ +internal fun Project.configureKotlinJvm() { + extensions.configure { + // Up to Java 11 APIs are available through desugaring + // https://developer.android.com/studio/write/java11-minimal-support-table + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + configureKotlin() +} + /** * Configure base Kotlin options @@ -89,6 +92,7 @@ private fun Project.configureKotlin() { kotlinOptions { // Set JVM target to 11 jvmTarget = JavaVersion.VERSION_11.toString() + languageVersion = "1.9" // Treat all Kotlin warnings as errors (disabled by default) // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties val warningsAsErrors: String? by project diff --git a/build.gradle.kts b/build.gradle.kts index 59845ad..fa3ea69 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +@file:Suppress("UNUSED_EXPRESSION") + // Top-level build file where you can add configuration options common to all sub-projects/modules. @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed plugins { diff --git a/core/datastore/src/main/java/com/stslex/aproselection/core/datastore/AppDataStoreImpl.kt b/core/datastore/src/main/java/com/stslex/aproselection/core/datastore/AppDataStoreImpl.kt index 9bb0662..87c2d8a 100644 --- a/core/datastore/src/main/java/com/stslex/aproselection/core/datastore/AppDataStoreImpl.kt +++ b/core/datastore/src/main/java/com/stslex/aproselection/core/datastore/AppDataStoreImpl.kt @@ -5,7 +5,9 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map class AppDataStoreImpl( @@ -20,14 +22,18 @@ class AppDataStoreImpl( } override val uuid: Flow - get() = context.dataStore.data.map { prefs -> - prefs[UUID_KEY].orEmpty() - } + get() = context.dataStore.data + .map { prefs -> + prefs[UUID_KEY].orEmpty() + } + .flowOn(Dispatchers.IO) override val token: Flow - get() = context.dataStore.data.map { prefs -> - prefs[TOKEN_KEY].orEmpty() - } + get() = context.dataStore.data + .map { prefs -> + prefs[TOKEN_KEY].orEmpty() + } + .flowOn(Dispatchers.IO) override suspend fun setUuid(uuid: String) { context.dataStore.updateData { prefs -> @@ -37,7 +43,6 @@ class AppDataStoreImpl( } } - override suspend fun setToken(token: String) { context.dataStore.updateData { prefs -> prefs.toMutablePreferences().apply { diff --git a/core/network/src/main/java/com/stslex/aproselection/core/network/client/NetworkClientImpl.kt b/core/network/src/main/java/com/stslex/aproselection/core/network/client/NetworkClientImpl.kt index c10685f..77935d8 100644 --- a/core/network/src/main/java/com/stslex/aproselection/core/network/client/NetworkClientImpl.kt +++ b/core/network/src/main/java/com/stslex/aproselection/core/network/client/NetworkClientImpl.kt @@ -42,6 +42,7 @@ class NetworkClientImpl( private val _uuid = MutableStateFlow("") private val uuid = _uuid.asStateFlow() + // TODO (вынести из init блока) init { dataStore.token .onEach { diff --git a/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClientImpl.kt b/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClientImpl.kt index 2ee7d23..748dd9d 100644 --- a/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClientImpl.kt +++ b/core/network/src/main/java/com/stslex/aproselection/core/network/clients/auth/AuthNetworkClientImpl.kt @@ -30,7 +30,6 @@ class AuthNetworkClientImpl( .body() } - override fun auth( user: UserAuthSendModel ): Flow = flow { diff --git a/core/ui/src/main/java/com/stslex/aproselection/core/ui/ext/CoroutineExt.kt b/core/ui/src/main/java/com/stslex/aproselection/core/ui/ext/CoroutineExt.kt new file mode 100644 index 0000000..8f1451a --- /dev/null +++ b/core/ui/src/main/java/com/stslex/aproselection/core/ui/ext/CoroutineExt.kt @@ -0,0 +1,25 @@ +package com.stslex.aproselection.core.ui.ext + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun SharedFlow.CollectAsEvent( + minActionState: Lifecycle.State = Lifecycle.State.STARTED, + action: suspend (value: T) -> Unit +) { + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(Unit) { + this@CollectAsEvent + .flowWithLifecycle( + lifecycle = lifecycleOwner.lifecycle, + minActiveState = minActionState + ) + .collectLatest(action) + } +} \ No newline at end of file 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/AppDestination.kt index bd9ad1f..d9d8d3b 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/AppDestination.kt @@ -3,7 +3,9 @@ package com.stslex.aproselection.core.ui.navigation enum class AppDestination( vararg val argsNames: String ) { - AUTH; + AUTH, + HOME, + SPLASH; val route: String get() = StringBuilder() 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/NavigationScreen.kt index 3826ca5..2520e83 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/NavigationScreen.kt @@ -1,25 +1,34 @@ package com.stslex.aproselection.core.ui.navigation -sealed class NavigationScreen { +sealed interface NavigationScreen { - abstract val screen: AppDestination + val screen: AppDestination val screenRoute: String get() = "${screen.route}${appArgs.argumentsForRoute}" - open val isSingleTop: Boolean + val isSingleTop: Boolean get() = false - open val appArgs: AppArguments + val appArgs: AppArguments get() = AppArguments.Empty - object Auth : NavigationScreen() { - + data object Auth : NavigationScreen { override val screen: AppDestination = AppDestination.AUTH override val isSingleTop: Boolean = true } - object PopBackStack : NavigationScreen() { + data object Home : NavigationScreen { + override val screen: AppDestination = AppDestination.HOME + 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/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 99a9ff4..68a9b2b 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { id("aproselection.android.library") id("aproselection.android.library.compose") diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepositoryImpl.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepositoryImpl.kt index a46d124..abab613 100644 --- a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepositoryImpl.kt +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/data/repository/AuthRepositoryImpl.kt @@ -14,7 +14,6 @@ class AuthRepositoryImpl( private val dataSource: AppDataStore ) : AuthRepository { - override fun auth( username: String, password: String 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 5da2af8..85689cd 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,98 +1,186 @@ package com.stslex.aproselection.feature.auth.ui +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.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.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.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.navigation.NavigationScreen +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 kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow @Composable fun AuthScreen( - text: String, - navigate: (NavigationScreen) -> Unit, - auth: (String, String) -> Unit, + state: AuthScreenState, modifier: Modifier = Modifier, ) { - var inputUsername by remember { - mutableStateOf("") - } - - var inputPassword by remember { - mutableStateOf("") - } - Box( modifier = modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center, ) { + AuthScreenContent(state) + SnackbarHost( + modifier = Modifier + .padding(16.dp) + .align(Alignment.BottomCenter), + hostState = state.snackbarHostState + ) { snackbarData -> + Snackbar { + Text(text = snackbarData.visuals.message) + } + } + } + if (state.screenLoadingState == ScreenLoadingState.Loading) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f)), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } +} - Column( - horizontalAlignment = Alignment.CenterHorizontally +@Composable +fun AuthScreenContent( + state: AuthScreenState, + modifier: Modifier = Modifier +) { + + val buttonRes by remember { + derivedStateOf { state.authFieldsState.inverse.buttonResId } + } + + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + AuthUsernameTextField( + inputUsername = state.username, + onTextChange = state::onUsernameChange + ) + Divider(Modifier.padding(16.dp)) + AuthPasswordTextField( + inputPassword = state.password, + onTextChange = state::onPasswordChange + ) + AnimatedVisibility( + visible = state.isRegisterState ) { - TextField( - value = inputUsername, - onValueChange = { value -> - if (inputUsername != value) { - inputUsername = value - } - }, - maxLines = 1, - ) - Divider(Modifier.padding(16.dp)) - TextField( - value = inputPassword, - onValueChange = { value -> - if (inputPassword != value) { - inputPassword = value - } - }, - maxLines = 1, - ) - Divider(Modifier.padding(16.dp)) - ElevatedButton( - onClick = { - auth(inputUsername, inputPassword) - inputUsername = "" - } + Column( + horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = "submit", - style = MaterialTheme.typography.headlineMedium + Spacer(modifier = Modifier.height(8.dp)) + AuthPasswordTextField( + inputPassword = state.passwordSubmit, + onTextChange = state::onPasswordSubmitChange ) } - Divider(Modifier.padding(16.dp)) + } + 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( + onClick = state::onAuthFieldChange + ) { Text( - text = text, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onBackground + text = stringResource(id = buttonRes), + style = MaterialTheme.typography.titleMedium ) } } } -//@Preview(device = "id:pixel_6", showSystemUi = true, showBackground = true) -//@Composable -//fun AuthScreenPreview() { -// AuthScreen( -// text = "text", -// navigate = {}, -// setUsername = {} -// ) -//} \ No newline at end of file +@Composable +fun AuthUsernameTextField( + inputUsername: String, + onTextChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + TextField( + modifier = modifier, + value = inputUsername, + onValueChange = { value -> + if (inputUsername != value) { + onTextChange(value) + } + }, + 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) + } + }, + singleLine = true, + supportingText = { + Text(text = "enter password") + }, + visualTransformation = PasswordVisualTransformation(), + label = { + Text(text = "password") + } + ) +} + +@Preview(device = "id:pixel_6", showSystemUi = true, showBackground = true) +@Composable +fun AuthScreenPreview() { + 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 ecb9ab3..11a54d5 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,38 +2,134 @@ 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.feature.auth.domain.interactor.AuthInteractor +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.ScreenEvent +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenState +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update class AuthViewModel( - private val interactor: AuthInteractor + private val interactor: AuthInteractor, + private val navigate: (NavigationScreen) -> Unit ) : BaseViewModel() { - private val _text: MutableStateFlow = MutableStateFlow("...") + private val _screenState = MutableStateFlow(ScreenState()) + val screenState: StateFlow + get() = _screenState.asStateFlow() - val text: StateFlow - get() = _text.asStateFlow() + private val _screenEvents: MutableSharedFlow = MutableSharedFlow() + val screenEvent: SharedFlow + get() = _screenEvents.asSharedFlow() - fun auth(username: String, password: String) { - _text.value = "..." + fun process(action: ScreenAction) { + when (action) { + is ScreenAction.UsernameInput -> processUsernameInput(action) + is ScreenAction.OnSubmitClicked -> processSubmitClicked() + is ScreenAction.PasswordInput -> processPasswordInput(action) + is ScreenAction.PasswordSubmitInput -> processPasswordSubmitInput(action) + is ScreenAction.OnAuthFieldChange -> processAuthFieldChange() + } + } + + private fun processUsernameInput(action: ScreenAction.UsernameInput) { + _screenState.update { currentValue -> + currentValue.copy( + username = action.value + ) + } + } + + private fun processPasswordInput(action: ScreenAction.PasswordInput) { + _screenState.update { currentValue -> + currentValue.copy( + password = action.value + ) + } + } + + private fun processPasswordSubmitInput(action: ScreenAction.PasswordSubmitInput) { + _screenState.update { currentValue -> + currentValue.copy( + passwordSubmit = action.value + ) + } + } + + private fun processAuthFieldChange() { + _screenState.update { currentValue -> + currentValue.copy( + authFieldsState = currentValue.authFieldsState.inverse + ) + } + } + + private fun processSubmitClicked() { + when (screenState.value.authFieldsState) { + AuthFieldsState.AUTH -> auth() + AuthFieldsState.REGISTER -> register() + } + } + + private fun register() { + val state = screenState.value + setLoadingState(ScreenLoadingState.Loading) interactor .register( - username = username, - password = password + username = state.username, + password = state.password ) .catch { throwable -> - _text.emit("Error: ${throwable.localizedMessage}") + _screenEvents.emit(ScreenEvent.Error(ErrorType.UnResolve(throwable))) + setLoadingState(ScreenLoadingState.Content) handleError(throwable) } - .onEach { userModel -> - _text.emit("Success: uuid--${userModel.uuid}") + .onEach { + setLoadingState(ScreenLoadingState.Content) } .launchIn(viewModelScope) } + + private fun auth() { + val state = screenState.value + setLoadingState(ScreenLoadingState.Loading) + + interactor + .auth( + username = state.username, + password = state.password + ) + .catch { throwable -> + _screenEvents.emit(ScreenEvent.Error(ErrorType.UnResolve(throwable))) + setLoadingState(ScreenLoadingState.Content) + handleError(throwable) + } + .onEach { + setLoadingState(ScreenLoadingState.Content) + navigate(NavigationScreen.Home) + } + .launchIn(viewModelScope) + } + + + private fun setLoadingState(screenLoadingState: ScreenLoadingState) { + _screenState.update { state -> + state.copy( + screenLoadingState = screenLoadingState + ) + } + } } 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 new file mode 100644 index 0000000..934331a --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/AuthFieldsState.kt @@ -0,0 +1,17 @@ +package com.stslex.aproselection.feature.auth.ui.model + +import androidx.annotation.StringRes +import com.stslex.aproselection.feature.auth.R + +enum class AuthFieldsState( + @StringRes val buttonResId: Int +) { + AUTH(R.string.auth), + REGISTER(R.string.register); + + val inverse: AuthFieldsState + get() = when (this) { + AUTH -> REGISTER + REGISTER -> AUTH + } +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/ScreenLoadingState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/ScreenLoadingState.kt new file mode 100644 index 0000000..908bcd7 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/ScreenLoadingState.kt @@ -0,0 +1,21 @@ +package com.stslex.aproselection.feature.auth.ui.model + +sealed interface ScreenLoadingState { + + data object Loading : ScreenLoadingState + + data object Content : ScreenLoadingState + + data class Error( + val error: ErrorType + ) : ScreenLoadingState +} + +sealed interface ErrorType { + + sealed interface Api : ErrorType + + data class UnResolve( + val throwable: Throwable + ) : ErrorType +} \ 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 new file mode 100644 index 0000000..cd09784 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenAction.kt @@ -0,0 +1,20 @@ +package com.stslex.aproselection.feature.auth.ui.model.mvi + +sealed interface ScreenAction { + + data class UsernameInput( + val value: String + ) : ScreenAction + + data class PasswordInput( + val value: String + ) : ScreenAction + + data class PasswordSubmitInput( + val value: String + ) : ScreenAction + + data object OnSubmitClicked : ScreenAction + + data object OnAuthFieldChange : ScreenAction +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenEvent.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenEvent.kt new file mode 100644 index 0000000..81739ed --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenEvent.kt @@ -0,0 +1,10 @@ +package com.stslex.aproselection.feature.auth.ui.model.mvi + +import com.stslex.aproselection.feature.auth.ui.model.ErrorType + +interface ScreenEvent { + + data class Error( + val errorType: ErrorType + ) : ScreenEvent +} \ No newline at end of file diff --git a/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenState.kt b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenState.kt new file mode 100644 index 0000000..7d9d8e3 --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/mvi/ScreenState.kt @@ -0,0 +1,14 @@ +package com.stslex.aproselection.feature.auth.ui.model.mvi + +import androidx.compose.runtime.Stable +import com.stslex.aproselection.feature.auth.ui.model.AuthFieldsState +import com.stslex.aproselection.feature.auth.ui.model.ScreenLoadingState + +@Stable +data class ScreenState( + val screenLoadingState: ScreenLoadingState = ScreenLoadingState.Content, + val username: String = "", + val password: String = "", + val passwordSubmit: String = "", + val authFieldsState: AuthFieldsState = AuthFieldsState.AUTH +) 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 new file mode 100644 index 0000000..7c63abe --- /dev/null +++ b/feature/auth/src/main/java/com/stslex/aproselection/feature/auth/ui/model/screen/AuthScreenState.kt @@ -0,0 +1,104 @@ +package com.stslex.aproselection.feature.auth.ui.model.screen + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +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.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import com.stslex.aproselection.core.ui.ext.CollectAsEvent +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.ScreenEvent +import com.stslex.aproselection.feature.auth.ui.model.mvi.ScreenState +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +@OptIn(ExperimentalComposeUiApi::class) +@Stable +data class AuthScreenState( + val screenLoadingState: ScreenLoadingState = ScreenLoadingState.Content, + val username: String = "", + val password: String = "", + val passwordSubmit: String = "", + val authFieldsState: AuthFieldsState = AuthFieldsState.AUTH, + val snackbarHostState: SnackbarHostState, + private val processAction: (ScreenAction) -> Unit, + private val keyboardController: SoftwareKeyboardController? = null +) { + val isFieldsValid = username.length >= 4 && password.length >= 4 + 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) + } + + fun onAuthFieldChange() { + processAction(ScreenAction.OnAuthFieldChange) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun rememberAuthScreenState( + screenStateFlow: () -> StateFlow, + screenEventFlow: () -> SharedFlow, + processAction: (ScreenAction) -> Unit, +): AuthScreenState { + + val screenState by remember { + screenStateFlow() + }.collectAsState() + + val snackbarHostState = remember { + SnackbarHostState() + } + + screenEventFlow().CollectAsEvent { event -> + when (event) { + is ScreenEvent.Error -> when (val error = event.errorType) { + is ErrorType.UnResolve -> { + snackbarHostState.showSnackbar(error.throwable.message.orEmpty()) + } + + is ErrorType.Api -> Unit // TODO add api parse error logic + } + } + } + + val keyboardController = LocalSoftwareKeyboardController.current + + return remember(screenState) { + AuthScreenState( + screenLoadingState = screenState.screenLoadingState, + username = screenState.username, + password = screenState.password, + passwordSubmit = screenState.passwordSubmit, + authFieldsState = screenState.authFieldsState, + snackbarHostState = snackbarHostState, + processAction = processAction, + keyboardController = keyboardController + ) + } +} 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 3273528..ab7605c 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 @@ -1,8 +1,5 @@ package com.stslex.aproselection.feature.auth.ui.navigation -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable @@ -10,7 +7,9 @@ import com.stslex.aproselection.core.ui.navigation.AppDestination import com.stslex.aproselection.core.ui.navigation.NavigationScreen 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, @@ -19,16 +18,18 @@ fun NavGraphBuilder.authRouter( composable( route = AppDestination.AUTH.navigationRoute ) { - val viewModel: AuthViewModel = koinViewModel() + val viewModel: AuthViewModel = koinViewModel( + parameters = { parametersOf(navigate) } + ) - val text by remember { - viewModel.text - }.collectAsState() + val authScreenState = rememberAuthScreenState( + screenStateFlow = viewModel::screenState, + screenEventFlow = viewModel::screenEvent, + processAction = viewModel::process + ) AuthScreen( - text = text, - navigate = navigate, - auth = viewModel::auth, + state = authScreenState, modifier = modifier ) } diff --git a/feature/auth/src/main/res/values/strings.xml b/feature/auth/src/main/res/values/strings.xml new file mode 100644 index 0000000..2a54413 --- /dev/null +++ b/feature/auth/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + log in + register (if not have account) + \ No newline at end of file diff --git a/feature/home/.gitignore b/feature/home/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/feature/home/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts new file mode 100644 index 0000000..2a95b30 --- /dev/null +++ b/feature/home/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("aproselection.android.library") + id("aproselection.android.library.compose") +} + +dependencies { + implementation(project(":core:ui")) + implementation(project(":core:datastore")) + implementation(project(":core:network")) +} + +android.namespace = "com.stslex.aproselection.feature.home" diff --git a/feature/home/consumer-rules.pro b/feature/home/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/feature/home/proguard-rules.pro b/feature/home/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/feature/home/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/home/src/androidTest/java/com/stslex/aproselection/feature/home/ExampleInstrumentedTest.kt b/feature/home/src/androidTest/java/com/stslex/aproselection/feature/home/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..ac10328 --- /dev/null +++ b/feature/home/src/androidTest/java/com/stslex/aproselection/feature/home/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.stslex.aproselection.feature.home + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.stslex.aproselection.feature.home.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/home/src/main/AndroidManifest.xml b/feature/home/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/feature/home/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/home/src/main/java/com/stslex/aproselection/feature/home/di/ModuleFeatureHome.kt b/feature/home/src/main/java/com/stslex/aproselection/feature/home/di/ModuleFeatureHome.kt new file mode 100644 index 0000000..8a944de --- /dev/null +++ b/feature/home/src/main/java/com/stslex/aproselection/feature/home/di/ModuleFeatureHome.kt @@ -0,0 +1,9 @@ +package com.stslex.aproselection.feature.home.di + +import com.stslex.aproselection.feature.home.ui.HomeViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +val moduleFeatureHome = module { + viewModelOf(::HomeViewModel) +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/HomeScreen.kt b/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/HomeScreen.kt new file mode 100644 index 0000000..f2d1bbb --- /dev/null +++ b/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/HomeScreen.kt @@ -0,0 +1,45 @@ +package com.stslex.aproselection.feature.home.ui + +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.ElevatedButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun HomeScreen( + logOut: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Success", + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Spacer(modifier = Modifier.height(32.dp)) + OutlinedButton( + onClick = logOut + ) { + Text(text = "logout") + } + } + } +} \ 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 new file mode 100644 index 0000000..09cb017 --- /dev/null +++ b/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/HomeViewModel.kt @@ -0,0 +1,20 @@ +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() { + viewModelScope.launch { + appDataStore.setToken("") + appDataStore.setUuid("") + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..02df7fd --- /dev/null +++ b/feature/home/src/main/java/com/stslex/aproselection/feature/home/ui/navigation/HomeRouter.kt @@ -0,0 +1,29 @@ +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.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) } + ) + + HomeScreen( + modifier = modifier, + logOut = viewModel::logOut + ) + } +} \ No newline at end of file diff --git a/feature/home/src/test/java/com/stslex/aproselection/feature/home/ExampleUnitTest.kt b/feature/home/src/test/java/com/stslex/aproselection/feature/home/ExampleUnitTest.kt new file mode 100644 index 0000000..bd523d3 --- /dev/null +++ b/feature/home/src/test/java/com/stslex/aproselection/feature/home/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.stslex.aproselection.feature.home + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c8db7f9..9241798 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] androidDesugarJdkLibs = "2.0.3" -kotlin = "1.8.22" +kotlin = "1.9.0" +kotlinLanguage = "1.9" androidGradlePlugin = "8.0.2" ktx = "1.10.1" @@ -8,7 +9,7 @@ material = "1.9.0" appcompat = "1.6.1" immutableCollection = "0.3.5" -composeCompiler = "1.4.8" +composeCompiler = "1.5.1" composeBom = "2023.06.01" composeNavigation = "2.6.0" accompanist = "0.30.0" @@ -46,6 +47,7 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi androidx-compose-activity = "androidx.activity:activity-compose:1.7.2" androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material = { group = "androidx.compose.material", name = "material" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 4088e18..99483fc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,3 +23,4 @@ include(":core:network") include(":feature:auth") include(":core:ui") include(":core:datastore") +include(":feature:home")