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

Paginate sessions #313

Closed
wants to merge 1 commit into from
Closed
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 @@ -39,7 +39,7 @@ class FakeAndroidMakersStore : RoomsRepository, VenueRepository, SpeakersReposit
TODO("Not yet implemented")
}

override fun getSessions(): Flow<Result<List<Session>>> {
override fun watchSessions(): Flow<Result<List<Session>>> {
TODO("Not yet implemented")
}

Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ androidx-credentials-playServicesAuth = { group = "androidx.credentials", name =
androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
apollo-adapters = { module = "com.apollographql.apollo3:apollo-adapters", version.ref = "apollo" }
apollo-normalized-cache = { module = "com.apollographql.apollo3:apollo-normalized-cache", version.ref = "apollo" }
apollo-normalized-cache-sqlite = { module = "com.apollographql.apollo3:apollo-normalized-cache-sqlite", version.ref = "apollo" }
apollo-normalized-cache = { module = "com.apollographql.apollo3:apollo-normalized-cache-incubating", version.ref = "apollo" }
apollo-normalized-cache-sqlite = { module = "com.apollographql.apollo3:apollo-normalized-cache-sqlite-incubating", version.ref = "apollo" }
apollo-runtime = { module = "com.apollographql.apollo3:apollo-runtime", version.ref = "apollo" }
atomicfu = "org.jetbrains.kotlinx:atomicfu:0.23.2"
qdsfdhvh-imageloader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "image-loader" }
Expand Down
14 changes: 12 additions & 2 deletions shared/data/src/commonMain/graphql/extra.graphqls
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# noinspection GraphQLUnresolvedReference,GraphQLMissingType
extend schema
@link(
url: "https://specs.apollo.dev/kotlin_labs/v0.3",
import: ["@typePolicy", "@fieldPolicy"]
)

extend type Room @typePolicy(keyFields: "id")

extend type Session @typePolicy(keyFields: "id")
extend type RootQuery @fieldPolicy(forField: "session", keyArgs: "id")

extend type RootQuery
@fieldPolicy(forField: "session", keyArgs: "id")
@fieldPolicy(forField: "sessions", paginationArgs: "first after")
@typePolicy(embeddedFields: "sessions")

extend type Speaker @typePolicy(keyFields: "id")

extend type Venue @typePolicy(keyFields: "name")

extend type SessionConnection @typePolicy(embeddedFields: "pageInfo")
7 changes: 5 additions & 2 deletions shared/data/src/commonMain/graphql/operations.graphql
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
query GetSessions {
sessions(first: 1000) {
query GetSessions($after: String) {
sessions(first: 10, after: $after) {
nodes {
...SessionDetails
}
pageInfo {
endCursor
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package fr.androidmakers.store.graphql

import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.annotations.ApolloExperimental
import com.apollographql.apollo3.api.http.HttpRequest
import com.apollographql.apollo3.api.http.HttpResponse
import com.apollographql.apollo3.cache.normalized.api.FieldRecordMerger
import com.apollographql.apollo3.cache.normalized.api.FieldRecordMerger.FieldMerger
import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory
import com.apollographql.apollo3.cache.normalized.api.MetadataGenerator
import com.apollographql.apollo3.cache.normalized.api.MetadataGeneratorContext
import com.apollographql.apollo3.cache.normalized.normalizedCache
import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory
import com.apollographql.apollo3.network.http.HttpInterceptor
Expand All @@ -17,25 +22,83 @@ fun ApolloClient(
userRepository: UserRepository,
): ApolloClient {
val memoryCacheFactory = MemoryCacheFactory(20_000_000).chain(sqlNormalizedCacheFactory)
@OptIn(ApolloExperimental::class)
return ApolloClient.Builder()
.serverUrl("https://androidmakers.fr/graphql")
.addHttpInterceptor(object : HttpInterceptor {
override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse {
return chain.proceed(
request.newBuilder()
.apply {
/**
*
*/
val token = getIdToken(userRepository)
if (token != null) {
addHeader("Authorization", "Bearer $token")
}
.serverUrl("https://androidmakers.fr/graphql")
.addHttpInterceptor(object : HttpInterceptor {
override suspend fun intercept(
request: HttpRequest,
chain: HttpInterceptorChain
): HttpResponse {
return chain.proceed(
request.newBuilder()
.apply {
/**
*
*/
val token = getIdToken(userRepository)
if (token != null) {
addHeader("Authorization", "Bearer $token")
}
.build()
)
}
})
.normalizedCache(memoryCacheFactory)
.build()
}
.build()
)
}
})
.normalizedCache(
normalizedCacheFactory = memoryCacheFactory,
metadataGenerator = AndroidMakersMetaDataGenerator,
recordMerger = FieldRecordMerger(AndroidMakersFieldMerger),
)
.build()
}

@OptIn(ApolloExperimental::class)
object AndroidMakersMetaDataGenerator : MetadataGenerator {
@Suppress("UNCHECKED_CAST")
override fun metadataForObject(obj: Any?, context: MetadataGeneratorContext): Map<String, Any?> {
return if (context.field.type.rawType().name == "SessionConnection") {
obj as Map<String, Any?>
val pageInfo = obj["pageInfo"] as? Map<String, Any?>
val endCursor = pageInfo?.get("endCursor") as? String
mapOf(
"endCursor" to endCursor,
"after" to context.argumentValue("after"),
)
} else {
emptyMap()
}
}
}

@OptIn(ApolloExperimental::class)
object AndroidMakersFieldMerger : FieldMerger {
@Suppress("UNCHECKED_CAST")
override fun mergeFields(
existing: FieldRecordMerger.FieldInfo,
incoming: FieldRecordMerger.FieldInfo
): FieldRecordMerger.FieldInfo {
val existingEndCursor = existing.metadata["endCursor"] as? String
val incomingAfterArgument = incoming.metadata["after"] as? String
return if (existingEndCursor == null && incomingAfterArgument == null) {
incoming
} else if (incomingAfterArgument == existingEndCursor) {
val existingValue = existing.value as Map<String, Any?>
val existingNodes = existingValue["nodes"] as? List<*>

val incomingValue = incoming.value as Map<String, Any?>
val incomingNodes = incomingValue["nodes"] as? List<*>

val mergedNodes: List<*> = existingNodes.orEmpty() + incomingNodes.orEmpty()
val mergedValue = (existingValue + incomingValue).toMutableMap()
mergedValue["nodes"] = mergedNodes

FieldRecordMerger.FieldInfo(
value = mergedValue,
metadata = incoming.metadata,
)
} else {
incoming
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package fr.androidmakers.store.graphql

import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.api.Optional
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.Session
import fr.androidmakers.domain.repo.SessionsRepository
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -37,9 +39,22 @@ class SessionsGraphQLRepository(private val apolloClient: ApolloClient) : Sessio
.toResultFlow()
}

override fun getSessions(): Flow<Result<List<Session>>> {
return apolloClient.query(GetSessionsQuery())
.cacheAndNetwork()
.map { it.map { it.sessions.nodes.map { it.sessionDetails.toSession() } } }
override fun watchSessions(): Flow<List<Session>> {
return apolloClient.query(GetSessionsQuery(after = Optional.present(null)))
.fetchPolicy(FetchPolicy.CacheAndNetwork)
.watch()
.map { it.data?.sessions?.nodes?.map { it.sessionDetails.toSession() }.orEmpty() }
}

override suspend fun fetchNextSessionsPage() {
val cacheResponse = apolloClient.query(GetSessionsQuery())
.fetchPolicy(FetchPolicy.CacheOnly)
.execute()
val endCursor = cacheResponse.data?.sessions?.pageInfo?.endCursor
if (endCursor != null) {
apolloClient.query(GetSessionsQuery(Optional.present(endCursor)))
.fetchPolicy(FetchPolicy.NetworkOnly)
.execute()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.androidmakers.di

import fr.androidmakers.domain.interactor.ApplyForAppClinicUseCase
import fr.androidmakers.domain.interactor.FetchNextSessionsPageUseCase
import fr.androidmakers.domain.interactor.GetAfterpartyVenueUseCase
import fr.androidmakers.domain.interactor.GetAgendaUseCase
import fr.androidmakers.domain.interactor.GetConferenceVenueUseCase
Expand All @@ -22,6 +23,7 @@ expect val domainPlatformModule: Module

val domainModule = module {
factory { GetAgendaUseCase(get(), get(), get()) }
factory { FetchNextSessionsPageUseCase(get()) }
factory { GetConferenceVenueUseCase(get()) }
factory { GetAfterpartyVenueUseCase(get()) }
factory { MergeBookmarksUseCase(get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package fr.androidmakers.domain.interactor

import fr.androidmakers.domain.repo.SessionsRepository

class FetchNextSessionsPageUseCase(
private val sessionsRepository: SessionsRepository,
) {
suspend operator fun invoke() {
sessionsRepository.fetchNextSessionsPage()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,11 @@ class GetAgendaUseCase(
) {
operator fun invoke(): Flow<Result<Agenda>> {
return combine(
sessionsRepository.getSessions(),
sessionsRepository.watchSessions(),
roomsRepository.getRooms(),
speakersRepository.getSpeakers(),
) { sessions, rooms, speakers ->

sessions.exceptionOrNull()?.let {
it.printStackTrace()
return@combine Result.failure(it)
}
rooms.exceptionOrNull()?.let {
it.printStackTrace()
return@combine Result.failure(it)
Expand All @@ -34,7 +30,7 @@ class GetAgendaUseCase(

Result.success(
Agenda(
sessions = sessions.getOrThrow().associateBy { it.id },
sessions = sessions.associateBy { it.id },
rooms = rooms.getOrThrow().associateBy { it.id },
speakers = speakers.getOrThrow().associateBy { it.id }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import kotlinx.coroutines.flow.Flow
interface SessionsRepository {
fun getSession(id: String): Flow<Result<Session>>

fun getSessions(): Flow<Result<List<Session>>>
fun watchSessions(): Flow<List<Session>>

suspend fun fetchNextSessionsPage()

fun getBookmarks(userId: String): Flow<Result<Set<String>>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ val viewModelModule = module {
factory { VenueViewModel(get(), get(), get()) }
factory { (speakerId: String) -> SpeakerDetailsViewModel(speakerId, get(), get()) }
factory { AgendaLayoutViewModel(get()) }
factory { AgendaPagerViewModel(get(), get(), get(), get()) }
factory { AgendaPagerViewModel(get(), get(), get(), get(), get()) }
factory { (sessionId: String) -> SessionDetailViewModel(sessionId, get(), get(), get(), get(), get(), get(), get(), get()) }
factory { AboutViewModel(get(), get(), get(), get(), get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ fun AgendaColumn(
onSessionClicked: (UISession) -> Unit,
onSessionBookmarked: (UISession, Boolean) -> Unit,
onApplyForAppClinicClicked: () -> Unit,
onFetchMoreItems: () -> Unit,
) {
val listState = rememberLazyListState()

Expand Down Expand Up @@ -104,6 +105,10 @@ fun AgendaColumn(
}
}
}

item {
onFetchMoreItems()
}
}
}

Expand Down
Loading
Loading