Add compose UI
This commit is contained in:
parent
8866ac8477
commit
a9dc65d0b6
11 changed files with 377 additions and 2 deletions
|
|
@ -34,6 +34,10 @@ android {
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = project.androidx_compose
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
@ -73,7 +77,18 @@ dependencies {
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_livedata_version"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_livedata_version"
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:$androidx_swiperefreshlayout_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-android:$koin_version"
|
||||||
|
implementation "io.insert-koin:koin-androidx-compose:$koin_version"
|
||||||
|
|
||||||
implementation "androidx.room:room-runtime:$androidx_room_version"
|
implementation "androidx.room:room-runtime:$androidx_room_version"
|
||||||
kapt "androidx.room:room-compiler:$androidx_room_version"
|
kapt "androidx.room:room-compiler:$androidx_room_version"
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,16 @@
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ui.home.MainActivity" />
|
<activity android:name=".ui.home.MainActivity" />
|
||||||
<activity android:name=".ui.auth.AuthActivity" />
|
<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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package org.fnives.test.showcase.ui.shared
|
package org.fnives.test.showcase.ui.shared
|
||||||
|
|
||||||
@Suppress("DataClassContainsFunctions")
|
class Event<T : Any>(private val data: T) {
|
||||||
data class Event<T : Any>(private val data: T) {
|
|
||||||
|
|
||||||
private var consumed: Boolean = false
|
private var consumed: Boolean = false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ project.ext {
|
||||||
androidx_swiperefreshlayout_version = "1.1.0"
|
androidx_swiperefreshlayout_version = "1.1.0"
|
||||||
androidx_room_version = "2.4.1"
|
androidx_room_version = "2.4.1"
|
||||||
activity_ktx_version = "1.4.0"
|
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"
|
coroutines_version = "1.6.0"
|
||||||
turbine_version = "0.7.0"
|
turbine_version = "0.7.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue