SDP-GeoHunt/geo-hunt

View on GitHub
app/src/main/java/com/github/geohunt/app/ui/screens/home/HomeFeedViewModel.kt

Summary

Maintainability
B
5 hrs
Test Coverage
C
77%
package com.github.geohunt.app.ui.screens.home

import android.app.Application
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.github.geohunt.app.data.repository.ActiveHuntsRepositoryInterface
import com.github.geohunt.app.data.repository.AppContainer
import com.github.geohunt.app.data.repository.AuthRepositoryInterface
import com.github.geohunt.app.data.repository.ChallengeRepositoryInterface
import com.github.geohunt.app.data.repository.FollowRepository
import com.github.geohunt.app.data.repository.LocationRepositoryInterface
import com.github.geohunt.app.domain.GetChallengeHuntStateUseCase
import com.github.geohunt.app.domain.GetUserFeedUseCase
import com.github.geohunt.app.model.Challenge
import com.github.geohunt.app.model.Location
import com.github.geohunt.app.model.User
import com.github.geohunt.app.ui.AuthViewModel
import com.github.geohunt.app.ui.components.appbar.HomeScreenFeed
import com.github.geohunt.app.ui.components.buttons.ChallengeHuntState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch

/**
 * Represents a scrolling feed of challenges.
 */
class Feed(
    private val challengeFlow: Flow<List<Challenge>?>,
    private val coroutineScope: CoroutineScope,
    private val activeHuntsRepository: ActiveHuntsRepositoryInterface,
    val challengeRepository: ChallengeRepositoryInterface,
    private val getChallengeHuntStateUseCase: GetChallengeHuntStateUseCase
) {
    /**
     * Inner state representation of a challenge.
     *
     * Holds state about all UI elements relevant to the challenge, e.g. author, hunt state, etc.
     */
    private class ChallengeData(
        val _author: MutableStateFlow<User?> = MutableStateFlow(null),
        val _huntState: MutableStateFlow<ChallengeHuntState> = MutableStateFlow(ChallengeHuntState.UNKNOWN),
        val _isBusy: MutableStateFlow<Boolean> = MutableStateFlow(false)
    ) {
        val author: StateFlow<User?>
            get() = _author.asStateFlow()

        val huntState: StateFlow<ChallengeHuntState>
            get() = _huntState.asStateFlow()

        val isBusy: StateFlow<Boolean>
            get() = _isBusy.asStateFlow()
    }

    private val _challenges: MutableStateFlow<List<Challenge>?> = MutableStateFlow(null)
    val challenges: StateFlow<List<Challenge>?> = _challenges.asStateFlow()

    init {
        coroutineScope.launch {
            challengeFlow.collect {
                _challenges.value = it
            }
        }
    }


    private val challengeCache: MutableMap<Challenge, ChallengeData> = mutableMapOf()

    private fun fetchChallengeData(challenge: Challenge) {
        challengeCache[challenge] = ChallengeData()
        coroutineScope.launch {
            challengeCache[challenge]!!._author.value = challengeRepository.getAuthor(challenge)

            getChallengeHuntStateUseCase.getChallengeHuntState(challenge).collect {
                challengeCache[challenge]!!._huntState.value = it
            }
        }
    }

    fun getAuthor(challenge: Challenge): StateFlow<User?> {
        if (!challengeCache.containsKey(challenge)) {
            fetchChallengeData(challenge)
        }

        return challengeCache[challenge]!!.author
    }

    fun getChallengeHuntState(challenge: Challenge): StateFlow<ChallengeHuntState> {
        if (!challengeCache.containsKey(challenge)) {
            fetchChallengeData(challenge)
        }

        return challengeCache[challenge]!!.huntState
    }

    fun isBusy(challenge: Challenge): StateFlow<Boolean> {
        return challengeCache[challenge]!!.isBusy
    }

    fun hunt(challenge: Challenge) {
        challengeCache[challenge]!!._isBusy.value = true

        coroutineScope.launch {
            activeHuntsRepository.joinHunt(challenge)
            challengeCache[challenge]!!._isBusy.value = false
        }
    }

    fun leaveHunt(challenge: Challenge) {
        challengeCache[challenge]!!._isBusy.value = true
        coroutineScope.launch {
            activeHuntsRepository.leaveHunt(challenge)
            challengeCache[challenge]!!._isBusy.value = false
        }
    }
}

class HomeFeedViewModel(
    override val authRepository: AuthRepositoryInterface,
    private val getUserFeedUseCase: GetUserFeedUseCase,
    private val getChallengeHuntStateUseCase: GetChallengeHuntStateUseCase,
    private val challengeRepository: ChallengeRepositoryInterface,
    private val locationRepository: LocationRepositoryInterface,
    private val followRepository: FollowRepository,
    private val activeHuntsRepository: ActiveHuntsRepositoryInterface
): AuthViewModel(authRepository) {
    private val _userLocation: MutableStateFlow<Location?> = MutableStateFlow(null)
    val userLocation: StateFlow<Location?> = _userLocation.asStateFlow()

    private fun fetchLocation() {
        viewModelScope.launch {
            locationRepository.getLocations(viewModelScope).collect {
                _userLocation.value = it
            }
        }
    }

    init {
        fetchLocation()
    }

    private val feeds: MutableMap<HomeScreenFeed, Feed> = mutableMapOf()

    @OptIn(ExperimentalCoroutinesApi::class)
    fun getFeed(feed: HomeScreenFeed): Feed {
        if (!feeds.contains(feed)) {
            feeds[feed] = when(feed) {
                HomeScreenFeed.Home -> Feed(
                    challengeFlow = followRepository.getFollowList().flatMapLatest { getUserFeedUseCase.getFollowFeed(it) },
                    coroutineScope = viewModelScope,
                    activeHuntsRepository, challengeRepository, getChallengeHuntStateUseCase
                )

                HomeScreenFeed.Discover -> Feed(
                    challengeFlow = userLocation.flatMapLatest {
                        if (it != null) getUserFeedUseCase.getDiscoverFeed(it) else flowOf(null)
                    },
                    coroutineScope = viewModelScope,
                    activeHuntsRepository, challengeRepository, getChallengeHuntStateUseCase
                )

                HomeScreenFeed.Bounties -> throw UnsupportedOperationException()
            }
        }

        return feeds[feed]!!
    }

    fun follow(user: User) {
        viewModelScope.launch {
            followRepository.follow(user)
        }
    }

    fun unfollow(user: User) {
        viewModelScope.launch {
            followRepository.unfollow(user)
        }
    }

    fun isFollowing(user: User?): Flow<Boolean> {
        return if (user != null) followRepository.doesFollow(user) else flowOf(false)
    }

    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
                val container = AppContainer.getInstance(application)

                HomeFeedViewModel(
                    container.auth,
                    container.feedUseCase,
                    container.huntStateUseCase,
                    container.challenges,
                    container.location,
                    container.follow,
                    container.activeHunts
                )
            }
        }
    }
}