From a9dc65d0b60ab329881ff91328ac5a59c0112ab0 Mon Sep 17 00:00:00 2001 From: Alex Gabor Date: Mon, 28 Feb 2022 08:40:39 +0200 Subject: [PATCH] Add compose UI --- app/build.gradle | 15 +++ app/src/main/AndroidManifest.xml | 10 ++ .../showcase/ui/compose/ComposeActivity.kt | 22 ++++ .../ui/compose/screen/AppNavigation.kt | 33 ++++++ .../ui/compose/screen/auth/AuthScreen.kt | 101 ++++++++++++++++++ .../ui/compose/screen/auth/AuthScreenState.kt | 80 ++++++++++++++ .../ui/compose/screen/home/HomeScreen.kt | 39 +++++++ .../ui/compose/screen/home/HomeScreenState.kt | 41 +++++++ .../ui/compose/screen/splash/SplashScreen.kt | 31 ++++++ .../fnives/test/showcase/ui/shared/Event.kt | 3 +- gradlescripts/versions.gradle | 4 + 11 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/compose/ComposeActivity.kt create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/compose/screen/AppNavigation.kt create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreen.kt create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreenState.kt create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/compose/screen/home/HomeScreen.kt create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/compose/screen/home/HomeScreenState.kt create mode 100644 app/src/main/java/org/fnives/test/showcase/ui/compose/screen/splash/SplashScreen.kt diff --git a/app/build.gradle b/app/build.gradle index 9ea2b8c..5e58fcd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,6 +34,10 @@ android { buildFeatures { viewBinding true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = project.androidx_compose } sourceSets { @@ -73,7 +77,18 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_livedata_version" implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_version" + implementation "androidx.activity:activity-compose:$activity_ktx_version" + implementation "androidx.navigation:navigation-compose:$androidx_navigation" + + implementation "androidx.compose.ui:ui:$androidx_compose" + implementation "androidx.compose.ui:ui-tooling:$androidx_compose" + implementation "androidx.compose.foundation:foundation:$androidx_compose" + implementation "androidx.compose.material:material:$androidx_compose" + implementation "com.google.accompanist:accompanist-insets:$google_accompanist" + implementation "com.google.accompanist:accompanist-swiperefresh:$google_accompanist" + implementation "io.insert-koin:koin-android:$koin_version" + implementation "io.insert-koin:koin-androidx-compose:$koin_version" implementation "androidx.room:room-runtime:$androidx_room_version" kapt "androidx.room:room-compiler:$androidx_room_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 92f5a5e..f90a9ed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/ComposeActivity.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/ComposeActivity.kt new file mode 100644 index 0000000..bcecc68 --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/ComposeActivity.kt @@ -0,0 +1,22 @@ +package org.fnives.test.showcase.ui.compose + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.MaterialTheme +import com.google.accompanist.insets.ProvideWindowInsets +import org.fnives.test.showcase.ui.compose.screen.AppNavigation + +class ComposeActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + ProvideWindowInsets { + MaterialTheme { + AppNavigation() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/AppNavigation.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/AppNavigation.kt new file mode 100644 index 0000000..4ab1c20 --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/AppNavigation.kt @@ -0,0 +1,33 @@ +package org.fnives.test.showcase.ui.compose.screen + +import androidx.compose.foundation.background +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.delay +import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase +import org.fnives.test.showcase.ui.compose.screen.auth.AuthScreen +import org.fnives.test.showcase.ui.compose.screen.splash.SplashScreen +import org.koin.androidx.compose.get + +@Composable +fun AppNavigation() { + val navController = rememberNavController() + + val isUserLogeInUseCase = get() + LaunchedEffect(isUserLogeInUseCase) { + delay(500) + navController.navigate(if (isUserLogeInUseCase.invoke()) "Home" else "Auth") + } + + NavHost(navController, startDestination = "Splash", modifier = Modifier.background(MaterialTheme.colors.surface)) { + composable("Splash") { SplashScreen() } + composable("Auth") { AuthScreen() } + composable("Home") { Text("Home") } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreen.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreen.kt new file mode 100644 index 0000000..4b4f740 --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreen.kt @@ -0,0 +1,101 @@ +package org.fnives.test.showcase.ui.compose.screen.auth + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.unit.dp +import com.google.accompanist.insets.systemBarsPadding +import org.fnives.test.showcase.R + +@Composable +fun AuthScreen( + authScreenState: AuthScreenState = rememberAuthScreen() +) { + Column( + Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + Title() + Column( + Modifier + .fillMaxWidth() + .padding(16.dp) + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + value = authScreenState.username, + onValueChange = { authScreenState.onUsernameChanged(it) }, + modifier = Modifier.fillMaxWidth() + ) + TextField( + value = authScreenState.password, + onValueChange = { authScreenState.onPasswordChanged(it) }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + } + + Snackbar(authScreenState) + LoginButton( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(16.dp), + onClick = { authScreenState.onLogin() } + ) + } +} + +@Composable +private fun Snackbar(authScreenState: AuthScreenState) { + val snackbarState = remember { SnackbarHostState() } + val errorType = authScreenState.error?.consume() + LaunchedEffect(errorType) { + if (errorType != null) { + snackbarState.showSnackbar(errorType.name) + } + } + SnackbarHost(hostState = snackbarState) { + val stringId = errorType?.stringResId() + if (stringId != null) { + Snackbar(modifier = Modifier.padding(horizontal = 16.dp)) { + Text(text = stringResource(stringId)) + } + } + } +} + +@Composable +private fun LoginButton(modifier: Modifier = Modifier, onClick: () -> Unit) { + Box(modifier) { + Button(onClick = onClick, Modifier.fillMaxWidth()) { + Text(text = "Login") + } + } +} + +@Composable +private fun Title() { + Text( + stringResource(id = R.string.login_title), + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.h4 + ) +} + +private fun AuthScreenState.ErrorType.stringResId() = when (this) { + AuthScreenState.ErrorType.INVALID_CREDENTIALS -> R.string.credentials_invalid + AuthScreenState.ErrorType.GENERAL_NETWORK_ERROR -> R.string.something_went_wrong + AuthScreenState.ErrorType.UNSUPPORTED_USERNAME -> R.string.username_is_invalid + AuthScreenState.ErrorType.UNSUPPORTED_PASSWORD -> R.string.password_is_invalid +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreenState.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreenState.kt new file mode 100644 index 0000000..a7fa90b --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/auth/AuthScreenState.kt @@ -0,0 +1,80 @@ +package org.fnives.test.showcase.ui.compose.screen.auth + +import androidx.compose.runtime.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.fnives.test.showcase.core.login.LoginUseCase +import org.fnives.test.showcase.model.auth.LoginCredentials +import org.fnives.test.showcase.model.auth.LoginStatus +import org.fnives.test.showcase.model.shared.Answer +import org.fnives.test.showcase.ui.shared.Event +import org.koin.androidx.compose.get + +@Composable +fun rememberAuthScreen( + stateScope: CoroutineScope = rememberCoroutineScope(), + loginUseCase: LoginUseCase = get(), +): AuthScreenState { + return remember { AuthScreenState(stateScope, loginUseCase) } +} + +class AuthScreenState( + private val stateScope: CoroutineScope, + private val loginUseCase: LoginUseCase, +) { + + var username by mutableStateOf("") + private set + var password by mutableStateOf("") + private set + var loading by mutableStateOf(false) + private set + var error by mutableStateOf?>(null) + private set + var navigateToHome by mutableStateOf?>(null) + private set + + fun onUsernameChanged(username: String) { + this.username = username + } + + fun onPasswordChanged(password: String) { + this.password = password + } + + fun onLogin() { + if (loading) { + return + } + loading = true + stateScope.launch { + val credentials = LoginCredentials( + username = username, + password = password + ) + when (val response = loginUseCase.invoke(credentials)) { + is Answer.Error -> error = Event(ErrorType.GENERAL_NETWORK_ERROR) + is Answer.Success -> processLoginStatus(response.data) + } + loading = false + } + } + + private fun processLoginStatus(loginStatus: LoginStatus) { + when (loginStatus) { + LoginStatus.SUCCESS -> navigateToHome = Event(Unit) + LoginStatus.INVALID_CREDENTIALS -> error = Event(ErrorType.INVALID_CREDENTIALS) + LoginStatus.INVALID_USERNAME -> error = Event(ErrorType.UNSUPPORTED_USERNAME).also { println("asdasdasd: ${it.hashCode()}") + } + LoginStatus.INVALID_PASSWORD -> error = Event(ErrorType.UNSUPPORTED_PASSWORD) + } + println("asdasdasd: ${error.hashCode()}") + } + + enum class ErrorType { + INVALID_CREDENTIALS, + GENERAL_NETWORK_ERROR, + UNSUPPORTED_USERNAME, + UNSUPPORTED_PASSWORD + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/home/HomeScreen.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/home/HomeScreen.kt new file mode 100644 index 0000000..b0cd4fa --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/home/HomeScreen.kt @@ -0,0 +1,39 @@ +package org.fnives.test.showcase.ui.compose.screen.home + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import org.fnives.test.showcase.R + +@Composable +fun HomeScreen( + homeScreenState = rememberHomeScreenState() +) { + Column(Modifier.fillMaxSize()) { + Title() + SwipeRefresh(state = rememberSwipeRefreshState(isRefreshing = false), onRefresh = { }) { + LazyColumn { + + } + } + } +} + + +@Composable +private fun Title() { + Text( + stringResource(id = R.string.login_title), + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.h4 + ) +} diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/home/HomeScreenState.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/home/HomeScreenState.kt new file mode 100644 index 0000000..fe59ac1 --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/home/HomeScreenState.kt @@ -0,0 +1,41 @@ +package org.fnives.test.showcase.ui.compose.screen.home + +import androidx.compose.runtime.* +import org.fnives.test.showcase.core.content.AddContentToFavouriteUseCase +import org.fnives.test.showcase.core.content.FetchContentUseCase +import org.fnives.test.showcase.core.content.GetAllContentUseCase +import org.fnives.test.showcase.core.content.RemoveContentFromFavouritesUseCase +import org.fnives.test.showcase.core.login.LogoutUseCase +import org.koin.androidx.compose.get + +@Composable +fun rememberHomeScreenState( + getAllContentUseCase: GetAllContentUseCase = get(), + logoutUseCase: LogoutUseCase = get(), + fetchContentUseCase: FetchContentUseCase = get(), + addContentToFavouriteUseCase: AddContentToFavouriteUseCase = get(), + removeContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase = get(), +): HomeScreenState { + return remember { + HomeScreenState( + getAllContentUseCase, + logoutUseCase, + fetchContentUseCase, + addContentToFavouriteUseCase, + removeContentFromFavouritesUseCase + ) + } +} + +class HomeScreenState( + private val getAllContentUseCase: GetAllContentUseCase, + private val logoutUseCase: LogoutUseCase, + private val fetchContentUseCase: FetchContentUseCase, + private val addContentToFavouriteUseCase: AddContentToFavouriteUseCase, + private val removeContentFromFavouritesUseCase: RemoveContentFromFavouritesUseCase +) { + + var loading by mutableStateOf(false) + private set + +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/splash/SplashScreen.kt b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/splash/SplashScreen.kt new file mode 100644 index 0000000..a557a66 --- /dev/null +++ b/app/src/main/java/org/fnives/test/showcase/ui/compose/screen/splash/SplashScreen.kt @@ -0,0 +1,31 @@ +package org.fnives.test.showcase.ui.compose.screen.splash + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import org.fnives.test.showcase.R + + +@Composable +fun SplashScreen() { + Box(Modifier.fillMaxSize().background(colorResource(R.color.purple_700)), contentAlignment = Alignment.Center) { + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier.size(120.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/test/showcase/ui/shared/Event.kt b/app/src/main/java/org/fnives/test/showcase/ui/shared/Event.kt index 30a323c..dde6082 100644 --- a/app/src/main/java/org/fnives/test/showcase/ui/shared/Event.kt +++ b/app/src/main/java/org/fnives/test/showcase/ui/shared/Event.kt @@ -1,7 +1,6 @@ package org.fnives.test.showcase.ui.shared -@Suppress("DataClassContainsFunctions") -data class Event(private val data: T) { +class Event(private val data: T) { private var consumed: Boolean = false diff --git a/gradlescripts/versions.gradle b/gradlescripts/versions.gradle index d9fc0f0..e1d8d3e 100644 --- a/gradlescripts/versions.gradle +++ b/gradlescripts/versions.gradle @@ -7,6 +7,10 @@ project.ext { androidx_swiperefreshlayout_version = "1.1.0" androidx_room_version = "2.4.1" activity_ktx_version = "1.4.0" + androidx_navigation = "2.4.0" + + androidx_compose = "1.1.0-rc03" + google_accompanist = "0.20.3" coroutines_version = "1.6.0" turbine_version = "0.7.0"