Skip to content

Commit 6fef02b

Browse files
committed
pagination
1 parent 3cc61b7 commit 6fef02b

4 files changed

Lines changed: 182 additions & 17 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
plugins {
22
alias(libs.plugins.androidApplication)
33
alias(libs.plugins.jetbrainsKotlinAndroid) version "1.9.10"
4-
alias(libs.plugins.apollo)
4+
id("com.apollographql.apollo") version "4.0.0"
55
id("kotlin-kapt")
66
id("com.google.dagger.hilt.android")
77
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" // this version matches your Kotlin version
@@ -88,9 +88,8 @@ dependencies {
8888
debugImplementation("androidx.compose.ui:ui-tooling")
8989
androidTestImplementation(libs.androidx.junit)
9090
androidTestImplementation(libs.androidx.espresso.core)
91-
implementation(libs.apollo.runtime)
9291
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
93-
implementation(libs.apollo.runtime)
92+
implementation("com.apollographql.apollo:apollo-runtime:4.0.0")
9493
implementation("io.coil-kt.coil3:coil-compose:3.1.0")
9594
implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0")
9695
lintChecks(libs.compose.lint.checks)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
query PagedGames($limit: Int!, $offset: Int!) {
2+
games(limit: $limit, offset: $offset) {
3+
id
4+
city
5+
date
6+
gender
7+
location
8+
opponentId
9+
result
10+
sport
11+
state
12+
time
13+
scoreBreakdown
14+
utcDate
15+
team {
16+
id
17+
color
18+
image
19+
name
20+
}
21+
boxScore {
22+
team
23+
period
24+
time
25+
description
26+
scorer
27+
assist
28+
scoreBy
29+
corScore
30+
oppScore
31+
}
32+
}
33+
}

app/src/main/graphql/schema.graphqls

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
type Query {
2+
articles(sportsType: String): [ArticleType]
3+
24
youtubeVideos: [YoutubeVideoType]
35

46
youtubeVideo(id: String!): YoutubeVideoType
57

6-
games: [GameType]
8+
games("Number of games to return" limit: Int = 100, "Number of games to skip" offset: Int = 0): [GameType]
79

810
game(id: String!): GameType
911

10-
gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!): GameType
12+
gameByData(city: String!, date: String!, gender: String!, location: String, opponentId: String!, sport: String!, state: String!, time: String!, ticketLink: String): GameType
1113

1214
gamesBySport(sport: String!): [GameType]
1315

@@ -22,6 +24,30 @@ type Query {
2224
teamByName(name: String!): TeamType
2325
}
2426

27+
"""
28+
A GraphQL type representing a news article.
29+
30+
Attributes:
31+
- title: The title of the article
32+
- image: The filename of the article's main image
33+
- sports_type: The specific sport category
34+
- published_at: The publication date
35+
- url: The URL to the full article
36+
"""
37+
type ArticleType {
38+
id: String
39+
40+
title: String!
41+
42+
image: String
43+
44+
sportsType: String!
45+
46+
publishedAt: String!
47+
48+
url: String!
49+
}
50+
2551
"""
2652
A GraphQL type representing a YouTube video.
2753
@@ -42,6 +68,8 @@ type YoutubeVideoType {
4268

4369
thumbnail: String!
4470

71+
b64Thumbnail: String!
72+
4573
url: String!
4674

4775
publishedAt: String!
@@ -63,6 +91,7 @@ Attributes:
6391
- `time`: The time of the game. (optional)
6492
- `box_score`: The box score of the game.
6593
- `score_breakdown`: The score breakdown of the game.
94+
- `ticket_link`: The ticket link of the game. (optional)
6695
"""
6796
type GameType {
6897
id: String
@@ -90,6 +119,10 @@ type GameType {
90119
scoreBreakdown: [[String]]
91120

92121
team: TeamType
122+
123+
utcDate: String
124+
125+
ticketLink: String
93126
}
94127

95128
"""
@@ -133,6 +166,7 @@ Attributes:
133166
- `id`: The ID of the team (optional).
134167
- `color`: The color of the team.
135168
- `image`: The image of the team (optional).
169+
- `b64_image`: The base64 encoded image of the team (optional).
136170
- `name`: The name of the team.
137171
"""
138172
type TeamType {
@@ -142,24 +176,31 @@ type TeamType {
142176

143177
image: String
144178

179+
b64Image: String
180+
145181
name: String!
146182
}
147183

148184
type Mutation {
149185
"""
150186
Creates a new game.
151187
"""
152-
createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, time: String!): CreateGame
188+
createGame(boxScore: String, city: String!, date: String!, gender: String!, location: String, opponentId: String!, result: String, scoreBreakdown: String, sport: String!, state: String!, ticketLink: String, time: String!, utcDate: String): CreateGame
153189

154190
"""
155191
Creates a new team.
156192
"""
157-
createTeam(color: String!, image: String, name: String!): CreateTeam
193+
createTeam(b64Image: String, color: String!, image: String, name: String!): CreateTeam
158194

159195
"""
160196
Creates a new youtube video.
161197
"""
162-
createYoutubeVideo(description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo
198+
createYoutubeVideo(b64Thumbnail: String!, description: String!, id: String!, publishedAt: String!, thumbnail: String!, title: String!, url: String!): CreateYoutubeVideo
199+
200+
"""
201+
Creates a new article.
202+
"""
203+
createArticle(image: String, publishedAt: String!, slug: String!, sportsType: String!, title: String!, url: String!): CreateArticle
163204
}
164205

165206
type CreateGame {
@@ -174,6 +215,10 @@ type CreateYoutubeVideo {
174215
youtubeVideo: YoutubeVideoType
175216
}
176217

218+
type CreateArticle {
219+
article: ArticleType
220+
}
221+
177222
"""
178223
A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation and subscription operations.
179224
"""

app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package com.cornellappdev.score.model
22

33
import android.util.Log
44
import com.apollographql.apollo.ApolloClient
5+
import com.apollographql.apollo.exception.ApolloException
56
import com.cornellappdev.score.util.isValidSport
67
import com.cornellappdev.score.util.parseColor
78
import com.example.score.GameByIdQuery
89
import com.example.score.GamesQuery
10+
import com.example.score.PagedGamesQuery
911
import kotlinx.coroutines.CoroutineScope
1012
import kotlinx.coroutines.flow.MutableStateFlow
1113
import kotlinx.coroutines.flow.asStateFlow
@@ -36,11 +38,17 @@ class ScoreRepository @Inject constructor(
3638
MutableStateFlow<ApiResponse<GameDetailsGame>>(ApiResponse.Loading)
3739
val currentGamesFlow = _currentGameFlow.asStateFlow()
3840

41+
companion object {
42+
private const val PAGE_LIMIT = 100
43+
private const val MAX_RETRIES = 3
44+
private const val PAGE_TIMEOUT_MILLIS = 3000L
45+
}
46+
3947
/**
4048
* Asynchronously fetches the list of games from the API. Once finished, will send down
4149
* `upcomingGamesFlow` to be observed.
4250
*/
43-
fun fetchGames() = appScope.launch {
51+
fun fetchGamesPrev() = appScope.launch {
4452
_upcomingGamesFlow.value = ApiResponse.Loading
4553
try {
4654
val result =
@@ -92,33 +100,113 @@ class ScoreRepository @Inject constructor(
92100
}
93101
}
94102

103+
fun fetchGames() = appScope.launch {
104+
_upcomingGamesFlow.value = ApiResponse.Loading
105+
val allGames = mutableListOf<Game>()
106+
var offset = 0
107+
var retries = 0
108+
109+
try {
110+
while (true) {
111+
val pageResult: List<PagedGamesQuery.Game?>? = try {
112+
withTimeout(PAGE_TIMEOUT_MILLIS) {
113+
apolloClient.query(PagedGamesQuery(limit = PAGE_LIMIT, offset = offset))
114+
.execute()
115+
.data
116+
?.games
117+
}
118+
} catch (e: Exception) {
119+
null
120+
}
121+
122+
if (pageResult == null) {
123+
if (retries < MAX_RETRIES) {
124+
retries++
125+
continue
126+
} else {
127+
break
128+
}
129+
}
130+
131+
if (pageResult.isEmpty()) {
132+
break
133+
}
134+
135+
retries = 0
136+
137+
val pageGames: List<Game> = pageResult
138+
.filterNotNull()
139+
.filter { gql -> isValidSport(gql.sport ?: "") }
140+
.mapNotNull { gql ->
141+
val scores = gql.result?.split(",")?.getOrNull(1)?.split("-")
142+
val cornellScore = scores?.getOrNull(0)?.toNumberOrNull()
143+
val otherScore = scores?.getOrNull(1)?.toNumberOrNull()
144+
gql.team?.image?.let { imageUrl ->
145+
Game(
146+
id = gql.id ?: "",
147+
teamLogo = imageUrl,
148+
teamName = gql.team.name,
149+
teamColor = parseColor(gql.team.color).copy(alpha = 0.4f * 255),
150+
gender = if (gql.gender == "Mens") "Men's" else "Women's",
151+
sport = gql.sport,
152+
date = gql.date,
153+
city = gql.city,
154+
cornellScore = cornellScore,
155+
otherScore = otherScore
156+
)
157+
}
158+
}
159+
160+
allGames.addAll(pageGames)
161+
162+
if (pageResult.size < PAGE_LIMIT) break
163+
offset += PAGE_LIMIT
164+
}
165+
166+
_upcomingGamesFlow.value =
167+
if (allGames.isNotEmpty()) ApiResponse.Success(allGames)
168+
else ApiResponse.Error
169+
170+
} catch (e: Exception) {
171+
Log.e("ScoreRepository", "Error fetching upcoming games", e)
172+
_upcomingGamesFlow.value = ApiResponse.Error
173+
}
174+
}
175+
95176
/**
96177
* Asynchronously fetches game details for a particular game. Once finished, will update
97178
* `currentGamesFlow` to be observed.
98179
*/
99180
fun getGameById(id: String) = appScope.launch {
100181
_currentGameFlow.value = ApiResponse.Loading
101182
try {
102-
val result =
183+
val response =
103184
withTimeout(TIMEOUT_TIME_MILLIS) {
104-
apolloClient.query(GameByIdQuery(id)).execute().toResult()
185+
apolloClient.query(GameByIdQuery(id)).execute()
105186
}
106187

188+
if (response.hasErrors()) {
189+
Log.e("ScoreRepository", "Error fetching game with id: $id: ${response.errors}")
190+
_currentGameFlow.value = ApiResponse.Error
191+
return@launch
192+
}
107193

108-
result.getOrNull()?.game?.let {
194+
response.data?.game?.let {
109195
_currentGameFlow.value = ApiResponse.Success(it.toGameDetails())
110196
} ?: _currentGameFlow.update { ApiResponse.Error }
197+
198+
} catch (e: ApolloException) {
199+
Log.e("ScoreRepository", "Error fetching game with id: $id: ", e)
200+
_currentGameFlow.value = ApiResponse.Error
111201
} catch (e: Exception) {
112-
Log.e("ScoreRepository", "Error fetching game with id: ${id}: ", e)
202+
Log.e("ScoreRepository", "A timeout or other error occurred for game id: $id", e)
113203
_currentGameFlow.value = ApiResponse.Error
114204
}
115205
}
116206

117207
}
118208

119209
fun String.toNumberOrNull(): Number? {
120-
return when {
121-
this.contains(".") -> this.toFloatOrNull() // Try converting to Float if there's a decimal
122-
else -> this.toIntOrNull() // Otherwise, try converting to Int
123-
}
210+
return this.trim().toFloatOrNull() ?: this.trim().toIntOrNull()
124211
}
212+

0 commit comments

Comments
 (0)