Skip to content
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 @@ -22,24 +22,19 @@ import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.ui.stateInScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@HiltViewModel
class MainActivityViewModel @Inject constructor(
userDataRepository: UserDataRepository
) : ViewModel() {
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userDataStream.map {
Success(it)
}.stateIn(
scope = viewModelScope,
initialValue = Loading,
started = SharingStarted.WhileSubscribed(5_000)
)
}.stateInScope(viewModelScope, initialValue = Loading)
}

sealed interface MainActivityUiState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.navigation.navOptions
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.core.ui.stateInScope
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksRoute
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute
Expand All @@ -47,9 +48,7 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKM
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@Composable
fun rememberNiaAppState(
Expand Down Expand Up @@ -95,11 +94,7 @@ class NiaAppState(

val isOffline = networkMonitor.isOnline
.map(Boolean::not)
.stateIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
.stateInScope(coroutineScope, initialValue = false)

/**
* Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.samples.apps.nowinandroid.core.ui

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn

/**
* This is a helper extension function for making it easier to use [Flow.stateIn] within a ViewModel or etc.
* @Returns a [StateFlow] that shares the latest value emitted by the original [Flow] and starts
* with the [initialValue].
*/
fun <T> Flow<T>.stateInScope(
coroutineScope: CoroutineScope,
started: SharingStarted = SharingStarted.WhileSubscribed(5_000),
initialValue: T
): StateFlow<T> = stateIn(coroutineScope, started, initialValue)
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.Result.Error
import com.google.samples.apps.nowinandroid.core.result.Result.Loading
import com.google.samples.apps.nowinandroid.core.result.Result.Success
import com.google.samples.apps.nowinandroid.core.result.asResult
import com.google.samples.apps.nowinandroid.core.ui.stateInScope
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@HiltViewModel
Expand All @@ -54,20 +56,11 @@ class AuthorViewModel @Inject constructor(
authorId = authorArgs.authorId,
userDataRepository = userDataRepository,
authorsRepository = authorsRepository
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = AuthorUiState.Loading
)
).stateInScope(viewModelScope, initialValue = AuthorUiState.Loading)

val newsUiState: StateFlow<NewsUiState> =
getSaveableNewsResourcesStream.newsUiStateStream(authorId = authorArgs.authorId)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsUiState.Loading
)
val newsUiState: StateFlow<NewsUiState> = getSaveableNewsResourcesStream
.newsUiStateStream(authorId = authorArgs.authorId)
.stateInScope(viewModelScope, initialValue = NewsUiState.Loading)

fun followAuthorToggle(followed: Boolean) {
viewModelScope.launch {
Expand All @@ -80,49 +73,54 @@ class AuthorViewModel @Inject constructor(
userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked)
}
}
}

