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 { buildFeatures {
compose = true compose = true
buildConfig = true
} }
composeOptions { composeOptions {
@ -85,6 +86,7 @@ dependencies {
implementation("androidx.compose.ui:ui:1.4.3") implementation("androidx.compose.ui:ui:1.4.3")
implementation("androidx.compose.ui:ui-graphics: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.ui:ui-tooling-preview:1.4.3")
implementation("androidx.compose.material:material:1.4.3")
implementation("androidx.compose.material3:material3:1.1.1") implementation("androidx.compose.material3:material3:1.1.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3") 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_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -18,7 +19,6 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
@ -38,17 +38,21 @@ import kotlinx.coroutines.CoroutineScope
@Composable @Composable
fun AuthenticationScreen( fun AuthenticationScreen(
viewModel: AuthenticationViewModel = hiltViewModel() authenticationVM: AuthenticationViewModel = hiltViewModel(),
versionVM: VersionViewModel = hiltViewModel(),
) { ) {
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val activity = LocalActivity.current val activity = LocalActivity.current
val state = viewModel.rememberAuthenticationState() val state = authenticationVM.rememberAuthenticationState()
Surface { Surface {
AuthenticationScreenContent( AuthenticationScreenContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
version = versionVM.version,
onGoogleSignIn = { onGoogleSignIn = {
viewModel.signIn(activity = activity) authenticationVM.signIn(activity = activity)
}, },
) )
@ -74,34 +78,27 @@ fun HandleAuthenticationState(
@Composable @Composable
private fun AuthenticationScreenContent( private fun AuthenticationScreenContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
version: VersionViewModel.Version,
onGoogleSignIn: () -> Unit, onGoogleSignIn: () -> Unit,
) { ) {
Box( Column(
modifier = modifier, modifier = modifier,
contentAlignment = Alignment.BottomCenter, verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Bottom),
horizontalAlignment = Alignment.End,
) { ) {
Button( Button(
modifier = Modifier modifier = Modifier
.padding(all = 16.dp) .padding(all = 16.dp)
.fillMaxWidth(), .fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(), colors = ButtonDefaults.buttonColors(),
onClick = onGoogleSignIn, onClick = onGoogleSignIn,
) { ) {
Text(text = rememeberGoogleStringResource()) Text(text = rememeberGoogleStringResource())
} }
}
}
@Composable Text(
private fun rememberBackgroundGradient(): Brush { style = MaterialTheme.typography.labelSmall,
val colorScheme = MaterialTheme.colorScheme text = version.toText(),
return remember {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = 0.2f),
colorScheme.surface.copy(alpha = 0.5f),
colorScheme.surface.copy(alpha = 1.0f),
)
) )
} }
} }
@ -175,8 +172,14 @@ private fun rememeberGoogleStringResource(): AnnotatedString {
@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_YES)
private fun AuthenticationScreenContentPreview() { private fun AuthenticationScreenContentPreview() {
LexiconTheme { LexiconTheme {
Surface {
AuthenticationScreenContent( AuthenticationScreenContent(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
version = VersionViewModel.Version(R.string.app_name, "0.0.0", "0"),
onGoogleSignIn = { }, 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] val source = repository.data.value[savedStateHandle.characterDetailArgument.id]
return CharacterDetailUio( return CharacterDetailUio(
name = source.name, name = source.name,
diminutive = source.diminutive?.let { "./ $it" }, diminutive = source.diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" },
gender = when (source.gender) { gender = when (source.gender) {
Lexicon.Gender.MALE -> "homme" Lexicon.Gender.MALE -> "homme"
Lexicon.Gender.FEMALE -> "femme" 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.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -21,11 +31,14 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalSnack import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.navigateToCharacterDetail import com.pixelized.rplexicon.ui.navigation.navigateToCharacterDetail
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default
@ -43,15 +56,28 @@ sealed class LexiconErrorUio {
object Default : LexiconErrorUio() object Default : LexiconErrorUio()
} }
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun LexiconScreen( fun LexiconScreen(
viewModel: LexiconViewModel = hiltViewModel(), viewModel: LexiconViewModel = hiltViewModel(),
) { ) {
val scope = rememberCoroutineScope()
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val refresh = rememberPullRefreshState(
refreshing = viewModel.isLoading.value,
onRefresh = {
scope.launch {
viewModel.fetchLexicon()
}
},
)
Surface { Surface {
LexiconScreenContent( LexiconScreenContent(
items = viewModel.items, items = viewModel.items,
refresh = refresh,
refreshing = viewModel.isLoading,
onItem = { onItem = {
screen.navigateToCharacterDetail(id = it.id) screen.navigateToCharacterDetail(id = it.id)
}, },
@ -66,23 +92,49 @@ fun LexiconScreen(
} }
} }
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable @Composable
private fun LexiconScreenContent( private fun LexiconScreenContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
refresh: PullRefreshState,
refreshing: State<Boolean>,
items: State<List<LexiconItemUio>>, items: State<List<LexiconItemUio>>,
onItem: (LexiconItemUio) -> Unit, onItem: (LexiconItemUio) -> Unit,
) { ) {
LazyColumn( Scaffold(
modifier = modifier, modifier = modifier,
topBar = {
TopAppBar(
title = {
Text(text = stringResource(id = R.string.app_name))
},
)
}
) {
Box(
modifier = Modifier.padding(paddingValues = it),
contentAlignment = Alignment.TopCenter,
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refresh),
contentPadding = PaddingValues(vertical = 8.dp), contentPadding = PaddingValues(vertical = 8.dp),
) { ) {
items(items = items.value) { items(items = items.value) { item ->
LexiconItem( LexiconItem(
modifier = Modifier modifier = Modifier
.clickable { onItem(it) } .clickable { onItem(item) }
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp), .padding(vertical = 8.dp, horizontal = 16.dp),
item = it, item = item,
)
}
}
PullRefreshIndicator(
refreshing = refreshing.value,
state = refresh,
) )
} }
} }
@ -116,6 +168,7 @@ fun HandleError(
} }
} }
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_YES)
@ -124,6 +177,11 @@ private fun LexiconScreenContentPreview() {
Surface { Surface {
LexiconScreenContent( LexiconScreenContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
refresh = rememberPullRefreshState(
refreshing = false,
onRefresh = {},
),
refreshing = remember { mutableStateOf(false) },
items = remember { items = remember {
mutableStateOf( mutableStateOf(
listOf( 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.model.Lexicon
import com.pixelized.rplexicon.repository.LexiconRepository import com.pixelized.rplexicon.repository.LexiconRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,6 +19,9 @@ class LexiconViewModel @Inject constructor(
private val repository: LexiconRepository, private val repository: LexiconRepository,
) : ViewModel() { ) : ViewModel() {
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList()) private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
val items: State<List<LexiconItemUio>> get() = _items val items: State<List<LexiconItemUio>> get() = _items
@ -52,6 +54,7 @@ class LexiconViewModel @Inject constructor(
suspend fun fetchLexicon() { suspend fun fetchLexicon() {
try { try {
_isLoading.value = true
repository.fetchLexicon() repository.fetchLexicon()
} }
// user need to accept OAuth2 permission. // user need to accept OAuth2 permission.
@ -64,6 +67,10 @@ class LexiconViewModel @Inject constructor(
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
_error.emit(LexiconErrorUio.Default) _error.emit(LexiconErrorUio.Default)
} }
// clean the laoding state
finally {
_isLoading.value = false
}
} }
companion object { companion object {

View file

@ -1,17 +1,13 @@
package com.pixelized.rplexicon.ui.theme package com.pixelized.rplexicon.ui.theme
import android.app.Activity import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@ -25,42 +21,32 @@ private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = Purple40,
secondary = PurpleGrey40, secondary = PurpleGrey40,
tertiary = Pink40 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 @Composable
fun LexiconTheme( fun LexiconTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { 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 darkTheme -> DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }
// val view = LocalView.current
// if (!view.isInEditMode) { val view = LocalView.current
// SideEffect { if (!view.isInEditMode) {
// val window = (view.context as Activity).window SideEffect {
// window.statusBarColor = colorScheme.primary.toArgb() val window = (view.context as Activity).window
// WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme colorScheme.background.toArgb().let {
// } window.statusBarColor = it
// } window.navigationBarColor = it
}
WindowCompat.getInsetsController(window, view).let {
it.isAppearanceLightStatusBars = !darkTheme
it.isAppearanceLightNavigationBars = !darkTheme
}
}
}
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,