pull/2099/merge
muraki 1 week ago committed by GitHub
commit cb205fc7b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -110,6 +110,7 @@ class NavigationTest {
// TODO: implement tests related to navigation & resetting of destinations (b/213307564)
// Restoring content should be tested with another tab than the For You one, as that will
// still succeed even when restoring state is turned off.
/**
* When navigating between the different top level destinations, we should restore the state
* of previously visited destinations.

@ -17,18 +17,16 @@
package com.google.samples.apps.nowinandroid
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.ui.stateInUi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
@ -37,11 +35,7 @@ class MainActivityViewModel @Inject constructor(
) : ViewModel() {
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map {
Success(it)
}.stateIn(
scope = viewModelScope,
initialValue = Loading,
started = SharingStarted.WhileSubscribed(5_000),
)
}.stateInUi(initialValue = Loading)
}
sealed interface MainActivityUiState {

@ -105,5 +105,10 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() =
*/
"-Xconsistent-data-class-copy-visibility",
)
freeCompilerArgs.add(
// Enable context parameters (experimental, Kotlin 2.x).
// Used by Flow<T>.stateInUi which declares context(viewModel: ViewModel).
"-Xcontext-parameters",
)
}
}

@ -30,6 +30,7 @@ dependencies {
api(projects.core.model)
implementation(libs.androidx.browser)
implementation(libs.androidx.lifecycle.viewModelCompose)
implementation(libs.coil.kt)
implementation(libs.coil.kt.compose)

@ -0,0 +1,46 @@
/*
* Copyright 2026 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 androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
/**
* Converts a [Flow] to a [StateFlow] scoped to the [ViewModel]'s lifecycle, using
* [SharingStarted.WhileSubscribed] with a 5-second stop timeout.
*
* Shorthand for:
* ```
* stateIn(
* scope = viewModelScope,
* started = SharingStarted.WhileSubscribed(5_000),
* initialValue = initialValue,
* )
* ```
*
* The [ViewModel] context is resolved implicitly from `this` when called inside a [ViewModel].
*/
context(viewModel: ViewModel)
fun <T> Flow<T>.stateInUi(initialValue: T): StateFlow<T> = stateIn(
scope = viewModel.viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = initialValue,
)

@ -26,12 +26,11 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
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.stateInUi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -48,11 +47,7 @@ class BookmarksViewModel @Inject constructor(
userNewsResourceRepository.observeAllBookmarked()
.map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(Loading) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading,
)
.stateInUi(initialValue = Loading)
fun removeFromSavedResources(newsResourceId: String) {
viewModelScope.launch {

@ -29,15 +29,14 @@ import com.google.samples.apps.nowinandroid.core.data.util.SyncManager
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.notifications.DEEP_LINK_NEWS_RESOURCE_ID_KEY
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.stateInUi
import dagger.hilt.android.lifecycle.HiltViewModel
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.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -70,27 +69,15 @@ class ForYouViewModel @Inject constructor(
}
}
.map { it.firstOrNull() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)
.stateInUi(initialValue = null)
val isSyncing = syncManager.isSyncing
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false,
)
.stateInUi(initialValue = false)
val feedState: StateFlow<NewsFeedUiState> =
userNewsResourceRepository.observeAllForFollowedTopics()
.map(NewsFeedUiState::Success)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading,
)
.stateInUi(initialValue = NewsFeedUiState.Loading)
val onboardingUiState: StateFlow<OnboardingUiState> =
combine(
@ -103,11 +90,7 @@ class ForYouViewModel @Inject constructor(
OnboardingUiState.NotShown
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = OnboardingUiState.Loading,
)
.stateInUi(initialValue = OnboardingUiState.Loading)
fun updateTopicSelection(topicId: String, isChecked: Boolean) {
viewModelScope.launch {

@ -23,15 +23,14 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserDataReposit
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.ui.stateInUi
import com.google.samples.apps.nowinandroid.feature.interests.api.navigation.InterestsNavKey
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel(assistedFactory = InterestsViewModel.Factory::class)
@ -57,11 +56,7 @@ class InterestsViewModel @AssistedInject constructor(
selectedTopicId,
getFollowableTopics(sortBy = TopicSortField.NAME),
InterestsUiState::Interests,
).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading,
)
).stateInUi(initialValue = InterestsUiState.Loading)
fun followTopic(followedTopicId: String, followed: Boolean) {
viewModelScope.launch {

@ -28,14 +28,13 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserDataReposit
import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase
import com.google.samples.apps.nowinandroid.core.model.data.UserSearchResult
import com.google.samples.apps.nowinandroid.core.ui.stateInUi
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -75,20 +74,12 @@ class SearchViewModel @Inject constructor(
}
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SearchResultUiState.Loading,
)
}.stateInUi(initialValue = SearchResultUiState.Loading)
val recentSearchQueriesUiState: StateFlow<RecentSearchQueriesUiState> =
recentSearchQueriesUseCase()
.map(RecentSearchQueriesUiState::Success)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = RecentSearchQueriesUiState.Loading,
)
.stateInUi(initialValue = RecentSearchQueriesUiState.Loading)
fun onSearchQueryChanged(query: String) {
savedStateHandle[SEARCH_QUERY] = query

@ -21,16 +21,14 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.ui.stateInUi
import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Loading
import com.google.samples.apps.nowinandroid.feature.settings.impl.SettingsUiState.Success
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@HiltViewModel
class SettingsViewModel @Inject constructor(
@ -47,11 +45,7 @@ class SettingsViewModel @Inject constructor(
),
)
}
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5.seconds.inWholeMilliseconds),
initialValue = Loading,
)
.stateInUi(initialValue = Loading)
fun updateThemeBrand(themeBrand: ThemeBrand) {
viewModelScope.launch {

@ -27,16 +27,15 @@ import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourc
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.stateInUi
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
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(assistedFactory = TopicViewModel.Factory::class)
@ -51,22 +50,14 @@ class TopicViewModel @AssistedInject constructor(
userDataRepository = userDataRepository,
topicsRepository = topicsRepository,
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TopicUiState.Loading,
)
.stateInUi(initialValue = TopicUiState.Loading)
val newsUiState: StateFlow<NewsUiState> = newsUiState(
topicId = topicId,
userDataRepository = userDataRepository,
userNewsResourceRepository = userNewsResourceRepository,
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsUiState.Loading,
)
.stateInUi(initialValue = NewsUiState.Loading)
fun followTopicToggle(followed: Boolean) {
viewModelScope.launch {

@ -51,7 +51,7 @@ kotlinxCoroutines = "1.10.1"
kotlinxDatetime = "0.6.1"
kotlinxSerializationJson = "1.8.0"
ksp = "2.3.4"
ktlint = "1.4.0"
ktlint = "1.7.1"
okhttp = "4.12.0"
protobuf = "4.29.2"
protobufPlugin = "0.9.6"

Loading…
Cancel
Save