private fun authorUiStateStream(
authorId: String,
userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
): Flow<AuthorUiState> {
// Observe the followed authors, as they could change over time.
val followedAuthorIdsStream: Flow<Set<String>> =
userDataRepository.userDataStream
.map { it.followedAuthors }

// Observe author information
val authorStream: Flow<Author> = authorsRepository.getAuthorStream(
id = authorId
)

return combine(
followedAuthorIdsStream,
authorStream,
::Pair
)
.asResult()
.map { followedAuthorToAuthorResult ->
when (followedAuthorToAuthorResult) {
is Result.Success -> {
val (followedAuthors, author) = followedAuthorToAuthorResult.data
val followed = followedAuthors.contains(authorId)
AuthorUiState.Success(
followableAuthor = FollowableAuthor(
author = author,
isFollowed = followed
)
)
}
is Result.Loading -> {
AuthorUiState.Loading
}
is Result.Error -> {
AuthorUiState.Error
}
private fun authorUiStateStream(
authorId: String,
userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
): Flow<AuthorUiState> {
// Observe the followed authors, as they could change over time.
val followedAuthorIdsStream: Flow<Set<String>> =
userDataRepository.userDataStream.map { it.followedAuthors }

// Observe author information
val authorStream: Flow<Author> = authorsRepository.getAuthorStream(
id = authorId
)

return combine(
followedAuthorIdsStream,
authorStream,
::Pair
)
.asResult()
.map { followedAuthorToAuthorResult ->
handleToAuthorResult(followedAuthorToAuthorResult, authorId)
}
}
}

private fun handleToAuthorResult(
followedAuthorToAuthorResult: Result<Pair<Set<String>, Author>>,
authorId: String
) = when (followedAuthorToAuthorResult) {
is Success -> onSuccessResult(followedAuthorToAuthorResult, authorId)
is Loading -> AuthorUiState.Loading
is Error -> AuthorUiState.Error
}

private fun onSuccessResult(
followedAuthorToAuthorResult: Success<Pair<Set<String>, Author>>,
authorId: String
): AuthorUiState.Success {
val (followedAuthors, author) = followedAuthorToAuthorResult.data
val followed = followedAuthors.contains(authorId)
return AuthorUiState.Success(
followableAuthor = FollowableAuthor(
author = author,
isFollowed = followed
)
)
}
}

private fun GetSaveableNewsResourcesStreamUseCase.newsUiStateStream(
Expand All @@ -134,9 +132,9 @@ private fun GetSaveableNewsResourcesStreamUseCase.newsUiStateStream(
).asResult()
.map { newsResult ->
when (newsResult) {
is Result.Success -> NewsUiState.Success(newsResult.data)
is Result.Loading -> NewsUiState.Loading
is Result.Error -> NewsUiState.Error
is Success -> NewsUiState.Success(newsResult.data)
is Loading -> NewsUiState.Loading
is Error -> NewsUiState.Error
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResources
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.stateInScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@HiltViewModel
Expand All @@ -44,11 +43,7 @@ class BookmarksViewModel @Inject constructor(
.map { newsResources -> newsResources.filter(SaveableNewsResource::isSaved) } // Only show bookmarked news resources.
.map<List<SaveableNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(Loading) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading
)
.stateInScope(viewModelScope, initialValue = Loading)

fun removeFromSavedResources(newsResourceId: String) {
viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,16 @@ import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResources
import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.stateInScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@HiltViewModel
Expand All @@ -50,12 +49,9 @@ class ForYouViewModel @Inject constructor(
private val shouldShowOnboarding: Flow<Boolean> =
userDataRepository.userDataStream.map { !it.shouldHideOnboarding }

val isSyncing = syncStatusMonitor.isSyncing
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
val isSyncing = syncStatusMonitor
.isSyncing
.stateInScope(viewModelScope, initialValue = false)

val feedState: StateFlow<NewsFeedUiState> =
userDataRepository.userDataStream
Expand All @@ -79,11 +75,7 @@ class ForYouViewModel @Inject constructor(
// As the selected topics and topic state changes, this will cancel the old feed
// monitoring and start the new one.
.flatMapLatest { it }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading
)
.stateInScope(viewModelScope, initialValue = NewsFeedUiState.Loading)

val onboardingUiState: StateFlow<OnboardingUiState> =
combine(
Expand All @@ -99,12 +91,7 @@ class ForYouViewModel @Inject constructor(
} else {
OnboardingUiState.NotShown
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = OnboardingUiState.Loading
)
}.stateInScope(viewModelScope, initialValue = OnboardingUiState.Loading)

fun updateTopicSelection(topicId: String, isChecked: Boolean) {
viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAutho
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.ui.stateInScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

Expand All @@ -54,11 +53,7 @@ class InterestsViewModel @Inject constructor(
getSortedFollowableAuthorsStream(),
getFollowableTopicsStream(sortBy = TopicSortField.NAME),
InterestsUiState::Interests
).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
).stateInScope(viewModelScope, initialValue = InterestsUiState.Loading)

fun followTopic(followedTopicId: String, followed: Boolean) {
viewModelScope.launch {
Expand Down
Loading