Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix blink favorite icon #945

Closed
wants to merge 14 commits into from
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.produceState
import androidx.compose.runtime.staticCompositionLocalOf
import io.github.takahirom.rin.produceRetainedState
import kotlinx.coroutines.CancellationException
Expand Down Expand Up @@ -57,6 +59,28 @@ fun SafeLaunchedEffect(key: Any?, block: suspend CoroutineScope.() -> Unit) {
}
}

@Composable
fun <T : R, R> StateFlow<T>.safeCollectAsState(
context: CoroutineContext = EmptyCoroutineContext,
): State<R> {
val composeEffectErrorHandler = LocalComposeEffectErrorHandler.current
return produceState(value, this, context) {
try {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else {
withContext(context) {
collect { value = it }
}
}
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
composeEffectErrorHandler.emit(e)
}
}
}

@Composable
fun <T : R, R> Flow<T>.safeCollectAsRetainedState(
initial: R,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import androidx.compose.runtime.setValue
import co.touchlab.kermit.Logger
import io.github.droidkaigi.confsched.compose.SafeLaunchedEffect
import io.github.droidkaigi.confsched.compose.safeCollectAsRetainedState
import io.github.droidkaigi.confsched.compose.safeCollectAsState
import io.github.droidkaigi.confsched.data.sessions.response.SessionsAllResponse
import io.github.droidkaigi.confsched.data.user.UserDataStore
import io.github.droidkaigi.confsched.model.DroidKaigi2024Day
import io.github.droidkaigi.confsched.model.SessionsRepository
import io.github.droidkaigi.confsched.model.Timetable
import io.github.droidkaigi.confsched.model.TimetableItem
import io.github.droidkaigi.confsched.model.TimetableItemId
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
Expand All @@ -44,7 +44,7 @@ public class DefaultSessionsRepository(
refreshSessionData()
emitAll(sessionCacheDataStore.getTimetableStream())
},
userDataStore.getFavoriteSessionStream(),
userDataStore.getFavoriteSessionStream,
) { timetable, favorites ->
timetable.copy(bookmarks = favorites)
}
Expand Down Expand Up @@ -110,8 +110,8 @@ public class DefaultSessionsRepository(
}
}.safeCollectAsRetainedState(Timetable())
val favoriteSessions by remember {
userDataStore.getFavoriteSessionStream()
}.safeCollectAsRetainedState(persistentSetOf())
userDataStore.getFavoriteSessionStream
}.safeCollectAsState()

Logger.d { "DefaultSessionsRepository timetable() count=${timetable.timetableItems.size}" }
return timetable.copy(bookmarks = favoriteSessions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,28 @@ import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import io.github.droidkaigi.confsched.model.TimetableItemId
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

public class UserDataStore(private val dataStore: DataStore<Preferences>) {

private val mutableIdToken = MutableStateFlow<String?>(null)
public val idToken: StateFlow<String?> = mutableIdToken

public fun getFavoriteSessionStream(): Flow<PersistentSet<TimetableItemId>> {
return dataStore.data
private val singletonCoroutineScope: CoroutineScope = CoroutineScope(Job())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be an unmanaged dispatcher in tests, which could result in flaky tests. I think we need to pass a dispatcher for this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comment.

I modified it so that testDispatcher can be used during testing.
Does this fix seem okay to you?

move coroutineScope field to constructor parameter
Add TestUserDataStoreModule


public val getFavoriteSessionStream: StateFlow<PersistentSet<TimetableItemId>> =
dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
Expand All @@ -34,11 +41,14 @@ public class UserDataStore(private val dataStore: DataStore<Preferences>) {
(preferences[KEY_FAVORITE_SESSION_IDS]?.split(",") ?: listOf())
.map { TimetableItemId(it) }
.toPersistentSet()
}
}
}.stateIn(
scope = singletonCoroutineScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = persistentSetOf()
)

public suspend fun toggleFavorite(id: TimetableItemId) {
val updatedFavorites = getFavoriteSessionStream().first().toMutableSet()
val updatedFavorites = getFavoriteSessionStream.first().toMutableSet()

if (updatedFavorites.contains(id)) {
updatedFavorites.remove(id)
Expand Down
Loading