Skip to content

Commit

Permalink
Add loading indicator to UI
Browse files Browse the repository at this point in the history
Make tri-states more explicit
Ensure composables have idiomatic modifier parameter
  • Loading branch information
jeffdgr8 committed Feb 24, 2024
1 parent 890e34c commit 4b439e9
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import data.db.DatabaseProvider
import data.db.UserScopeProvider
import data.source.user.UserRepository
import domain.model.Note
import domain.model.User
import kotbase.Collection
import kotbase.Expression
import kotbase.From
Expand Down Expand Up @@ -36,7 +37,7 @@ class NoteRepository(

init {
userRepository.user
.filterNot { it?.userId.isNullOrBlank() }
.filterIsInstance<User.Authenticated>()
.onEach {
dbCollection.createIndex(FTS_INDEX, FullTextIndexConfiguration("title", "text"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package data.source.user

import data.db.DatabaseProvider
import domain.model.User
import kotbase.Collection
import kotbase.ktx.documentFlow
import kotlinx.coroutines.*
Expand All @@ -13,13 +14,21 @@ class UserRepository(
private val dbCollection: Collection
get() = dbProvider.database.defaultCollection

val user: StateFlow<UserDoc?> =
val user: StateFlow<User> =
dbCollection.documentFlow(USER_DOC_ID, dbProvider.readContext)
.map(::decodeDocument)
.stateIn(dbProvider.scope, SharingStarted.Eagerly, UserDoc())
.map { userDoc ->
userDoc?.let {
User.Authenticated(it.userId, it.password)
} ?: User.None
}
.stateIn(dbProvider.scope, SharingStarted.Eagerly, User.Unknown)

val userId: String?
get() = user.value?.userId?.ifBlank { null }
get() = when (val user = user.value) {
is User.Authenticated -> user.userId
else -> null
}

suspend fun saveUser(userId: String, password: String): Boolean {
withContext(dbProvider.writeContext) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package domain.model

sealed interface User {
data class Authenticated(val userId: String, val password: String) : User
data object None : User
data object Unknown : User
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package domain.replication

import data.db.DatabaseProvider
import data.source.user.UserRepository
import domain.model.User
import io.ktor.client.HttpClient
import io.ktor.client.request.basicAuth
import io.ktor.client.request.get
Expand All @@ -19,10 +20,10 @@ class AuthService(

val authStatus: StateFlow<AuthStatus> =
userRepository.user.map {
when {
it == null -> AuthStatus.LoggedOut
it.userId.isBlank() -> AuthStatus.Unknown
else -> AuthStatus.LoggedIn
when (it) {
is User.Authenticated -> AuthStatus.LoggedIn
User.None -> AuthStatus.LoggedOut
User.Unknown -> AuthStatus.Unknown
}
}
.stateIn(dbProvider.scope, SharingStarted.Eagerly, AuthStatus.Unknown)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package domain.replication
import data.db.DatabaseProvider
import data.source.note.NoteRepository
import data.source.user.UserRepository
import domain.model.User
import kotbase.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
Expand All @@ -18,9 +19,10 @@ class ReplicationService(
userRepository.user
.onEach { user ->
replicator?.stop()
replicatorFlow.value = if (user?.userId?.isNotBlank() == true) {
createReplicator(user.userId, user.password)
} else null
replicatorFlow.value = when (user) {
is User.Authenticated -> createReplicator(user.userId, user.password)
else -> null
}
checkStart()
}
.launchIn(dbProvider.scope + Dispatchers.Default)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package presentation

import data.source.note.NoteRepository
import domain.model.Note
import domain.replication.AuthService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -27,15 +28,22 @@ class MainViewModel(
}

@OptIn(ExperimentalCoroutinesApi::class)
val notes = combine(searchText, useFts) { searchText, useFts ->
val notes: StateFlow<NotesState> =
combine(searchText, useFts) { searchText, useFts ->
Pair(searchText, useFts)
}
.flatMapLatest { (searchText, useFts) ->
noteRepository.getNotesFlow(searchText, useFts)
}
.stateIn(scope, SharingStarted.Eagerly, emptyList())
.flatMapLatest { (searchText, useFts) ->
noteRepository.getNotesFlow(searchText, useFts)
}
.map { NotesState.Notes(it) }
.stateIn(scope, SharingStarted.Eagerly, NotesState.Loading)

fun logout() {
authService.logout()
}
}

sealed interface NotesState {
data object Loading : NotesState
data class Notes(val notes: List<Note>) : NotesState
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ fun LoginScreen() {
fun UsernameField(
value: String,
onValueChange: (String) -> Unit,
onEnterPressed: () -> Unit
onEnterPressed: () -> Unit,
modifier: Modifier = Modifier
) {
val focusManager = LocalFocusManager.current

Expand All @@ -97,7 +98,7 @@ fun UsernameField(
label = { Text("Username") },
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
modifier = Modifier.onPreviewKeyEvent(tabFocus(focusManager, FocusDirection.Next))
modifier = modifier.onPreviewKeyEvent(tabFocus(focusManager, FocusDirection.Next))
.onPreviewKeyEvent(handleEnter(onEnterPressed))
)
}
Expand All @@ -106,7 +107,8 @@ fun UsernameField(
fun PasswordField(
value: String,
onValueChange: (String) -> Unit,
onEnterPressed: () -> Unit
onEnterPressed: () -> Unit,
modifier: Modifier = Modifier
) {
var showPassword by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
Expand All @@ -127,7 +129,7 @@ fun PasswordField(
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Go),
keyboardActions = KeyboardActions(onGo = { onEnterPressed() }),
modifier = Modifier.onPreviewKeyEvent(tabFocus(focusManager, FocusDirection.Next))
modifier = modifier.onPreviewKeyEvent(tabFocus(focusManager, FocusDirection.Next))
.onPreviewKeyEvent(handleEnter(onEnterPressed))
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ import domain.randomNanoId
import org.koin.compose.koinInject
import org.koin.core.parameter.parametersOf
import presentation.MainViewModel
import presentation.NotesState
import ui.widget.ReplicationStatus

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(onNoteSelected: (String) -> Unit) {
fun MainScreen(
onNoteSelected: (String) -> Unit
) {
val scope = rememberCoroutineScope()
val viewModel: MainViewModel = koinInject { parametersOf(scope) }
val notes by viewModel.notes.collectAsState()
Expand Down Expand Up @@ -81,7 +84,10 @@ fun MainScreen(onNoteSelected: (String) -> Unit) {
onTextChange = viewModel::updateSearchText,
modifier = Modifier.fillMaxWidth()
)
NotesGrid(notes, onNoteSelected)
when (val state = notes) {
NotesState.Loading -> LoadingIndicator()
is NotesState.Notes -> NotesGrid(state.notes, onNoteSelected)
}
}
}
}
Expand Down Expand Up @@ -133,6 +139,17 @@ fun FtsSwitch(
}
}

@Composable
fun LoadingIndicator(
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.weight(1F))
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
Spacer(modifier = Modifier.weight(1F))
}
}

@Composable
fun NotesGrid(
notes: List<Note>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ui.widget

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
Expand All @@ -10,14 +11,15 @@ import kotbase.ReplicatorStatus

@Composable
fun ReplicationStatus(
status: ReplicatorStatus?
status: ReplicatorStatus?,
modifier: Modifier = Modifier
) {
when (status?.activityLevel) {
ReplicatorActivityLevel.CONNECTING -> CircularProgressIndicator(modifier = Modifier.size(24.dp))
ReplicatorActivityLevel.CONNECTING -> CircularProgressIndicator(modifier = modifier.size(24.dp))
ReplicatorActivityLevel.BUSY -> CircularProgressIndicator(
progress = with(status.progress) { completed.toFloat() / total },
modifier = Modifier.size(24.dp)
modifier = modifier.size(24.dp)
)
else -> {}
else -> Spacer(modifier = modifier.size(24.dp))
}
}

0 comments on commit 4b439e9

Please sign in to comment.