Add compose UI

This commit is contained in:
Alex Gabor 2022-02-28 08:40:39 +02:00
parent 8866ac8477
commit a9dc65d0b6
11 changed files with 377 additions and 2 deletions

View file

@ -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"

View file

@ -25,6 +25,16 @@
</activity>
<activity android:name=".ui.home.MainActivity" />
<activity android:name=".ui.auth.AuthActivity" />
<activity
android:name=".ui.compose.ComposeActivity"
android:configChanges="colorMode|density|fontScale|fontWeightAdjustment|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -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()
}
}
}
}
}

View file

@ -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<IsUserLoggedInUseCase>()
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") }
}
}

View file

@ -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
}

View file

@ -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<Event<ErrorType>?>(null)
private set
var navigateToHome by mutableStateOf<Event<Unit>?>(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
}
}

View file

@ -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
)
}

View file

@ -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
}

View file

@ -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)
)
}
}

View file

@ -1,7 +1,6 @@
package org.fnives.test.showcase.ui.shared
@Suppress("DataClassContainsFunctions")
data class Event<T : Any>(private val data: T) {
class Event<T : Any>(private val data: T) {
private var consumed: Boolean = false