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

Make the app work offline #287

Merged
merged 3 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)}")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
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 {
// Ignore cache misses
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!!))
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,11 +15,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() } } }
}
}
Original file line number Diff line number Diff line change
@@ -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() } } }
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
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
import kotlinx.coroutines.flow.Flow
Expand All @@ -13,24 +11,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()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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() } }
}
}
Original file line number Diff line number Diff line change
@@ -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()
}


Loading