From daefa0dca89a15ce50c70b7f77552823fafb7158 Mon Sep 17 00:00:00 2001 From: Martin Bonnin <martin@mbonnin.net> Date: Wed, 17 Apr 2024 09:17:13 +0200 Subject: [PATCH 1/3] make the app work offline --- .../store/graphql/ApolloExtensions.kt | 27 +++++ .../store/graphql/RoomsGraphQLRepository.kt | 8 +- .../graphql/SessionsGraphQLRepository.kt | 100 +++--------------- .../graphql/SpeakersGraphQLRepository.kt | 27 ++--- 4 files changed, 59 insertions(+), 103 deletions(-) diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/ApolloExtensions.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/ApolloExtensions.kt index c8070979..1d9a3c8f 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/ApolloExtensions.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/ApolloExtensions.kt @@ -1,10 +1,16 @@ package fr.androidmakers.store.graphql +import com.apollographql.apollo3.ApolloCall import com.apollographql.apollo3.api.ApolloResponse import com.apollographql.apollo3.api.Operation +import com.apollographql.apollo3.cache.normalized.FetchPolicy +import com.apollographql.apollo3.cache.normalized.fetchPolicy +import com.apollographql.apollo3.exception.ApolloException import com.apollographql.apollo3.exception.CacheMissException +import com.apollographql.apollo3.exception.DefaultApolloException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flow internal fun <T : Operation.Data> Flow<ApolloResponse<T>>.ignoreCacheMisses(): Flow<ApolloResponse<T>> { return filterNot { @@ -12,3 +18,24 @@ internal fun <T : Operation.Data> Flow<ApolloResponse<T>>.ignoreCacheMisses(): F it.exception is CacheMissException } } + + +internal fun <T : Operation.Data> ApolloCall<T>.cacheAndNetwork(): Flow<Result<T>> { + return flow { + var hasData = false + var exception: ApolloException? = null + fetchPolicy(FetchPolicy.CacheAndNetwork) + .toFlow() + .collect { + if (it.data != null) { + hasData = true + emit(Result.success(it.data!!)) + } else { + exception = it.exception ?: DefaultApolloException("No data found") + } + } + if (!hasData) { + emit(Result.failure(exception!!)) + } + } +} diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt index 8d4c39ab..77fb60a8 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt @@ -18,11 +18,7 @@ class RoomsGraphQLRepository(private val apolloClient: ApolloClient): RoomsRepos override fun getRooms(): Flow<Result<List<Room>>> { return apolloClient.query(GetRoomsQuery()) - .fetchPolicy(FetchPolicy.NetworkFirst) - .watch() - .map { - it.dataAssertNoErrors.rooms.map { it.roomDetails.toRoom() } - } - .toResultFlow() + .cacheAndNetwork() + .map { it.map { it.rooms.map { it.roomDetails.toRoom() } } } } } diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt index e5652417..f25daaf7 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SessionsGraphQLRepository.kt @@ -1,113 +1,45 @@ package fr.androidmakers.store.graphql import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.api.Mutation import com.apollographql.apollo3.cache.normalized.FetchPolicy -import com.apollographql.apollo3.cache.normalized.apolloStore import com.apollographql.apollo3.cache.normalized.fetchPolicy -import com.apollographql.apollo3.cache.normalized.optimisticUpdates -import com.apollographql.apollo3.cache.normalized.refetchPolicy -import com.apollographql.apollo3.cache.normalized.watch import fr.androidmakers.domain.model.Session import fr.androidmakers.domain.repo.SessionsRepository -import fr.androidmakers.store.graphql.type.buildBookmarkConnection -import fr.androidmakers.store.graphql.type.buildSession import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class SessionsGraphQLRepository(private val apolloClient: ApolloClient) : SessionsRepository { - - suspend fun addBookmark(uid: String?, sessionId: String): Boolean { - return modifyBookmarks(uid, AddBookmarkMutation(sessionId)) { sessionIds -> - AddBookmarkMutation.Data { - addBookmark = buildBookmarkConnection { - nodes = (sessionIds + sessionId).map { - buildSession { - this.id = it - } - } - } - } - } - } - - suspend fun removeBookmark(uid: String?, sessionId: String): Boolean { - return modifyBookmarks(uid, RemoveBookmarkMutation(sessionId)) { sessionIds -> - RemoveBookmarkMutation.Data { - removeBookmark = buildBookmarkConnection { - nodes = (sessionIds - sessionId).map { - buildSession { - this.id = it - } - } - } - } - } - } - override suspend fun setBookmark(userId: String, sessionId: String, value: Boolean) { - try { - if (value) { - addBookmark(userId, sessionId) - } else { - removeBookmark(userId, sessionId) - } - } catch (e: Exception) { - e.printStackTrace() + val mutation = if (value) { + AddBookmarkMutation(sessionId) + } else { + RemoveBookmarkMutation(sessionId) + } + val response = apolloClient.mutation(mutation).execute() + if (response.exception != null) { + response.exception!!.printStackTrace() } } override fun getSession(id: String): Flow<Result<Session>> { return apolloClient.query(GetSessionQuery(id)) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .watch() - .ignoreCacheMisses() - .map { - it.dataAssertNoErrors.session.sessionDetails.toSession() - } - .toResultFlow() + .cacheAndNetwork() + .map { it.map { it.session.sessionDetails.toSession() } } } override fun getBookmarks(uid: String): Flow<Result<Set<String>>> { return apolloClient.query(BookmarksQuery()) .fetchPolicy(FetchPolicy.NetworkOnly) - .refetchPolicy(FetchPolicy.CacheOnly) - .watch().map { - it.data!!.bookmarkConnection!!.nodes.map { it.id }.toSet() - }.toResultFlow() - } - - private suspend fun <D : Mutation.Data> modifyBookmarks( - uid: String?, - mutation: Mutation<D>, - data: (sessionIds: List<String>) -> D - ): Boolean { - val optimisticData = try { - val bookmarks = apolloClient.apolloStore.readOperation(BookmarksQuery()).bookmarkConnection - data(bookmarks!!.nodes.map { it.id }) - } catch (e: Exception) { - null - } - val response = apolloClient.mutation(mutation) - .apply { - if (optimisticData != null) { - optimisticUpdates(optimisticData) - } + .toFlow() + .map { + it.dataAssertNoErrors.bookmarkConnection.nodes.map { it.id }.toSet() } - .execute() - - return response.data != null + .toResultFlow() } override fun getSessions(): Flow<Result<List<Session>>> { return apolloClient.query(GetSessionsQuery()) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .watch() - .ignoreCacheMisses() - .map { - it.dataAssertNoErrors.sessions.nodes.map { it.sessionDetails.toSession() } - } - .toResultFlow() + .cacheAndNetwork() + .map { it.map { it.sessions.nodes.map { it.sessionDetails.toSession() } } } } - } diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt index 16d62eb3..9f54f316 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt @@ -4,6 +4,7 @@ import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.cache.normalized.FetchPolicy import com.apollographql.apollo3.cache.normalized.fetchPolicy import com.apollographql.apollo3.cache.normalized.watch +import com.apollographql.apollo3.exception.DefaultApolloException import fr.androidmakers.domain.model.Speaker import fr.androidmakers.domain.repo.SpeakersRepository import kotlinx.coroutines.flow.Flow @@ -13,24 +14,24 @@ class SpeakersGraphQLRepository(private val apolloClient: ApolloClient) : Speake override fun getSpeakers(): Flow<Result<List<Speaker>>> { return apolloClient.query(GetSpeakersQuery()) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .watch() - .ignoreCacheMisses() - .map { - it.dataAssertNoErrors.speakers.map { it.speakerDetails.toSpeaker() } - }.toResultFlow() + .cacheAndNetwork() + .map { it.map { it.speakers.map { it.speakerDetails.toSpeaker() } } } } override fun getSpeaker(id: String): Flow<Result<Speaker>> { return apolloClient.query(GetSpeakersQuery()) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .watch() - .ignoreCacheMisses() + .cacheAndNetwork() .map { - it.dataAssertNoErrors.speakers.map { it.speakerDetails }.singleOrNull { it.id == id } - ?.toSpeaker() - ?: error("no speaker") + if (it.isSuccess) { + val speaker = it.getOrThrow().speakers.map { it.speakerDetails }.singleOrNull { it.id == id }?.toSpeaker() + if (speaker != null) { + Result.success(speaker) + } else { + Result.failure(DefaultApolloException("Something wrong happend")) + } + } else { + Result.failure(it.exceptionOrNull()!!) + } } - .toResultFlow() } } From c74415a5c322b928e6ab31130f3fea509cf798cb Mon Sep 17 00:00:00 2001 From: Martin Bonnin <martin@mbonnin.net> Date: Wed, 17 Apr 2024 09:19:45 +0200 Subject: [PATCH 2/3] remove watch --- .../graphql/PartnersGraphQLRepository.kt | 24 ++++++++----------- .../store/graphql/RoomsGraphQLRepository.kt | 3 --- .../graphql/SpeakersGraphQLRepository.kt | 3 --- .../store/graphql/VenueGraphQLRepository.kt | 12 ++-------- 4 files changed, 12 insertions(+), 30 deletions(-) diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/PartnersGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/PartnersGraphQLRepository.kt index 588325db..4cb3bd28 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/PartnersGraphQLRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/PartnersGraphQLRepository.kt @@ -1,9 +1,6 @@ package fr.androidmakers.store.graphql import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.cache.normalized.FetchPolicy -import com.apollographql.apollo3.cache.normalized.fetchPolicy -import com.apollographql.apollo3.cache.normalized.watch import fr.androidmakers.domain.model.Partner import fr.androidmakers.domain.model.PartnerGroup import fr.androidmakers.domain.repo.PartnersRepository @@ -13,24 +10,23 @@ import kotlinx.coroutines.flow.map class PartnersGraphQLRepository(private val apolloClient: ApolloClient): PartnersRepository { override fun getPartners(): Flow<Result<List<PartnerGroup>>> { return apolloClient.query(GetPartnerGroupsQuery()) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .watch() - .ignoreCacheMisses() + .cacheAndNetwork() .map { - it.dataAssertNoErrors.partnerGroups.map { partnerGroup -> - PartnerGroup( + it.map { + it.partnerGroups.map { partnerGroup -> + PartnerGroup( title = partnerGroup.title, partners = partnerGroup.partners.map { partner -> Partner( - logoUrl = partner.logoUrlLight, - name = partner.name, - url = partner.url, - logoUrlDark = partner.logoUrlDark + logoUrl = partner.logoUrlLight, + name = partner.name, + url = partner.url, + logoUrlDark = partner.logoUrlDark ) } - ) + ) + } } } - .toResultFlow() } } diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt index 77fb60a8..334faac3 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/RoomsGraphQLRepository.kt @@ -1,9 +1,6 @@ package fr.androidmakers.store.graphql import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.cache.normalized.FetchPolicy -import com.apollographql.apollo3.cache.normalized.fetchPolicy -import com.apollographql.apollo3.cache.normalized.watch import fr.androidmakers.domain.model.Room import fr.androidmakers.domain.repo.RoomsRepository import kotlinx.coroutines.flow.Flow diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt index 9f54f316..6c3e12d9 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/SpeakersGraphQLRepository.kt @@ -1,9 +1,6 @@ package fr.androidmakers.store.graphql import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.cache.normalized.FetchPolicy -import com.apollographql.apollo3.cache.normalized.fetchPolicy -import com.apollographql.apollo3.cache.normalized.watch import com.apollographql.apollo3.exception.DefaultApolloException import fr.androidmakers.domain.model.Speaker import fr.androidmakers.domain.repo.SpeakersRepository diff --git a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/VenueGraphQLRepository.kt b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/VenueGraphQLRepository.kt index fc705207..a600c387 100644 --- a/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/VenueGraphQLRepository.kt +++ b/shared/data/src/commonMain/kotlin/fr/androidmakers/store/graphql/VenueGraphQLRepository.kt @@ -1,9 +1,6 @@ package fr.androidmakers.store.graphql import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.cache.normalized.FetchPolicy -import com.apollographql.apollo3.cache.normalized.fetchPolicy -import com.apollographql.apollo3.cache.normalized.watch import fr.androidmakers.domain.model.Venue import fr.androidmakers.domain.repo.VenueRepository import kotlinx.coroutines.flow.Flow @@ -12,12 +9,7 @@ import kotlinx.coroutines.flow.map class VenueGraphQLRepository(private val apolloClient: ApolloClient): VenueRepository { override fun getVenue(id: String): Flow<Result<Venue>> { return apolloClient.query(GetVenueQuery(id)) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .watch() - .ignoreCacheMisses() - .map { - it.dataAssertNoErrors.venue.toVenue() - } - .toResultFlow() + .cacheAndNetwork() + .map { it.map { it.venue.toVenue() } } } } From 35608655a84e1a87931bd7381bf057862b2ceda3 Mon Sep 17 00:00:00 2001 From: Martin Bonnin <martin@mbonnin.net> Date: Wed, 17 Apr 2024 09:26:21 +0200 Subject: [PATCH 3/3] merge bookmarks on signin --- .../src/main/java/fr/paug/androidmakers/MainActivity.kt | 9 ++++++++- .../androidmakers/ui/common/navigation/UserViewModel.kt | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt b/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt index ce0ac2f4..7cfe2fa8 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/MainActivity.kt @@ -130,7 +130,14 @@ class MainActivity : AppCompatActivity() { val result = auth.signInWithCredential(firebaseCredential) // Sign in success, update UI with the signed-in user's information lifecycleScope.launch { - UserData().userRepository.setUser(result.user) + UserData().apply { + userRepository.setUser(result.user) + val uid = result.user?.uid + if (uid != null) { + syncBookmarksUseCase(uid) + } + } + println("user id=${result.user?.uid}") println("idToken=${result.user?.getIdToken(true)}") } diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt index 90d875f3..03561fd1 100644 --- a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/navigation/UserViewModel.kt @@ -1,11 +1,13 @@ package com.androidmakers.ui.common.navigation +import fr.androidmakers.domain.interactor.SyncBookmarksUseCase import fr.androidmakers.domain.repo.UserRepository import org.koin.core.component.KoinComponent import org.koin.core.component.inject class UserData: KoinComponent { val userRepository: UserRepository by inject() + val syncBookmarksUseCase: SyncBookmarksUseCase by inject() }