Skip to content

Adding FeedViewModel. #562

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

Merged
merged 5 commits into from
Nov 26, 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
6 changes: 3 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
root = true

[*.{kt,kts}]
max_line_length = 120
max_line_length = 140
indent_size = 4
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 1
ktlint_standard_property-naming = disabled
ktlint_standard_max-line-length = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
1 change: 0 additions & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ dependencies {
implementation(libs.accompanist.systemuicontroller)
implementation(libs.activity.compose)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.koin.android)
implementation(libs.ui.graphics)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@ class FakeTeamRepository : TeamRepository {
var activeTeams: List<Team> = emptyList()
var activeTeamsRequestCount: Int = 0

private val _insertedTeams: MutableList<Team> = mutableListOf()
private val mutableInsertedTeams: MutableList<Team> = mutableListOf()

private val _updatedFavorites: MutableMap<String, Boolean> = mutableMapOf()
private val mutableUpdatedFavorites: MutableMap<String, Boolean> = mutableMapOf()

fun verifyFavoriteStatus(
teamId: String,
expectedIsFavorite: Boolean,
) {
require(_updatedFavorites[teamId] == expectedIsFavorite)
require(mutableUpdatedFavorites[teamId] == expectedIsFavorite)
}

fun verifyFavoriteStatusNotUpdated(
teamId: String,
) {
require(_updatedFavorites[teamId] == null)
require(mutableUpdatedFavorites[teamId] == null)
}

val insertedTeams: List<Team>
get() = _insertedTeams.toList()
get() = mutableInsertedTeams.toList()

override fun getFavoriteTeams(): Flow<List<Team>> {
favoriteTeamsRequestCount++
Expand All @@ -49,14 +49,14 @@ class FakeTeamRepository : TeamRepository {
override suspend fun insertTeams(
teams: List<Team>,
) {
this._insertedTeams.addAll(teams)
this.mutableInsertedTeams.addAll(teams)
}

override suspend fun updateIsFavorite(
teamId: String,
isFavorite: Boolean,
) {
_updatedFavorites[teamId] = isFavorite
mutableUpdatedFavorites[teamId] = isFavorite
}

override fun getTeamById(
Expand Down
5 changes: 2 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ kotlinxDatetime = "0.6.1"
kotlinxSerialization = "1.7.3"
ktlint = "1.0.1"
ktor = "3.0.1"
lifecycle = "2.8.7"
lifecycle = "2.8.4"
material = "1.12.0"
minSdk = "23"
okio = "3.9.1"
Expand All @@ -54,8 +54,7 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-glance = { module = "androidx.glance:glance", version.ref = "glance" }
androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "palette" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test" }
Expand Down
1 change: 1 addition & 0 deletions shared/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.koin.core)
implementation(libs.slack.circuit)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import com.adammcneilly.pocketleague.core.displaymodels.MatchDetailDisplayModel
import com.adammcneilly.pocketleague.core.displaymodels.toDetailDisplayModel
import com.adammcneilly.pocketleague.core.displaymodels.toSummaryDisplayModel
import com.adammcneilly.pocketleague.core.models.Event
import com.adammcneilly.pocketleague.data.event.api.EventListRequest
import com.adammcneilly.pocketleague.data.event.api.EventRepository
import com.adammcneilly.pocketleague.feature.eventdetail.EventDetailScreen
import com.adammcneilly.pocketleague.shared.app.match.MatchDetailScreen
import com.slack.circuit.runtime.Navigator
Expand All @@ -30,7 +28,8 @@ private const val PLACEHOLDER_LIST_COUNT = 3
*/
class FeedPresenter(
private val getPastWeeksMatchesUseCase: GetPastWeeksMatchesUseCase,
private val eventRepository: EventRepository,
private val getOngoingEventsUseCase: GetOngoingEventsUseCase,
private val getUpcomingEventsUseCase: GetUpcomingEventsUseCase,
private val timeProvider: TimeProvider,
private val navigator: Navigator,
) : Presenter<FeedScreen.State> {
Expand Down Expand Up @@ -90,7 +89,7 @@ class FeedPresenter(
}
}

private fun observePastWeeksMatches() =
private fun observePastWeeksMatches(): Flow<List<MatchDetailDisplayModel>> =
getPastWeeksMatchesUseCase
.invoke()
.map { matchList ->
Expand All @@ -100,25 +99,17 @@ class FeedPresenter(
}

private fun observeOngoingEvents(): Flow<List<EventGroupDisplayModel>> {
val request = EventListRequest.OnDate(
dateUtc = timeProvider.now(),
)

return eventRepository
.stream(request)
return getOngoingEventsUseCase
.invoke()
.map { eventList ->
eventList.map(Event::toSummaryDisplayModel)
}
.map(EventGroupDisplayModel.Companion::mapFromEventList)
}

private fun observeUpcomingEvents(): Flow<List<EventGroupDisplayModel>> {
val request = EventListRequest.AfterDate(
dateUtc = timeProvider.now(),
)

return eventRepository
.stream(request)
return getUpcomingEventsUseCase
.invoke()
.map { eventList ->
eventList.map(Event::toSummaryDisplayModel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,19 @@ object FeedScreen : Screen {
object PresenterFactory : Presenter.Factory, KoinComponent {
private val timeProvider: TimeProvider by inject()
private val matchRepository: MatchRepository by inject()
private val eventRepository: EventRepository by inject()
private val getPastWeeksMatchesUseCase = GetPastWeeksMatchesUseCase(
timeProvider = timeProvider,
matchRepository = matchRepository,
)
private val eventRepository: EventRepository by inject()
private val getOngoingEventsUseCase = GetOngoingEventsUseCase(
eventRepository = eventRepository,
timeProvider = timeProvider,
)
private val getUpcomingEventsUseCase = GetUpcomingEventsUseCase(
eventRepository = eventRepository,
timeProvider = timeProvider,
)

override fun create(
screen: Screen,
Expand All @@ -107,7 +115,8 @@ object FeedScreen : Screen {
return when (screen) {
FeedScreen -> FeedPresenter(
getPastWeeksMatchesUseCase = getPastWeeksMatchesUseCase,
eventRepository = eventRepository,
getOngoingEventsUseCase = getOngoingEventsUseCase,
getUpcomingEventsUseCase = getUpcomingEventsUseCase,
timeProvider = timeProvider,
navigator = navigator,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.adammcneilly.pocketleague.shared.app.feed

import com.adammcneilly.pocketleague.core.displaymodels.EventGroupDisplayModel
import com.adammcneilly.pocketleague.core.displaymodels.MatchDetailDisplayModel

data class FeedUiState(
val recentMatches: List<MatchDetailDisplayModel>,
val ongoingEvents: List<EventGroupDisplayModel>,
val upcomingEvents: List<EventGroupDisplayModel>,
) {
companion object {
private const val PLACEHOLDER_LIST_COUNT = 3

/**
* Returns a default instance of [FeedUiState] where all display models are set
* to placeholders to display during a default loading state.
*/
fun placeholderState(): FeedUiState {
val recentMatches = List(PLACEHOLDER_LIST_COUNT) {
MatchDetailDisplayModel.placeholder
}

return FeedUiState(
recentMatches = recentMatches,
ongoingEvents = EventGroupDisplayModel.placeholder,
upcomingEvents = EventGroupDisplayModel.placeholder,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.adammcneilly.pocketleague.shared.app.feed

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.adammcneilly.pocketleague.core.datetime.TimeProvider
import com.adammcneilly.pocketleague.core.displaymodels.EventGroupDisplayModel
import com.adammcneilly.pocketleague.core.displaymodels.toDetailDisplayModel
import com.adammcneilly.pocketleague.core.displaymodels.toSummaryDisplayModel
import com.adammcneilly.pocketleague.core.models.Event
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

class FeedViewModel(
private val getPastWeeksMatchesUseCase: GetPastWeeksMatchesUseCase,
private val getOngoingEventsUseCase: GetOngoingEventsUseCase,
private val getUpcomingEventsUseCase: GetUpcomingEventsUseCase,
private val timeProvider: TimeProvider,
) : ViewModel() {
private val mutableState = MutableStateFlow(FeedUiState.placeholderState())
val state = mutableState.asStateFlow()

init {
observePastWeeksMatches()
observeOngoingEvents()
observeUpcomingEvents()
}

private fun observePastWeeksMatches() {
val matchFlow = getPastWeeksMatchesUseCase
.invoke()
.map { matchList ->
matchList.map { match ->
match.toDetailDisplayModel(timeProvider)
}
}

viewModelScope.launch {
matchFlow.collect { matchList ->
mutableState.update { currentState ->
currentState.copy(
recentMatches = matchList,
)
}
}
}
}

private fun observeOngoingEvents() {
val eventFlow = getOngoingEventsUseCase
.invoke()
.map { eventList ->
eventList.map(Event::toSummaryDisplayModel)
}
.map(EventGroupDisplayModel.Companion::mapFromEventList)

viewModelScope.launch {
eventFlow.collect { eventList ->
mutableState.update { currentState ->
currentState.copy(
ongoingEvents = eventList,
)
}
}
}
}

private fun observeUpcomingEvents() {
val eventFlow = getUpcomingEventsUseCase
.invoke()
.map { eventList ->
eventList.map(Event::toSummaryDisplayModel)
}
.map(EventGroupDisplayModel.Companion::mapFromEventList)
Comment on lines +73 to +76
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mapping can probably go into the use case.


viewModelScope.launch {
eventFlow.collect { eventList ->
mutableState.update { currentState ->
currentState.copy(
ongoingEvents = eventList,
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.adammcneilly.pocketleague.shared.app.feed

import com.adammcneilly.pocketleague.core.datetime.TimeProvider
import com.adammcneilly.pocketleague.core.models.Event
import com.adammcneilly.pocketleague.data.event.api.EventListRequest
import com.adammcneilly.pocketleague.data.event.api.EventRepository
import kotlinx.coroutines.flow.Flow

/**
* Return an observable type of events that are currently happening on today's date.
*/
class GetOngoingEventsUseCase(
private val eventRepository: EventRepository,
private val timeProvider: TimeProvider,
) {
/**
* @see [GetOngoingEventsUseCase]
*/
fun invoke(): Flow<List<Event>> {
val request = EventListRequest.OnDate(
dateUtc = timeProvider.now(),
)

return eventRepository.stream(request)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.adammcneilly.pocketleague.shared.app.feed

import com.adammcneilly.pocketleague.core.datetime.TimeProvider
import com.adammcneilly.pocketleague.core.models.Event
import com.adammcneilly.pocketleague.data.event.api.EventListRequest
import com.adammcneilly.pocketleague.data.event.api.EventRepository
import kotlinx.coroutines.flow.Flow

/**
* Return an observable type of events that are upcoming from today's date.
*/
class GetUpcomingEventsUseCase(
private val eventRepository: EventRepository,
private val timeProvider: TimeProvider,
) {
/**
* @see [GetUpcomingEventsUseCase]
*/
fun invoke(): Flow<List<Event>> {
val request = EventListRequest.AfterDate(
dateUtc = timeProvider.now(),
)

return eventRepository.stream(request)
}
}
Loading