Add pull to refresh on the Lexion.

This commit is contained in:
Andres Gomez, Thomas (ITDV CC) - AF (ext) 2023-07-16 14:37:18 +02:00
parent 6167999001
commit c5fb8bf99e
7 changed files with 153 additions and 63 deletions

View file

@ -64,6 +64,7 @@ android {
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
@ -85,6 +86,7 @@ dependencies {
implementation("androidx.compose.ui:ui:1.4.3")
implementation("androidx.compose.ui:ui-graphics:1.4.3")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
implementation("androidx.compose.material:material:1.4.3")
implementation("androidx.compose.material3:material3:1.1.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")

View file

@ -2,7 +2,8 @@ package com.pixelized.rplexicon.ui.screens.authentication
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -18,7 +19,6 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
@ -38,17 +38,21 @@ import kotlinx.coroutines.CoroutineScope
@Composable
fun AuthenticationScreen(
viewModel: AuthenticationViewModel = hiltViewModel()
authenticationVM: AuthenticationViewModel = hiltViewModel(),
versionVM: VersionViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
val activity = LocalActivity.current
val state = viewModel.rememberAuthenticationState()
val state = authenticationVM.rememberAuthenticationState()
Surface {
AuthenticationScreenContent(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
version = versionVM.version,
onGoogleSignIn = {
viewModel.signIn(activity = activity)
authenticationVM.signIn(activity = activity)
},
)
@ -74,34 +78,27 @@ fun HandleAuthenticationState(
@Composable
private fun AuthenticationScreenContent(
modifier: Modifier = Modifier,
version: VersionViewModel.Version,
onGoogleSignIn: () -> Unit,
) {
Box(
Column(
modifier = modifier,
contentAlignment = Alignment.BottomCenter,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Bottom),
horizontalAlignment = Alignment.End,
) {
Button(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(),
colors = ButtonDefaults.buttonColors(),
onClick = onGoogleSignIn,
) {
Text(text = rememeberGoogleStringResource())
}
}
}
@Composable
private fun rememberBackgroundGradient(): Brush {
val colorScheme = MaterialTheme.colorScheme
return remember {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = 0.2f),
colorScheme.surface.copy(alpha = 0.5f),
colorScheme.surface.copy(alpha = 1.0f),
)
Text(
style = MaterialTheme.typography.labelSmall,
text = version.toText(),
)
}
}
@ -175,8 +172,14 @@ private fun rememeberGoogleStringResource(): AnnotatedString {
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun AuthenticationScreenContentPreview() {
LexiconTheme {
AuthenticationScreenContent(
onGoogleSignIn = { },
)
Surface {
AuthenticationScreenContent(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
version = VersionViewModel.Version(R.string.app_name, "0.0.0", "0"),
onGoogleSignIn = { },
)
}
}
}

View file

@ -0,0 +1,34 @@
package com.pixelized.rplexicon.ui.screens.authentication
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.BuildConfig
import com.pixelized.rplexicon.R
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class VersionViewModel @Inject constructor() : ViewModel() {
val version = Version(
appName = R.string.app_name,
version = BuildConfig.VERSION_NAME,
code = BuildConfig.VERSION_CODE.toString()
)
@Stable
data class Version(
@StringRes val appName: Int,
val version: String,
val code: String,
) {
@Composable
@Stable
fun toText(): String {
return "${stringResource(id = appName)} ${version}.${code}"
}
}
}

View file

@ -21,7 +21,7 @@ class CharacterDetailViewModel @Inject constructor(
val source = repository.data.value[savedStateHandle.characterDetailArgument.id]
return CharacterDetailUio(
name = source.name,
diminutive = source.diminutive?.let { "./ $it" },
diminutive = source.diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" },
gender = when (source.gender) {
Lexicon.Gender.MALE -> "homme"
Lexicon.Gender.FEMALE -> "femme"

View file

@ -7,13 +7,23 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
@ -21,11 +31,14 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.navigateToCharacterDetail
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default
@ -43,15 +56,28 @@ sealed class LexiconErrorUio {
object Default : LexiconErrorUio()
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun LexiconScreen(
viewModel: LexiconViewModel = hiltViewModel(),
) {
val scope = rememberCoroutineScope()
val screen = LocalScreenNavHost.current
val refresh = rememberPullRefreshState(
refreshing = viewModel.isLoading.value,
onRefresh = {
scope.launch {
viewModel.fetchLexicon()
}
},
)
Surface {
LexiconScreenContent(
items = viewModel.items,
refresh = refresh,
refreshing = viewModel.isLoading,
onItem = {
screen.navigateToCharacterDetail(id = it.id)
},
@ -66,23 +92,49 @@ fun LexiconScreen(
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
private fun LexiconScreenContent(
modifier: Modifier = Modifier,
refresh: PullRefreshState,
refreshing: State<Boolean>,
items: State<List<LexiconItemUio>>,
onItem: (LexiconItemUio) -> Unit,
) {
LazyColumn(
Scaffold(
modifier = modifier,
contentPadding = PaddingValues(vertical = 8.dp),
topBar = {
TopAppBar(
title = {
Text(text = stringResource(id = R.string.app_name))
},
)
}
) {
items(items = items.value) {
LexiconItem(
Box(
modifier = Modifier.padding(paddingValues = it),
contentAlignment = Alignment.TopCenter,
) {
LazyColumn(
modifier = Modifier
.clickable { onItem(it) }
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = it,
.fillMaxSize()
.pullRefresh(state = refresh),
contentPadding = PaddingValues(vertical = 8.dp),
) {
items(items = items.value) { item ->
LexiconItem(
modifier = Modifier
.clickable { onItem(item) }
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = item,
)
}
}
PullRefreshIndicator(
refreshing = refreshing.value,
state = refresh,
)
}
}
@ -116,6 +168,7 @@ fun HandleError(
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
@ -124,6 +177,11 @@ private fun LexiconScreenContentPreview() {
Surface {
LexiconScreenContent(
modifier = Modifier.fillMaxSize(),
refresh = rememberPullRefreshState(
refreshing = false,
onRefresh = {},
),
refreshing = remember { mutableStateOf(false) },
items = remember {
mutableStateOf(
listOf(

View file

@ -9,7 +9,6 @@ import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecovera
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.repository.LexiconRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
@ -20,6 +19,9 @@ class LexiconViewModel @Inject constructor(
private val repository: LexiconRepository,
) : ViewModel() {
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
val items: State<List<LexiconItemUio>> get() = _items
@ -52,6 +54,7 @@ class LexiconViewModel @Inject constructor(
suspend fun fetchLexicon() {
try {
_isLoading.value = true
repository.fetchLexicon()
}
// user need to accept OAuth2 permission.
@ -64,6 +67,10 @@ class LexiconViewModel @Inject constructor(
Log.e(TAG, exception.message, exception)
_error.emit(LexiconErrorUio.Default)
}
// clean the laoding state
finally {
_isLoading.value = false
}
}
companion object {

View file

@ -1,17 +1,13 @@
package com.pixelized.rplexicon.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
@ -25,42 +21,32 @@ private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun LexiconTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
// dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
// val context = LocalContext.current
// if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
// }
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
// val view = LocalView.current
// if (!view.isInEditMode) {
// SideEffect {
// val window = (view.context as Activity).window
// window.statusBarColor = colorScheme.primary.toArgb()
// WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
// }
// }
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
colorScheme.background.toArgb().let {
window.statusBarColor = it
window.navigationBarColor = it
}
WindowCompat.getInsetsController(window, view).let {
it.isAppearanceLightStatusBars = !darkTheme
it.isAppearanceLightNavigationBars = !darkTheme
}
}
}
MaterialTheme(
colorScheme = colorScheme,