diff --git a/app/src/main/java/com/cornellappdev/score/components/SportCard.kt b/app/src/main/java/com/cornellappdev/score/components/SportCard.kt index 469ce34..d0edb7b 100644 --- a/app/src/main/java/com/cornellappdev/score/components/SportCard.kt +++ b/app/src/main/java/com/cornellappdev/score/components/SportCard.kt @@ -1,6 +1,5 @@ package com.cornellappdev.score.components -import android.icu.text.SimpleDateFormat import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -9,7 +8,14 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -40,8 +46,6 @@ import com.cornellappdev.score.theme.Style.labelsNormal import com.cornellappdev.score.theme.Style.teamName import com.cornellappdev.score.theme.Style.universityText import com.cornellappdev.score.theme.saturatedGreen -import java.util.Date -import java.util.Locale @Composable fun SportCard( @@ -101,7 +105,9 @@ fun SportCard( ) { AsyncImage( model = teamLogo, - modifier = Modifier.height(20.dp).padding(start = 4.dp, end = 4.dp), + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), contentDescription = "" ) diff --git a/app/src/main/java/com/cornellappdev/score/components/UpcomingGamesCarousel.kt b/app/src/main/java/com/cornellappdev/score/components/UpcomingGamesCarousel.kt index c160ed1..0825066 100644 --- a/app/src/main/java/com/cornellappdev/score/components/UpcomingGamesCarousel.kt +++ b/app/src/main/java/com/cornellappdev/score/components/UpcomingGamesCarousel.kt @@ -87,8 +87,8 @@ fun UpcomingGamesCarousel(games: List) { location = game.location, modifier = Modifier, headerModifier = Modifier, - gradientColor1 = CornellRed, //TODO: is it okay if this is hardcoded - gradientColor2 = Color(game.teamColor) + gradientColor1 = CornellRed, + gradientColor2 = game.teamColor ) } diff --git a/app/src/main/java/com/cornellappdev/score/model/Game.kt b/app/src/main/java/com/cornellappdev/score/model/Game.kt index 7a71627..bfdbd78 100644 --- a/app/src/main/java/com/cornellappdev/score/model/Game.kt +++ b/app/src/main/java/com/cornellappdev/score/model/Game.kt @@ -1,6 +1,9 @@ package com.cornellappdev.score.model -import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.Color +import com.cornellappdev.score.R +import com.cornellappdev.score.util.outputFormatter +import com.cornellappdev.score.util.parseDateOrNull import java.time.LocalDate // TODO Refactor to make easier to filter... actual gender, etc. @@ -8,7 +11,7 @@ import java.time.LocalDate data class Game( val teamName: String, val teamLogo: String, - val teamColor: String, + val teamColor: Color, val gender: String, val sport: String, val date: String, @@ -19,7 +22,7 @@ data class Game( data class GameCardData( val teamLogo: String, val team: String, - val teamColor: Int, + val teamColor: Color, val date: LocalDate?, val dateString: String, val isLive: Boolean, @@ -98,4 +101,22 @@ enum class GameStatus { NOT_STARTED, IN_PROGRESS, COMPLETED +} + +fun Game.toGameCardData(): GameCardData{ + return GameCardData( + teamLogo = teamLogo, + team = teamName, + teamColor = teamColor, + date = parseDateOrNull(date), + dateString = parseDateOrNull(date)?.format(outputFormatter) + ?: date, + isLive = (LocalDate.now() == parseDateOrNull(date)), + location = city, + gender = gender, + genderIcon = if (gender == "Mens") R.drawable.ic_gender_men else R.drawable.ic_gender_women, + sport = sport, + sportIcon = Sport.fromDisplayName(sport)?.emptyIcon + ?: R.drawable.ic_empty_placeholder + ) } \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/model/Result.kt b/app/src/main/java/com/cornellappdev/score/model/Result.kt new file mode 100644 index 0000000..71c065a --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/model/Result.kt @@ -0,0 +1,24 @@ +package com.cornellappdev.score.model + +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.exception.NoDataException + +/** + * Maps an ApolloResponse to a generic Kotlin result. It either provides the data with no errors, or + * a failure response containing the error message in the throwable. + */ +fun ApolloResponse.toResult(): Result { + if (hasErrors() || exception != null) { + return Result.failure( + exception?.cause ?: RuntimeException( + errors?.firstOrNull()?.message ?: "Unknown error occurred" + ) + ) + } + return try { + Result.success(dataOrThrow()) + } catch (e: NoDataException) { + Result.failure(e) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt index 073c0e4..f15f496 100644 --- a/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt +++ b/app/src/main/java/com/cornellappdev/score/model/ScoreRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton +import com.cornellappdev.score.util.parseColor /** * This is a singleton responsible for fetching and caching all data for Score. @@ -19,7 +20,6 @@ import javax.inject.Singleton @Singleton class ScoreRepository @Inject constructor( private val apolloClient: ApolloClient, - // TODO use this to launch queries private val appScope: CoroutineScope, ) { private val _upcomingGamesFlow = @@ -34,28 +34,33 @@ class ScoreRepository @Inject constructor( fun fetchGames() = appScope.launch { _upcomingGamesFlow.value = ApiResponse.Loading try { - val response = (apolloClient.query(GamesQuery()).execute()) - val games = response.data?.games ?: emptyList() - Log.d("ScoreRepository", "response fetched successfully") + val result = (apolloClient.query(GamesQuery()).execute()).toResult() - val list: List = games.mapNotNull { game -> - game?.team?.image?.let { - Game( - teamLogo = it,//game.team.image, - teamName = game.team.name, - teamColor = game.team.color, - gender = game.gender, - sport = game.sport, - date = game.date, - city = game.city - ) - } + if (result.isSuccess) { + val games = result.getOrNull() + + val upcomingGameslist: List = + games?.games?.mapNotNull { game -> + game?.team?.image?.let { + Game( + teamLogo = it, + teamName = game.team.name, + teamColor = parseColor(game.team.color).copy(alpha = 0.4f*255), + gender = game.gender, + sport = game.sport, + date = game.date, + city = game.city + ) + } + } ?: emptyList() + _upcomingGamesFlow.value = ApiResponse.Success(upcomingGameslist) + } else { + _upcomingGamesFlow.value = ApiResponse.Error } - Log.d("ScoreRepository", "#games: ${list.size}") - _upcomingGamesFlow.value = ApiResponse.Success(list.toList()) + } catch (e: Exception) { Log.e("ScoreRepository", "Error fetching posts: ", e) _upcomingGamesFlow.value = ApiResponse.Error } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt b/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt index e6db353..440ad57 100644 --- a/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt +++ b/app/src/main/java/com/cornellappdev/score/nav/root/RootNavigation.kt @@ -1,7 +1,5 @@ package com.cornellappdev.score.nav.root -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.hilt.navigation.compose.hiltViewModel @@ -11,7 +9,6 @@ import androidx.navigation.compose.rememberNavController import com.cornellappdev.score.screen.HomeScreen import kotlinx.serialization.Serializable -@RequiresApi(Build.VERSION_CODES.O) @Composable fun RootNavigation( rootNavigationViewModel: RootNavigationViewModel = hiltViewModel(), diff --git a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt index e2f1d1f..c8c6478 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/HomeScreen.kt @@ -1,21 +1,23 @@ package com.cornellappdev.score.screen -import android.os.Build -import androidx.annotation.RequiresApi +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -23,71 +25,118 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.cornellappdev.score.components.SportCard import com.cornellappdev.score.components.SportSelectorHeader import com.cornellappdev.score.components.UpcomingGamesCarousel -import com.cornellappdev.score.theme.Style.heading1 +import com.cornellappdev.score.model.ApiResponse +import com.cornellappdev.score.model.GenderDivision +import com.cornellappdev.score.model.SportSelection import com.cornellappdev.score.theme.Style.title +import com.cornellappdev.score.util.gameList +import com.cornellappdev.score.util.sportSelectionList +import com.cornellappdev.score.viewmodel.HomeUiState import com.cornellappdev.score.viewmodel.HomeViewModel -@RequiresApi(Build.VERSION_CODES.O)//TODO - change the manifest or leave this? @Composable fun HomeScreen( homeViewModel: HomeViewModel = hiltViewModel() ) { - val uiState by homeViewModel.uiStateFlow.collectAsState() + val uiState = homeViewModel.collectUiStateValue() Column( verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top), modifier = Modifier.statusBarsPadding() - ) - { - //TODO: check - displaying the earliest three games - UpcomingGamesCarousel( - uiState.upcomingGameList.subList( - 0, - minOf(3, uiState.upcomingGameList.size) - ) - ) - Column { - Text( - text = "Game Schedule", - style = heading1, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp) - ) - Spacer(modifier = Modifier.height(8.dp)) - SportSelectorHeader( - sports = uiState.selectionList, - selectedGender = uiState.selectedGender, - selectedSport = uiState.sportSelect, - onGenderSelected = { - homeViewModel.onGenderSelected(it) - }, - onSportSelected = { - homeViewModel.onSportSelected(it) + ) { + when (uiState.loadedState) { + is ApiResponse.Loading -> { + //TODO: Add loading screen + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } - ) - LazyColumn(modifier = Modifier.padding(horizontal = 24.dp)) { - items(uiState.filteredGames.size) { page -> - val game = uiState.filteredGames[page] - SportCard( - teamLogo = game.teamLogo,//painterResource(game.teamLogo), - team = game.team, - date = game.dateString, - isLive = game.isLive, - genderIcon = painterResource(game.genderIcon), - sportIcon = painterResource(game.sportIcon), - location = game.location, - topCornerRound = true + } + + is ApiResponse.Error -> { + //TODO: Add Error screen + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Failed to load games. Please try again.", ) } } + + is ApiResponse.Success -> { + HomeContent( + uiState = uiState, + onGenderSelected = { homeViewModel.onGenderSelected(it) }, + onSportSelected = { homeViewModel.onSportSelected(it) } + ) + } + } + } +} + +@Composable +private fun HomeContent( + uiState: HomeUiState, + onGenderSelected: (GenderDivision) -> Unit, + onSportSelected: (SportSelection) -> Unit +) { + UpcomingGamesCarousel(uiState.upcomingGames) + Column { + Text( + text = "Game Schedule", + style = title, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 8.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + SportSelectorHeader( + sports = uiState.selectionList, + selectedGender = uiState.selectedGender, + selectedSport = uiState.sportSelect, + onGenderSelected = onGenderSelected, + onSportSelected = onSportSelected + ) + LazyColumn(modifier = Modifier.padding(horizontal = 24.dp)) { + items(uiState.filteredGames) { + val game = it + SportCard( + teamLogo = game.teamLogo, + team = game.team, + date = game.dateString, + isLive = game.isLive, + genderIcon = painterResource(game.genderIcon), + sportIcon = painterResource(game.sportIcon), + location = game.location, + topCornerRound = true + ) + } } } } -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) +@Preview @Composable private fun HomeScreenPreview() { - HomeScreen() + Column( + modifier = Modifier + .fillMaxSize() + .background(color = Color.White) + ) { + HomeContent( + HomeUiState( + selectedGender = GenderDivision.ALL, + sportSelect = SportSelection.All, + selectionList = sportSelectionList, + loadedState = ApiResponse.Success(gameList) + ), + onGenderSelected = {}, + onSportSelected = {} + ) + } } + diff --git a/app/src/main/java/com/cornellappdev/score/util/ColorUtil.kt b/app/src/main/java/com/cornellappdev/score/util/ColorUtil.kt new file mode 100644 index 0000000..89e66bb --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/util/ColorUtil.kt @@ -0,0 +1,11 @@ +package com.cornellappdev.score.util + +import androidx.compose.ui.graphics.Color + +/** + * Converts a hexcode String into Color object + */ +fun parseColor(color: String): Color { + val colorInt = Integer.parseInt(color.removePrefix("#"), 16) + return Color(colorInt) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt b/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt new file mode 100644 index 0000000..0fe3def --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/util/DateUtil.kt @@ -0,0 +1,25 @@ +package com.cornellappdev.score.util + +import java.time.LocalDate +import java.time.format.DateTimeFormatter; + +/** + * Converts date of form String "month-abbr day (day-of-week)" (for example, "Apr 29 (Tue)") to a LocalDate object + * Returns null when parsing [strDate] fails + */ +fun parseDateOrNull(strDate: String): LocalDate? { + val subDate = strDate.substringBefore(" (") + val formatter = DateTimeFormatter.ofPattern("MMM d yyyy") + + return try { + LocalDate.parse("$subDate ${LocalDate.now().year}", formatter) //assumes current year + } catch (e: Exception) { + null + } +} + +/** + * DateTimeFormatter of pattern "M/d/yyyy" + */ +val outputFormatter = DateTimeFormatter.ofPattern("M/d/yyyy") + diff --git a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt index cbf7547..ac4c45a 100644 --- a/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt +++ b/app/src/main/java/com/cornellappdev/score/util/TestingConstants.kt @@ -1,5 +1,6 @@ package com.cornellappdev.score.util +import androidx.compose.ui.graphics.Color import com.cornellappdev.score.R import com.cornellappdev.score.model.GameCardData import com.cornellappdev.score.model.GameData @@ -13,7 +14,7 @@ import java.time.LocalDate val PENN_GAME = GameCardData( teamLogo = "https://cornellbigred.com/images/logos/penn_200x200.png?width=80&height=80&mode=max", team = "Penn", - teamColor = 0x66B31B1B, + teamColor = Color(0x66B31B1B), date = LocalDate.now(), dateString = "3/1/25", isLive = false, @@ -27,7 +28,7 @@ val PENN_GAME = GameCardData( val PRINCETON_GAME = GameCardData( teamLogo = "https://cornellbigred.com/images/logos/Princeton_Tigers.png?width=80&height=80&mode=max", team = "Princeton", - teamColor = 0x66FF6000, + teamColor = Color(0x66FF6000), date = LocalDate.now(), dateString = "3/1/25", isLive = false, diff --git a/app/src/main/java/com/cornellappdev/score/viewmodel/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/score/viewmodel/HomeViewModel.kt index 5b9c5c7..2121b70 100644 --- a/app/src/main/java/com/cornellappdev/score/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/score/viewmodel/HomeViewModel.kt @@ -1,22 +1,14 @@ package com.cornellappdev.score.viewmodel -import android.os.Build -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.lifecycle.viewModelScope -import com.cornellappdev.score.R import com.cornellappdev.score.model.ApiResponse -import com.cornellappdev.score.model.Game import com.cornellappdev.score.model.GameCardData import com.cornellappdev.score.model.GenderDivision import com.cornellappdev.score.model.ScoreRepository import com.cornellappdev.score.model.Sport import com.cornellappdev.score.model.SportSelection -import com.cornellappdev.score.nav.root.RootNavigationRepository +import com.cornellappdev.score.model.map +import com.cornellappdev.score.model.toGameCardData import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject @@ -24,28 +16,50 @@ data class HomeUiState( val selectedGender: GenderDivision, val sportSelect: SportSelection, val selectionList: List, - val upcomingGameList: List, - // TODO Add remaining dynamic data for UI + val loadedState: ApiResponse> ) { + //TODO: refactor filters to use flows - not best practice to expose original games list to the view val filteredGames: List - get() = upcomingGameList.filter { game -> - (selectedGender == GenderDivision.ALL || game.gender == selectedGender.displayName) - && (sportSelect is SportSelection.All || (sportSelect is SportSelection.SportSelect && game.sport == sportSelect.sport.displayName)) + get() = when (loadedState) { + is ApiResponse.Success -> loadedState.data.filter { game -> + (selectedGender == GenderDivision.ALL || game.gender == selectedGender.displayName) && + (sportSelect is SportSelection.All || (sportSelect is SportSelection.SportSelect && game.sport == sportSelect.sport.displayName)) + } + + ApiResponse.Loading -> emptyList() + ApiResponse.Error -> emptyList() } + val upcomingGames: List = filteredGames.take(3) } @HiltViewModel class HomeViewModel @Inject constructor( - private val rootNavigationRepository: RootNavigationRepository, private val scoreRepository: ScoreRepository ) : BaseViewModel( - initialUiState = HomeUiState( + HomeUiState( selectedGender = GenderDivision.ALL, sportSelect = SportSelection.All, selectionList = Sport.getSportSelectionList(), - upcomingGameList = emptyList() + loadedState = ApiResponse.Loading ) ) { + init { + scoreRepository.fetchGames() + asyncCollect(scoreRepository.upcomingGamesFlow) { response -> + applyMutation { + copy( + loadedState = response.map { list -> + list.map { game -> + game.toGameCardData() + }.filter { game -> + game.date?.isAfter(LocalDate.now().minusDays(1)) ?: false + }.sortedBy { it.date } + } + ) + } + } + } + fun onGenderSelected(gender: GenderDivision) { applyMutation { copy( @@ -61,106 +75,4 @@ class HomeViewModel @Inject constructor( ) } } - - private fun updateGameList(response: ApiResponse>) { - val games: List = when (response) { - is ApiResponse.Success -> response.data - ApiResponse.Error -> emptyList() - ApiResponse.Loading -> emptyList() - } - - val gameCards = games.filter { game -> - val currentDate = LocalDate.now() - val tomorrowDate = LocalDate.now().plusDays(1) - val formattedDate = formatDate(game.date) - formattedDate == currentDate || formattedDate == tomorrowDate - }.map { game -> - GameCardData( - teamLogo = game.teamLogo, - team = game.teamName, - teamColor = formatColor(game.teamColor), - date = formatDate(game.date), - dateString = dateToString(formatDate(game.date)), - isLive = (LocalDate.now() == formatDate(game.date)), - location = game.city, - gender = game.gender, - genderIcon = if (game.gender == "Mens") R.drawable.ic_gender_men else R.drawable.ic_gender_women, - sport = game.sport, - sportIcon = Sport.fromDisplayName(game.sport)?.emptyIcon - ?: R.drawable.ic_empty_placeholder - ) - }.sortedBy { it.date } - - applyMutation { - copy(upcomingGameList = gameCards) - } - } - -// fun onRefresh() { -// viewModelScope.launch { -// val response = scoreRepository.fetchGames() -// updateGameList(response) -// } -// } - - //Converts date from String "month day" to a LocalDate object - private fun formatDate(strDate: String): LocalDate? { - val monthMap = mapOf( - "Jan" to 1, - "Feb" to 2, - "Mar" to 3, - "Apr" to 4, - "May" to 5, - "Jun" to 6, - "Jul" to 7, - "Aug" to 8, - "Sep" to 9, - "Oct" to 10, - "Nov" to 11, - "Dec" to 12 - ) - - val parts = strDate.split(" ") - if (parts.size < 2) return null - - val month = monthMap[parts[0]] - if (month == null) { - return null - } - val day = parts[1].toIntOrNull() ?: return null - - val currentYear = LocalDate.now().year - //Log.d("HomeViewModel", "formatDate: ${LocalDate.of(currentYear, month, day)}") - return LocalDate.of(currentYear, month, day) - } - - /** - * Converts from format "#xxxxxx" to a valid hex, with alpha = 40. Ready to be passed into Color() - */ - - private fun formatColor(color: String): Int { - val alpha = (40 * 255 / 100)// Convert percent to hex (0-255) - val colorInt = Integer.parseInt(color.removePrefix("#"), 16) - return (alpha shl 24) or colorInt - } - - private fun dateToString(date: LocalDate?): String { - if (date == null) { - return "--" - } - //Log.d("HomeViewModel", "formatedDate: ${date.month.value}/${date.dayOfMonth}/${date.year}") - return "${date.month.value}/${date.dayOfMonth}/${date.year}" - } - - private fun observeUpcomingGames() = scoreRepository.upcomingGamesFlow.onEach { response -> - //Log.d("HomeViewModel", "Response: $response") - updateGameList(response) - }.launchIn(viewModelScope) - - init { - observeUpcomingGames() - viewModelScope.launch{ - scoreRepository.fetchGames() - } - } -} \ No newline at end of file +}