Merge pull request #120 from lihenggui/feature/foryou

Make for you module as the multiplatform module
pull/2064/head
Mercury Li 2 years ago committed by GitHub
commit 53f9a5963e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -16,15 +16,17 @@
package com.google.samples.apps.nowinandroid.core.di
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
@Component
actual abstract class DispatchersComponent {
abstract class AndroidDispatchersComponent : DispatchersComponent() {
@Provides
actual fun providesIODispatcher(): IODispatcher = Dispatchers.IO
override fun providesIODispatcher(): @IoDispatcher CoroutineDispatcher = Dispatchers.IO
@Provides
actual fun providesDefaultDispatcher(): DefaultDispatcher = Dispatchers.Default
override fun providesDefaultDispatcher(): @DefaultDispatcher CoroutineDispatcher =
Dispatchers.Default
}

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.core.di
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import me.tatarka.inject.annotations.Component
@ -29,9 +30,12 @@ import me.tatarka.inject.annotations.Scope
annotation class ApplicationScope
@Component
abstract class CoroutineScopeComponent {
abstract class CoroutineScopeComponent(
@Component val dispatchersComponent: DispatchersComponent,
) {
@DefaultDispatcher abstract val defaultDispatcher: CoroutineDispatcher
@Provides
fun providesCoroutineScope(
dispatcher: DefaultDispatcher,
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
fun providesCoroutineScope(): CoroutineScope =
CoroutineScope(SupervisorJob() + defaultDispatcher)
}

@ -0,0 +1,49 @@
/*
* Copyright 2024 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.di
import me.tatarka.inject.annotations.Qualifier
@Qualifier
@Target(
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.TYPE,
AnnotationTarget.PROPERTY,
)
annotation class IoDispatcher
@Qualifier
@Target(
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.TYPE,
AnnotationTarget.PROPERTY,
)
annotation class MainDispatcher
@Qualifier
@Target(
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.TYPE,
AnnotationTarget.PROPERTY,
)
annotation class DefaultDispatcher

@ -19,13 +19,10 @@ package com.google.samples.apps.nowinandroid.core.di
import kotlinx.coroutines.CoroutineDispatcher
import me.tatarka.inject.annotations.Provides
typealias DefaultDispatcher = CoroutineDispatcher
typealias IODispatcher = CoroutineDispatcher
expect abstract class DispatchersComponent {
abstract class DispatchersComponent {
@Provides
fun providesIODispatcher(): IODispatcher
abstract fun providesIODispatcher(): @IoDispatcher CoroutineDispatcher
@Provides
fun providesDefaultDispatcher(): DefaultDispatcher
abstract fun providesDefaultDispatcher(): @DefaultDispatcher CoroutineDispatcher
}

@ -16,15 +16,16 @@
package com.google.samples.apps.nowinandroid.core.di
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
@Component
actual abstract class DispatchersComponent {
abstract class JvmDispatchersComponent : DispatchersComponent() {
@Provides
actual fun providesIODispatcher(): IODispatcher = Dispatchers.IO
override fun providesIODispatcher(): @IoDispatcher CoroutineDispatcher = Dispatchers.IO
@Provides
actual fun providesDefaultDispatcher(): DefaultDispatcher = Dispatchers.Default
override fun providesDefaultDispatcher(): @DefaultDispatcher CoroutineDispatcher = Dispatchers.Default
}

@ -16,17 +16,18 @@
package com.google.samples.apps.nowinandroid.core.di
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
@Component
actual abstract class DispatchersComponent {
// TODO Provides an actual IODispatcher
abstract class NativeDispatchersComponent : DispatchersComponent() {
@Provides
actual fun providesIODispatcher(): IODispatcher = Dispatchers.Default
override fun providesIODispatcher(): @IoDispatcher CoroutineDispatcher = Dispatchers.IO
@Provides
actual fun providesDefaultDispatcher(): DefaultDispatcher = Dispatchers.Default
override fun providesDefaultDispatcher(): @DefaultDispatcher CoroutineDispatcher =
Dispatchers.Default
}

@ -0,0 +1,56 @@
/*
* Copyright 2024 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.data.di
import android.app.Application
import com.google.samples.apps.nowinandroid.core.data.util.ConnectivityManagerNetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneBroadcastMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.di.AndroidApplicationComponent
import com.google.samples.apps.nowinandroid.core.di.CoroutineScopeComponent
import com.google.samples.apps.nowinandroid.core.di.DispatchersComponent
import com.google.samples.apps.nowinandroid.core.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
@Component
abstract class AndroidPlatformDependentDataModule(
@Component val applicationComponent: AndroidApplicationComponent,
@Component val dispatchersComponent: DispatchersComponent,
@Component val coroutineScopeComponent: CoroutineScopeComponent,
) : PlatformDependentDataModule() {
abstract val application: Application
@IoDispatcher abstract val ioDispatcher: CoroutineDispatcher
abstract val coroutineScope: CoroutineScope
@Provides
override fun bindsNetworkMonitor(): NetworkMonitor {
return ConnectivityManagerNetworkMonitor(
application,
ioDispatcher,
)
}
@Provides
override fun bindsTimeZoneMonitor(): TimeZoneMonitor {
return TimeZoneBroadcastMonitor(application, coroutineScope, ioDispatcher)
}
}

@ -1,35 +0,0 @@
/*
* Copyright 2024 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.data.di
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
@Component
internal actual abstract class PlatformDependentDataModule {
@Provides
internal actual fun bindsNetworkMonitor(): NetworkMonitor {
TODO()
}
@Provides
internal actual fun bindsTimeZoneMonitor(): TimeZoneMonitor {
TODO()
}
}

@ -23,7 +23,7 @@ import android.content.IntentFilter
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.di.IODispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharedFlow
@ -42,7 +42,7 @@ import java.time.ZoneId
internal class TimeZoneBroadcastMonitor(
private val context: Context,
appScope: CoroutineScope,
private val ioDispatcher: IODispatcher,
private val ioDispatcher: CoroutineDispatcher,
) : TimeZoneMonitor {
override val currentTimeZone: SharedFlow<TimeZone> =

@ -43,7 +43,7 @@ abstract class DataModule {
): NewsRepository = newsRepository
@Provides
fun userDataRepository(
fun bindsUserDataRepository(
userDataRepository: OfflineFirstUserDataRepository,
): UserDataRepository = userDataRepository

@ -20,10 +20,10 @@ import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import me.tatarka.inject.annotations.Provides
internal expect abstract class PlatformDependentDataModule {
abstract class PlatformDependentDataModule {
@Provides
internal fun bindsNetworkMonitor(): NetworkMonitor
abstract fun bindsNetworkMonitor(): NetworkMonitor
@Provides
internal fun bindsTimeZoneMonitor(): TimeZoneMonitor
abstract fun bindsTimeZoneMonitor(): TimeZoneMonitor
}

@ -22,7 +22,7 @@ import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
@Component
internal abstract class UserNewsResourceRepositoryModule {
abstract class UserNewsResourceRepositoryModule {
@Provides
fun bindsUserNewsResourceRepository(
userDataRepository: CompositeUserNewsResourceRepository,

@ -23,8 +23,9 @@ import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity
import com.google.samples.apps.nowinandroid.core.di.IODispatcher
import com.google.samples.apps.nowinandroid.core.di.IoDispatcher
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@ -42,7 +43,7 @@ class DefaultSearchContentsRepository(
private val newsResourceFtsDao: NewsResourceFtsDao,
private val topicDao: TopicDao,
private val topicFtsDao: TopicFtsDao,
private val ioDispatcher: IODispatcher,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : SearchContentsRepository {
override suspend fun populateFtsData() {

@ -29,9 +29,9 @@ import me.tatarka.inject.annotations.Provides
* Leave empty for now
*/
@Component
internal actual abstract class PlatformDependentDataModule {
abstract class JvmPlatformDependentDataModule : PlatformDependentDataModule() {
@Provides
internal actual fun bindsNetworkMonitor(): NetworkMonitor {
override fun bindsNetworkMonitor(): NetworkMonitor {
return object : NetworkMonitor {
override val isOnline: Flow<Boolean>
get() = flowOf(true)
@ -39,7 +39,7 @@ internal actual abstract class PlatformDependentDataModule {
}
@Provides
internal actual fun bindsTimeZoneMonitor(): TimeZoneMonitor {
override fun bindsTimeZoneMonitor(): TimeZoneMonitor {
return object : TimeZoneMonitor {
override val currentTimeZone: Flow<TimeZone>
get() = flowOf(TimeZone.UTC)

@ -29,9 +29,9 @@ import me.tatarka.inject.annotations.Provides
* Leave empty for now
*/
@Component
internal actual abstract class PlatformDependentDataModule {
abstract class NativePlatformDependentDataModule : PlatformDependentDataModule() {
@Provides
internal actual fun bindsNetworkMonitor(): NetworkMonitor {
override fun bindsNetworkMonitor(): NetworkMonitor {
return object : NetworkMonitor {
override val isOnline: Flow<Boolean>
get() = flowOf(true)
@ -39,7 +39,7 @@ internal actual abstract class PlatformDependentDataModule {
}
@Provides
internal actual fun bindsTimeZoneMonitor(): TimeZoneMonitor {
override fun bindsTimeZoneMonitor(): TimeZoneMonitor {
return object : TimeZoneMonitor {
override val currentTimeZone: Flow<TimeZone>
get() = flowOf(TimeZone.UTC)

@ -23,42 +23,41 @@ import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao
import com.google.samples.apps.nowinandroid.core.database.dao.RecentSearchQueryDao
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao
import com.google.samples.apps.nowinandroid.core.di.IODispatcher
import com.google.samples.apps.nowinandroid.core.di.DispatchersComponent
import com.google.samples.apps.nowinandroid.core.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
@Component
internal abstract class DatabaseModule {
abstract class DatabaseModule(@Component val dispatchersComponent: DispatchersComponent) {
@IoDispatcher abstract val dispatcher: CoroutineDispatcher
@Provides
fun providesNiaDatabase(driver: SqlDriver): NiaDatabase = NiaDatabase(driver)
@Provides
fun providesTopicsDao(
database: NiaDatabase,
dispatcher: IODispatcher,
): TopicDao = TopicDao(database, dispatcher)
@Provides
fun providesNewsResourceDao(
database: NiaDatabase,
dispatcher: IODispatcher,
): NewsResourceDao = NewsResourceDao(database, dispatcher)
@Provides
fun providesTopicFtsDao(
database: NiaDatabase,
dispatcher: IODispatcher,
): TopicFtsDao = TopicFtsDao(database, dispatcher)
@Provides
fun providesNewsResourceFtsDao(
database: NiaDatabase,
dispatcher: IODispatcher,
): NewsResourceFtsDao = NewsResourceFtsDao(database, dispatcher)
@Provides
fun providesRecentSearchQueryDao(
database: NiaDatabase,
dispatcher: IODispatcher,
): RecentSearchQueryDao = RecentSearchQueryDao(database, dispatcher)
}

@ -15,8 +15,7 @@
*/
package com.google.samples.apps.nowinandroid.core.datastore
import com.google.samples.apps.nowinandroid.core.di.IODispatcher
import com.google.samples.apps.nowinandroid.core.di.IoDispatcher
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
@ -25,6 +24,7 @@ import com.russhwolf.settings.Settings
import com.russhwolf.settings.serialization.decodeValue
import com.russhwolf.settings.serialization.decodeValueOrNull
import com.russhwolf.settings.serialization.encodeValue
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
@ -38,7 +38,7 @@ private const val USER_DATA_KEY = "userData"
@Inject
class NiaPreferencesDataSource(
private val settings: Settings,
private val dispatcher: IODispatcher,
@IoDispatcher private val dispatcher: CoroutineDispatcher,
) {
// FlowSettings did not support JS, use a workaround instead
// https://github.com/russhwolf/multiplatform-settings/issues/139

@ -16,13 +16,14 @@
package com.google.samples.apps.nowinandroid.core.network.demo
import com.google.samples.apps.nowinandroid.core.di.IODispatcher
import com.google.samples.apps.nowinandroid.core.di.IoDispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.assets.NEWS_DATA
import com.google.samples.apps.nowinandroid.core.network.assets.TOPICS_DATA
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
@ -31,7 +32,7 @@ import me.tatarka.inject.annotations.Inject
* [NiaNetworkDataSource] implementation that provides static news resources to aid development
*/
class DemoNiaNetworkDataSource @Inject constructor(
private val ioDispatcher: IODispatcher,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val networkJson: Json,
) : NiaNetworkDataSource {

@ -1,25 +0,0 @@
/*
* Copyright 2024 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.feature.bookmarks.di
import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksViewModel
import me.tatarka.inject.annotations.Component
@Component
abstract class BookmarkComponent {
abstract val viewModel: BookmarksViewModel
}

@ -19,9 +19,6 @@ package com.google.samples.apps.nowinandroid.feature.bookmarks.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute
import com.google.samples.apps.nowinandroid.feature.bookmarks.di.BookmarkComponent
const val BOOKMARKS_ROUTE = "bookmarks_route"
@ -31,8 +28,7 @@ fun NavGraphBuilder.bookmarksScreen(
onTopicClick: (String) -> Unit,
onShowSnackbar: suspend (String, String?) -> Boolean,
) {
val viewModel = BookmarkComponent::class.create().viewModel
composable(route = BOOKMARKS_ROUTE) {
BookmarksRoute(onTopicClick, onShowSnackbar, viewModel)
}
// composable(route = BOOKMARKS_ROUTE) {
// BookmarksRoute(onTopicClick, onShowSnackbar, viewModel)
// }
}

@ -20,16 +20,15 @@ import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNe
import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Success
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
@ -38,9 +37,6 @@ import kotlin.test.assertIs
* https://developer.android.com/kotlin/flow/test#statein
*/
class BookmarksViewModelTest {
@get:Rule
val dispatcherRule = MainDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val newsRepository = TestNewsRepository()
private val userNewsResourceRepository = CompositeUserNewsResourceRepository(
@ -49,7 +45,7 @@ class BookmarksViewModelTest {
)
private lateinit var viewModel: BookmarksViewModel
@Before
@BeforeTest
fun setup() {
viewModel = BookmarksViewModel(
userDataRepository = userDataRepository,
@ -75,6 +71,7 @@ class BookmarksViewModelTest {
collectJob.cancel()
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun oneBookmark_whenRemoving_removesFromFeed() = runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.feedUiState.collect() }

@ -15,8 +15,9 @@
*/
plugins {
alias(libs.plugins.nowinandroid.android.feature)
alias(libs.plugins.nowinandroid.android.library.compose)
alias(libs.plugins.nowinandroid.cmp.feature)
alias(libs.plugins.jetbrains.compose)
alias(libs.plugins.compose)
alias(libs.plugins.nowinandroid.android.library.jacoco)
alias(libs.plugins.roborazzi)
}
@ -25,16 +26,32 @@ android {
namespace = "com.google.samples.apps.nowinandroid.feature.foryou"
}
dependencies {
implementation(libs.accompanist.permissions)
implementation(projects.core.data)
implementation(projects.core.domain)
testImplementation(libs.hilt.android.testing)
testImplementation(libs.robolectric)
testImplementation(projects.core.testing)
testImplementation(projects.core.screenshotTesting)
testDemoImplementation(libs.roborazzi)
androidTestImplementation(projects.core.testing)
kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.data)
implementation(projects.core.domain)
implementation(compose.material3)
implementation(compose.foundation)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.coil)
implementation(libs.coil.compose)
}
androidMain.dependencies {
implementation(libs.accompanist.permissions)
}
commonMain.dependencies {
implementation(projects.core.testing)
}
androidUnitTest.dependencies {
implementation(libs.robolectric)
implementation(libs.roborazzi)
implementation(projects.core.screenshotTesting)
}
androidInstrumentedTest.dependencies {
implementation(projects.core.testing)
}
}
}

@ -16,10 +16,6 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import android.net.Uri
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -63,29 +59,18 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.tracing.trace
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus.Denied
import com.google.accompanist.permissions.rememberPermissionState
import coil3.ImageLoader
import coil3.compose.LocalPlatformContext
import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
@ -100,16 +85,22 @@ import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.UserNewsResourcePreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.ui.launchCustomChromeTab
import com.google.samples.apps.nowinandroid.core.ui.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
import nowinandroid.feature.foryou.generated.resources.Res
import nowinandroid.feature.foryou.generated.resources.feature_foryou_done
import nowinandroid.feature.foryou.generated.resources.feature_foryou_loading
import nowinandroid.feature.foryou.generated.resources.feature_foryou_onboarding_guidance_subtitle
import nowinandroid.feature.foryou.generated.resources.feature_foryou_onboarding_guidance_title
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.PreviewParameter
@Composable
internal fun ForYouRoute(
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: ForYouViewModel = hiltViewModel(),
viewModel: ForYouViewModel,
) {
val onboardingUiState by viewModel.onboardingUiState.collectAsStateWithLifecycle()
val feedState by viewModel.feedState.collectAsStateWithLifecycle()
@ -148,8 +139,8 @@ internal fun ForYouScreen(
val isOnboardingLoading = onboardingUiState is OnboardingUiState.Loading
val isFeedLoading = feedState is NewsFeedUiState.Loading
// This code should be called when the UI is ready for use and relates to Time To Full Display.
ReportDrawnWhen { !isSyncing && !isOnboardingLoading && !isFeedLoading }
// // This code should be called when the UI is ready for use and relates to Time To Full Display.
// ReportDrawnWhen { !isSyncing && !isOnboardingLoading && !isFeedLoading }
val itemsAvailable = feedItemsSize(feedState, onboardingUiState)
@ -157,7 +148,7 @@ internal fun ForYouScreen(
val scrollbarState = state.scrollbarState(
itemsAvailable = itemsAvailable,
)
TrackScrollJank(scrollableState = state, stateName = "forYou:feed")
// TrackScrollJank(scrollableState = state, stateName = "forYou:feed")
Box(
modifier = modifier
@ -216,7 +207,7 @@ internal fun ForYouScreen(
targetOffsetY = { fullHeight -> -fullHeight },
) + fadeOut(),
) {
val loadingContentDescription = stringResource(id = R.string.feature_foryou_loading)
val loadingContentDescription = stringResource(Res.string.feature_foryou_loading)
Box(
modifier = Modifier
.fillMaxWidth()
@ -243,7 +234,7 @@ internal fun ForYouScreen(
)
}
TrackScreenViewEvent(screenName = "ForYou")
NotificationPermissionEffect()
// NotificationPermissionEffect()
DeepLinkEffect(
deepLinkedUserNewsResource,
onDeepLinkOpened,
@ -271,7 +262,7 @@ private fun LazyStaggeredGridScope.onboarding(
item(span = StaggeredGridItemSpan.FullLine, contentType = "onboarding") {
Column(modifier = interestsItemModifier) {
Text(
text = stringResource(R.string.feature_foryou_onboarding_guidance_title),
text = stringResource(Res.string.feature_foryou_onboarding_guidance_title),
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
@ -279,7 +270,7 @@ private fun LazyStaggeredGridScope.onboarding(
style = MaterialTheme.typography.titleMedium,
)
Text(
text = stringResource(R.string.feature_foryou_onboarding_guidance_subtitle),
text = stringResource(Res.string.feature_foryou_onboarding_guidance_subtitle),
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, start = 24.dp, end = 24.dp),
@ -305,7 +296,7 @@ private fun LazyStaggeredGridScope.onboarding(
.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.feature_foryou_done),
text = stringResource(Res.string.feature_foryou_done),
)
}
}
@ -320,11 +311,11 @@ private fun TopicSelection(
onboardingUiState: OnboardingUiState.Shown,
onTopicCheckedChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier,
) = trace("TopicSelection") {
) {
val lazyGridState = rememberLazyGridState()
val topicSelectionTestTag = "forYou:topicSelection"
TrackScrollJank(scrollableState = lazyGridState, stateName = topicSelectionTestTag)
// TrackScrollJank(scrollableState = lazyGridState, stateName = topicSelectionTestTag)
Box(
modifier = modifier
@ -381,7 +372,7 @@ private fun SingleTopicButton(
imageUrl: String,
isSelected: Boolean,
onClick: (String, Boolean) -> Unit,
) = trace("SingleTopicButton") {
) {
Surface(
modifier = Modifier
.width(312.dp)
@ -434,52 +425,53 @@ fun TopicIcon(
modifier: Modifier = Modifier,
) {
DynamicAsyncImage(
placeholder = painterResource(R.drawable.feature_foryou_ic_icon_placeholder),
// placeholder = painterResource(R.drawable.feature_foryou_ic_icon_placeholder),
imageUrl = imageUrl,
// decorative
contentDescription = null,
modifier = modifier
.padding(10.dp)
.size(32.dp),
imageLoader = ImageLoader(LocalPlatformContext.current),
)
}
@Composable
@OptIn(ExperimentalPermissionsApi::class)
private fun NotificationPermissionEffect() {
// Permission requests should only be made from an Activity Context, which is not present
// in previews
if (LocalInspectionMode.current) return
if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) return
val notificationsPermissionState = rememberPermissionState(
android.Manifest.permission.POST_NOTIFICATIONS,
)
LaunchedEffect(notificationsPermissionState) {
val status = notificationsPermissionState.status
if (status is Denied && !status.shouldShowRationale) {
notificationsPermissionState.launchPermissionRequest()
}
}
}
// @Composable
// @OptIn(ExperimentalPermissionsApi::class)
// private fun NotificationPermissionEffect() {
// // Permission requests should only be made from an Activity Context, which is not present
// // in previews
// if (LocalInspectionMode.current) return
// if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) return
// val notificationsPermissionState = rememberPermissionState(
// android.Manifest.permission.POST_NOTIFICATIONS,
// )
// LaunchedEffect(notificationsPermissionState) {
// val status = notificationsPermissionState.status
// if (status is Denied && !status.shouldShowRationale) {
// notificationsPermissionState.launchPermissionRequest()
// }
// }
// }
@Composable
private fun DeepLinkEffect(
userNewsResource: UserNewsResource?,
onDeepLinkOpened: (String) -> Unit,
) {
val context = LocalContext.current
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
LaunchedEffect(userNewsResource) {
if (userNewsResource == null) return@LaunchedEffect
if (!userNewsResource.hasBeenViewed) onDeepLinkOpened(userNewsResource.id)
launchCustomChromeTab(
context = context,
uri = Uri.parse(userNewsResource.url),
toolbarColor = backgroundColor,
)
}
// val context = LocalContext.current
// val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
//
// LaunchedEffect(userNewsResource) {
// if (userNewsResource == null) return@LaunchedEffect
// if (!userNewsResource.hasBeenViewed) onDeepLinkOpened(userNewsResource.id)
//
// launchCustomChromeTab(
// context = context,
// uri = Uri.parse(userNewsResource.url),
// toolbarColor = backgroundColor,
// )
// }
}
private fun feedItemsSize(

@ -29,7 +29,7 @@ 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.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.LINKED_NEWS_RESOURCE_ID
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -39,9 +39,8 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
import me.tatarka.inject.annotations.Inject
@HiltViewModel
class ForYouViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
syncManager: SyncManager,
@ -54,6 +53,7 @@ class ForYouViewModel @Inject constructor(
private val shouldShowOnboarding: Flow<Boolean> =
userDataRepository.userData.map { !it.shouldHideOnboarding }
@OptIn(ExperimentalCoroutinesApi::class)
val deepLinkedNewsResource = savedStateHandle.getStateFlow<String?>(
key = LINKED_NEWS_RESOURCE_ID,
null,

@ -22,8 +22,6 @@ import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouRoute
const val LINKED_NEWS_RESOURCE_ID = "linkedNewsResourceId"
const val FOR_YOU_ROUTE = "for_you_route/{$LINKED_NEWS_RESOURCE_ID}"
@ -36,12 +34,12 @@ fun NavGraphBuilder.forYouScreen(onTopicClick: (String) -> Unit) {
composable(
route = FOR_YOU_ROUTE,
deepLinks = listOf(
navDeepLink { uriPattern = DEEP_LINK_URI_PATTERN },
// navDeepLink { uriPattern = DEEP_LINK_URI_PATTERN },
),
arguments = listOf(
navArgument(LINKED_NEWS_RESOURCE_ID) { type = NavType.StringType },
),
) {
ForYouRoute(onTopicClick)
// ForYouRoute(onTopicClick)
}
}

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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
http://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.
-->
<manifest />
Loading…
Cancel
Save