Kotlinify codebase

- Remove unnecessary nullable types
- Replace no-op  method bodies with Unit
- Convert to expression body
- Replace if with when
- Remove braces from 'when' entries
- Remove braces from if statement
- Convert to single line lambda
- oneline if/returns
- Replace 'contains' call with 'in' operator

Following this refactor, it could be great to envision a more "strict" code formatter like ktlint 1.0 (we are currently stuck at 0.48.1)
pull/1039/head
Simon Marquis 2 years ago
parent 5087c86412
commit caa482bc71

@ -90,7 +90,7 @@ class NavigationTest {
lateinit var topicsRepository: TopicsRepository lateinit var topicsRepository: TopicsRepository
private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) =
ReadOnlyProperty<Any?, String> { _, _ -> activity.getString(resId) } ReadOnlyProperty<Any, String> { _, _ -> activity.getString(resId) }
// The strings used for matching in these tests // The strings used for matching in these tests
private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.navigate_up) private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.navigate_up)

@ -90,9 +90,7 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch { lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState viewModel.uiState
.onEach { .onEach { uiState = it }
uiState = it
}
.collect() .collect()
} }
} }

@ -20,6 +20,7 @@ import android.app.Activity
import android.util.Log import android.util.Log
import android.view.Window import android.view.Window
import androidx.metrics.performance.JankStats import androidx.metrics.performance.JankStats
import androidx.metrics.performance.JankStats.OnFrameListener
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -29,26 +30,20 @@ import dagger.hilt.android.components.ActivityComponent
@InstallIn(ActivityComponent::class) @InstallIn(ActivityComponent::class)
object JankStatsModule { object JankStatsModule {
@Provides @Provides
fun providesOnFrameListener(): JankStats.OnFrameListener { fun providesOnFrameListener(): OnFrameListener = OnFrameListener { frameData ->
return JankStats.OnFrameListener { frameData -> // Make sure to only log janky frames.
// Make sure to only log janky frames. if (frameData.isJank) {
if (frameData.isJank) { // We're currently logging this but would better report it to a backend.
// We're currently logging this but would better report it to a backend. Log.v("NiA Jank", frameData.toString())
Log.v("NiA Jank", frameData.toString())
}
} }
} }
@Provides @Provides
fun providesWindow(activity: Activity): Window { fun providesWindow(activity: Activity): Window = activity.window
return activity.window
}
@Provides @Provides
fun providesJankStats( fun providesJankStats(
window: Window, window: Window,
frameListener: JankStats.OnFrameListener, frameListener: OnFrameListener,
): JankStats { ): JankStats = JankStats.createAndTrack(window, frameListener)
return JankStats.createAndTrack(window, frameListener)
}
} }

@ -97,9 +97,7 @@ fun NiaApp(
) { ) {
val shouldShowGradientBackground = val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
var showSettingsDialog by rememberSaveable { var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
mutableStateOf(false)
}
NiaBackground { NiaBackground {
NiaGradientBackground( NiaGradientBackground(

@ -164,9 +164,7 @@ class NiaAppState(
} }
} }
fun navigateToSearch() { fun navigateToSearch() = navController.navigateToSearch()
navController.navigateToSearch()
}
} }
/** /**

@ -29,15 +29,11 @@ import androidx.test.uiautomator.HasChildrenOp.EXACTLY
fun untilHasChildren( fun untilHasChildren(
childCount: Int = 1, childCount: Int = 1,
op: HasChildrenOp = AT_LEAST, op: HasChildrenOp = AT_LEAST,
): UiObject2Condition<Boolean> { ): UiObject2Condition<Boolean> = object : UiObject2Condition<Boolean>() {
return object : UiObject2Condition<Boolean>() { override fun apply(element: UiObject2): Boolean = when (op) {
override fun apply(element: UiObject2): Boolean { AT_LEAST -> element.childCount >= childCount
return when (op) { EXACTLY -> element.childCount == childCount
AT_LEAST -> element.childCount >= childCount AT_MOST -> element.childCount <= childCount
EXACTLY -> element.childCount == childCount
AT_MOST -> element.childCount <= childCount
}
}
} }
} }

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.baselineprofile package com.google.samples.apps.nowinandroid.baselineprofile
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.junit4.BaselineProfileRule import androidx.benchmark.macro.junit4.BaselineProfileRule
import com.google.samples.apps.nowinandroid.PACKAGE_NAME import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.startActivityAndAllowNotifications import com.google.samples.apps.nowinandroid.startActivityAndAllowNotifications
@ -30,11 +31,9 @@ class StartupBaselineProfile {
@get:Rule val baselineProfileRule = BaselineProfileRule() @get:Rule val baselineProfileRule = BaselineProfileRule()
@Test @Test
fun generate() = fun generate() = baselineProfileRule.collect(
baselineProfileRule.collect( PACKAGE_NAME,
PACKAGE_NAME, includeInStartupProfile = true,
includeInStartupProfile = true, profileBlock = MacrobenchmarkScope::startActivityAndAllowNotifications,
) { )
startActivityAndAllowNotifications()
}
} }

@ -79,14 +79,12 @@ internal abstract class PrintApkLocationTask : DefaultTask() {
fun taskAction() { fun taskAction() {
val hasFiles = sources.orNull?.any { directory -> val hasFiles = sources.orNull?.any { directory ->
directory.asFileTree.files.any { directory.asFileTree.files.any {
it.isFile && it.parentFile.path.contains("build${File.separator}generated").not() it.isFile && "build${File.separator}generated" !in it.parentFile.path
} }
} ?: throw RuntimeException("Cannot check androidTest sources") } ?: throw RuntimeException("Cannot check androidTest sources")
// Don't print APK location if there are no androidTest source files // Don't print APK location if there are no androidTest source files
if (!hasFiles) { if (!hasFiles) return
return
}
val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get()) val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
?: throw RuntimeException("Cannot load APKs") ?: throw RuntimeException("Cannot load APKs")

@ -23,15 +23,10 @@ import kotlinx.coroutines.flow.onStart
sealed interface Result<out T> { sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T> data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable? = null) : Result<Nothing> data class Error(val exception: Throwable) : Result<Nothing>
data object Loading : Result<Nothing> data object Loading : Result<Nothing>
} }
fun <T> Flow<T>.asResult(): Flow<Result<T>> { fun <T> Flow<T>.asResult(): Flow<Result<T>> = map<T, Result<T>> { Result.Success(it) }
return this .onStart { emit(Result.Loading) }
.map<T, Result<T>> { .catch { emit(Result.Error(it)) }
Result.Success(it)
}
.onStart { emit(Result.Loading) }
.catch { emit(Result.Error(it)) }
}

@ -39,9 +39,7 @@ class DefaultRecentSearchRepository @Inject constructor(
override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> = override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> =
recentSearchQueryDao.getRecentSearchQueryEntities(limit).map { searchQueries -> recentSearchQueryDao.getRecentSearchQueryEntities(limit).map { searchQueries ->
searchQueries.map { searchQueries.map { it.asExternalModel() }
it.asExternalModel()
}
} }
override suspend fun clearRecentSearches() = recentSearchQueryDao.clearRecentSearchQueries() override suspend fun clearRecentSearches() = recentSearchQueryDao.clearRecentSearchQueries()

@ -26,10 +26,10 @@ import javax.inject.Inject
* Fake implementation of the [RecentSearchRepository] * Fake implementation of the [RecentSearchRepository]
*/ */
class FakeRecentSearchRepository @Inject constructor() : RecentSearchRepository { class FakeRecentSearchRepository @Inject constructor() : RecentSearchRepository {
override suspend fun insertOrReplaceRecentSearch(searchQuery: String) { /* no-op */ } override suspend fun insertOrReplaceRecentSearch(searchQuery: String) = Unit
override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> = override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> =
flowOf(emptyList()) flowOf(emptyList())
override suspend fun clearRecentSearches() { /* no-op */ } override suspend fun clearRecentSearches() = Unit
} }

@ -27,7 +27,7 @@ import javax.inject.Inject
*/ */
class FakeSearchContentsRepository @Inject constructor() : SearchContentsRepository { class FakeSearchContentsRepository @Inject constructor() : SearchContentsRepository {
override suspend fun populateFtsData() { /* no-op */ } override suspend fun populateFtsData() = Unit
override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf() override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf()
override fun getSearchContentsCount(): Flow<Int> = flowOf(1) override fun getSearchContentsCount(): Flow<Int> = flowOf(1)
} }

@ -55,9 +55,8 @@ class FakeTopicsRepository @Inject constructor(
) )
}.flowOn(ioDispatcher) }.flowOn(ioDispatcher)
override fun getTopic(id: String): Flow<Topic> { override fun getTopic(id: String): Flow<Topic> = getTopics()
return getTopics().map { it.first { topic -> topic.id == id } } .map { it.first { topic -> topic.id == id } }
}
override suspend fun syncWith(synchronizer: Synchronizer) = true override suspend fun syncWith(synchronizer: Synchronizer) = true
} }

@ -82,7 +82,7 @@ class CompositeUserNewsResourceRepositoryTest {
// Check that only news resources with the given topic id are returned. // Check that only news resources with the given topic id are returned.
assertEquals( assertEquals(
sampleNewsResources sampleNewsResources
.filter { it.topics.contains(sampleTopic1) } .filter { sampleTopic1 in it.topics }
.mapToUserNewsResources(emptyUserData), .mapToUserNewsResources(emptyUserData),
userNewsResources.first(), userNewsResources.first(),
) )
@ -104,7 +104,7 @@ class CompositeUserNewsResourceRepositoryTest {
// Check that only news resources with the given topic id are returned. // Check that only news resources with the given topic id are returned.
assertEquals( assertEquals(
sampleNewsResources sampleNewsResources
.filter { it.topics.contains(sampleTopic1) } .filter { sampleTopic1 in it.topics }
.mapToUserNewsResources(userData), .mapToUserNewsResources(userData),
userNewsResources.first(), userNewsResources.first(),
) )

@ -91,14 +91,14 @@ class UserNewsResourceTest {
// Construct the expected FollowableTopic. // Construct the expected FollowableTopic.
val followableTopic = FollowableTopic( val followableTopic = FollowableTopic(
topic = topic, topic = topic,
isFollowed = userData.followedTopics.contains(topic.id), isFollowed = topic.id in userData.followedTopics,
) )
assertTrue(userNewsResource.followableTopics.contains(followableTopic)) assertTrue(userNewsResource.followableTopics.contains(followableTopic))
} }
// Check that the saved flag is set correctly. // Check that the saved flag is set correctly.
assertEquals( assertEquals(
userData.bookmarkedNewsResources.contains(newsResource1.id), newsResource1.id in userData.bookmarkedNewsResources,
userNewsResource.isSaved, userNewsResource.isSaved,
) )
} }

@ -34,9 +34,7 @@ val nonPresentInterestsIds = setOf("2")
*/ */
class TestNewsResourceDao : NewsResourceDao { class TestNewsResourceDao : NewsResourceDao {
private var entitiesStateFlow = MutableStateFlow( private val entitiesStateFlow = MutableStateFlow(emptyList<NewsResourceEntity>())
emptyList<NewsResourceEntity>(),
)
internal var topicCrossReferences: List<NewsResourceTopicCrossRef> = listOf() internal var topicCrossReferences: List<NewsResourceTopicCrossRef> = listOf()
@ -131,7 +129,7 @@ class TestNewsResourceDao : NewsResourceDao {
override suspend fun deleteNewsResources(ids: List<String>) { override suspend fun deleteNewsResources(ids: List<String>) {
val idSet = ids.toSet() val idSet = ids.toSet()
entitiesStateFlow.update { entities -> entitiesStateFlow.update { entities ->
entities.filterNot { idSet.contains(it.id) } entities.filterNot { it.id in idSet }
} }
} }
} }

@ -91,11 +91,10 @@ class TestNiaNetworkDataSource : NiaNetworkDataSource {
} }
} }
fun List<NetworkChangeList>.after(version: Int?): List<NetworkChangeList> = fun List<NetworkChangeList>.after(version: Int?): List<NetworkChangeList> = when (version) {
when (version) { null -> this
null -> this else -> filter { it.changeListVersion > version }
else -> this.filter { it.changeListVersion > version } }
}
/** /**
* Return items from [this] whose id defined by [idGetter] is in [ids] if [ids] is not null * Return items from [this] whose id defined by [idGetter] is in [ids] if [ids] is not null
@ -105,7 +104,7 @@ private fun <T> List<T>.matchIds(
idGetter: (T) -> String, idGetter: (T) -> String,
) = when (ids) { ) = when (ids) {
null -> this null -> this
else -> ids.toSet().let { idSet -> this.filter { idSet.contains(idGetter(it)) } } else -> ids.toSet().let { idSet -> filter { idGetter(it) in idSet } }
} }
/** /**

@ -28,20 +28,15 @@ import kotlinx.coroutines.flow.update
*/ */
class TestTopicDao : TopicDao { class TestTopicDao : TopicDao {
private var entitiesStateFlow = MutableStateFlow( private val entitiesStateFlow = MutableStateFlow(emptyList<TopicEntity>())
emptyList<TopicEntity>(),
)
override fun getTopicEntity(topicId: String): Flow<TopicEntity> { override fun getTopicEntity(topicId: String): Flow<TopicEntity> =
throw NotImplementedError("Unused in tests") throw NotImplementedError("Unused in tests")
}
override fun getTopicEntities(): Flow<List<TopicEntity>> = override fun getTopicEntities(): Flow<List<TopicEntity>> = entitiesStateFlow
entitiesStateFlow
override fun getTopicEntities(ids: Set<String>): Flow<List<TopicEntity>> = override fun getTopicEntities(ids: Set<String>): Flow<List<TopicEntity>> =
getTopicEntities() getTopicEntities().map { topics -> topics.filter { it.id in ids } }
.map { topics -> topics.filter { it.id in ids } }
override suspend fun getOneOffTopicEntities(): List<TopicEntity> = emptyList() override suspend fun getOneOffTopicEntities(): List<TopicEntity> = emptyList()
@ -55,15 +50,11 @@ class TestTopicDao : TopicDao {
override suspend fun upsertTopics(entities: List<TopicEntity>) { override suspend fun upsertTopics(entities: List<TopicEntity>) {
// Overwrite old values with new values // Overwrite old values with new values
entitiesStateFlow.update { oldValues -> entitiesStateFlow.update { oldValues -> (entities + oldValues).distinctBy(TopicEntity::id) }
(entities + oldValues).distinctBy(TopicEntity::id)
}
} }
override suspend fun deleteTopics(ids: List<String>) { override suspend fun deleteTopics(ids: List<String>) {
val idSet = ids.toSet() val idSet = ids.toSet()
entitiesStateFlow.update { entities -> entitiesStateFlow.update { entities -> entities.filterNot { it.id in idSet } }
entities.filterNot { idSet.contains(it.id) }
}
} }
} }

@ -52,7 +52,6 @@ object ListToMapMigration : DataMigration<UserPreferences> {
hasDoneListToMapMigration = true hasDoneListToMapMigration = true
} }
override suspend fun shouldMigrate(currentData: UserPreferences): Boolean { override suspend fun shouldMigrate(currentData: UserPreferences): Boolean =
return !currentData.hasDoneListToMapMigration !currentData.hasDoneListToMapMigration
}
} }

@ -103,9 +103,7 @@ class NiaPreferencesDataSource @Inject constructor(
suspend fun setDynamicColorPreference(useDynamicColor: Boolean) { suspend fun setDynamicColorPreference(useDynamicColor: Boolean) {
userPreferences.updateData { userPreferences.updateData {
it.copy { it.copy { this.useDynamicColor = useDynamicColor }
this.useDynamicColor = useDynamicColor
}
} }
} }
@ -190,9 +188,7 @@ class NiaPreferencesDataSource @Inject constructor(
suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) { suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
userPreferences.updateData { userPreferences.updateData {
it.copy { it.copy { this.shouldHideOnboarding = shouldHideOnboarding }
this.shouldHideOnboarding = shouldHideOnboarding
}
} }
} }
} }

@ -16,7 +16,8 @@
package com.google.samples.apps.nowinandroid.core.designsystem package com.google.samples.apps.nowinandroid.core.designsystem
import android.os.Build import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
@ -219,60 +220,41 @@ class ThemeTest {
} }
@Composable @Composable
private fun dynamicLightColorSchemeWithFallback(): ColorScheme { private fun dynamicLightColorSchemeWithFallback(): ColorScheme = when {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { SDK_INT >= VERSION_CODES.S -> dynamicLightColorScheme(LocalContext.current)
dynamicLightColorScheme(LocalContext.current) else -> LightDefaultColorScheme
} else {
LightDefaultColorScheme
}
} }
@Composable @Composable
private fun dynamicDarkColorSchemeWithFallback(): ColorScheme { private fun dynamicDarkColorSchemeWithFallback(): ColorScheme = when {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { SDK_INT >= VERSION_CODES.S -> dynamicDarkColorScheme(LocalContext.current)
dynamicDarkColorScheme(LocalContext.current) else -> DarkDefaultColorScheme
} else {
DarkDefaultColorScheme
}
} }
private fun emptyGradientColors(colorScheme: ColorScheme): GradientColors { private fun emptyGradientColors(colorScheme: ColorScheme): GradientColors =
return GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp)) GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp))
}
private fun defaultGradientColors(colorScheme: ColorScheme): GradientColors { private fun defaultGradientColors(colorScheme: ColorScheme): GradientColors = GradientColors(
return GradientColors( top = colorScheme.inverseOnSurface,
top = colorScheme.inverseOnSurface, bottom = colorScheme.primaryContainer,
bottom = colorScheme.primaryContainer, container = colorScheme.surface,
container = colorScheme.surface, )
)
}
private fun dynamicGradientColorsWithFallback(colorScheme: ColorScheme): GradientColors { private fun dynamicGradientColorsWithFallback(colorScheme: ColorScheme): GradientColors = when {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { SDK_INT >= VERSION_CODES.S -> emptyGradientColors(colorScheme)
emptyGradientColors(colorScheme) else -> defaultGradientColors(colorScheme)
} else {
defaultGradientColors(colorScheme)
}
} }
private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme { private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme = BackgroundTheme(
return BackgroundTheme( color = colorScheme.surface,
color = colorScheme.surface, tonalElevation = 2.dp,
tonalElevation = 2.dp, )
)
}
private fun defaultTintTheme(): TintTheme { private fun defaultTintTheme(): TintTheme = TintTheme()
return TintTheme()
}
private fun dynamicTintThemeWithFallback(colorScheme: ColorScheme): TintTheme { private fun dynamicTintThemeWithFallback(colorScheme: ColorScheme): TintTheme = when {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { SDK_INT >= VERSION_CODES.S -> TintTheme(colorScheme.primary)
TintTheme(colorScheme.primary) else -> TintTheme()
} else {
TintTheme()
}
} }
/** /**

@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color.Companion.Unspecified
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@ -79,7 +80,7 @@ fun DynamicAsyncImage(
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
painter = if (isError.not() && !isLocalInspection) imageLoader else placeholder, painter = if (isError.not() && !isLocalInspection) imageLoader else placeholder,
contentDescription = contentDescription, contentDescription = contentDescription,
colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, colorFilter = if (iconTint != Unspecified) ColorFilter.tint(iconTint) else null,
) )
} }
} }

@ -40,9 +40,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
fun NiaTopAppBar( fun NiaTopAppBar(
@StringRes titleRes: Int, @StringRes titleRes: Int,
navigationIcon: ImageVector, navigationIcon: ImageVector,
navigationIconContentDescription: String?, navigationIconContentDescription: String,
actionIcon: ImageVector, actionIcon: ImageVector,
actionIconContentDescription: String?, actionIconContentDescription: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
onNavigationClick: () -> Unit = {}, onNavigationClick: () -> Unit = {},

@ -229,10 +229,5 @@ fun LazyStaggeredGridState.scrollbarState(
.collect { value = it } .collect { value = it }
}.value }.value
private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float { private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float =
var sum = 0f fold(0f) { acc, it -> acc + selector(it) }
for (element in this) {
sum += selector(element)
}
return sum
}

@ -25,7 +25,7 @@ import androidx.compose.ui.graphics.Color
*/ */
@Immutable @Immutable
data class TintTheme( data class TintTheme(
val iconTint: Color? = null, val iconTint: Color = Color.Unspecified,
) )
/** /**

@ -37,22 +37,20 @@ class GetFollowableTopicsUseCase @Inject constructor(
* *
* @param sortBy - the field used to sort the topics. Default NONE = no sorting. * @param sortBy - the field used to sort the topics. Default NONE = no sorting.
*/ */
operator fun invoke(sortBy: TopicSortField = NONE): Flow<List<FollowableTopic>> { operator fun invoke(sortBy: TopicSortField = NONE): Flow<List<FollowableTopic>> = combine(
return combine( userDataRepository.userData,
userDataRepository.userData, topicsRepository.getTopics(),
topicsRepository.getTopics(), ) { userData, topics ->
) { userData, topics -> val followedTopics = topics
val followedTopics = topics .map { topic ->
.map { topic -> FollowableTopic(
FollowableTopic( topic = topic,
topic = topic, isFollowed = topic.id in userData.followedTopics,
isFollowed = topic.id in userData.followedTopics, )
)
}
when (sortBy) {
NAME -> followedTopics.sortedBy { it.topic.name }
else -> followedTopics
} }
when (sortBy) {
NAME -> followedTopics.sortedBy { it.topic.name }
else -> followedTopics
} }
} }
} }

@ -45,14 +45,13 @@ data class UserNewsResource internal constructor(
followableTopics = newsResource.topics.map { topic -> followableTopics = newsResource.topics.map { topic ->
FollowableTopic( FollowableTopic(
topic = topic, topic = topic,
isFollowed = userData.followedTopics.contains(topic.id), isFollowed = topic.id in userData.followedTopics,
) )
}, },
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id), isSaved = newsResource.id in userData.bookmarkedNewsResources,
hasBeenViewed = userData.viewedNewsResources.contains(newsResource.id), hasBeenViewed = newsResource.id in userData.viewedNewsResources,
) )
} }
fun List<NewsResource>.mapToUserNewsResources(userData: UserData): List<UserNewsResource> { fun List<NewsResource>.mapToUserNewsResources(userData: UserData): List<UserNewsResource> =
return map { UserNewsResource(it, userData) } map { UserNewsResource(it, userData) }
}

@ -24,10 +24,10 @@ import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat.checkSelfPermission
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.InboxStyle import androidx.core.app.NotificationCompat.InboxStyle
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -57,30 +57,24 @@ class SystemTrayNotifier @Inject constructor(
override fun postNewsNotifications( override fun postNewsNotifications(
newsResources: List<NewsResource>, newsResources: List<NewsResource>,
) = with(context) { ) = with(context) {
if (ActivityCompat.checkSelfPermission( if (checkSelfPermission(this, permission.POST_NOTIFICATIONS) != PERMISSION_GRANTED) {
this,
permission.POST_NOTIFICATIONS,
) != PackageManager.PERMISSION_GRANTED
) {
return return
} }
val truncatedNewsResources = newsResources val truncatedNewsResources = newsResources.take(MAX_NUM_NOTIFICATIONS)
.take(MAX_NUM_NOTIFICATIONS)
val newsNotifications = truncatedNewsResources val newsNotifications = truncatedNewsResources.map { newsResource ->
.map { newsResource -> createNewsNotification {
createNewsNotification { setSmallIcon(
setSmallIcon( com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification,
com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification, )
) .setContentTitle(newsResource.title)
.setContentTitle(newsResource.title) .setContentText(newsResource.content)
.setContentText(newsResource.content) .setContentIntent(newsPendingIntent(newsResource))
.setContentIntent(newsPendingIntent(newsResource)) .setGroup(NEWS_NOTIFICATION_GROUP)
.setGroup(NEWS_NOTIFICATION_GROUP) .setAutoCancel(true)
.setAutoCancel(true)
}
} }
}
val summaryNotification = createNewsNotification { val summaryNotification = createNewsNotification {
val title = getString( val title = getString(
R.string.news_notification_group_summary, R.string.news_notification_group_summary,
@ -117,9 +111,7 @@ class SystemTrayNotifier @Inject constructor(
newsResources: List<NewsResource>, newsResources: List<NewsResource>,
title: String, title: String,
): InboxStyle = newsResources ): InboxStyle = newsResources
.fold(InboxStyle()) { inboxStyle, newsResource -> .fold(InboxStyle()) { inboxStyle, newsResource -> inboxStyle.addLine(newsResource.title) }
inboxStyle.addLine(newsResource.title)
}
.setBigContentTitle(title) .setBigContentTitle(title)
.setSummaryText(title) .setSummaryText(title)
} }

@ -25,7 +25,6 @@ import dagger.hilt.android.testing.HiltTestApplication
* A custom runner to set up the instrumented application class for tests. * A custom runner to set up the instrumented application class for tests.
*/ */
class NiaTestRunner : AndroidJUnitRunner() { class NiaTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { override fun newApplication(cl: ClassLoader, name: String, context: Context): Application =
return super.newApplication(cl, HiltTestApplication::class.java.name, context) super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
} }

@ -43,9 +43,7 @@ class TestNewsRepository : NewsRepository {
} }
} }
query.filterNewsIds?.let { filterNewsIds -> query.filterNewsIds?.let { filterNewsIds ->
result = newsResources.filter { result = newsResources.filter { it.id in filterNewsIds }
filterNewsIds.contains(it.id)
}
} }
result result
} }

@ -32,7 +32,5 @@ class TestRecentSearchRepository : RecentSearchRepository {
cachedRecentSearches.add(RecentSearchQuery(searchQuery)) cachedRecentSearches.add(RecentSearchQuery(searchQuery))
} }
override suspend fun clearRecentSearches() { override suspend fun clearRecentSearches() = cachedRecentSearches.clear()
cachedRecentSearches.clear()
}
} }

@ -29,18 +29,15 @@ class TestSearchContentsRepository : SearchContentsRepository {
private val cachedTopics: MutableList<Topic> = mutableListOf() private val cachedTopics: MutableList<Topic> = mutableListOf()
private val cachedNewsResources: MutableList<NewsResource> = mutableListOf() private val cachedNewsResources: MutableList<NewsResource> = mutableListOf()
override suspend fun populateFtsData() { /* no-op */ } override suspend fun populateFtsData() = Unit
override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf( override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf(
SearchResult( SearchResult(
topics = cachedTopics.filter { topics = cachedTopics.filter {
it.name.contains(searchQuery) || searchQuery in it.name || searchQuery in it.shortDescription || searchQuery in it.longDescription
it.shortDescription.contains(searchQuery) ||
it.longDescription.contains(searchQuery)
}, },
newsResources = cachedNewsResources.filter { newsResources = cachedNewsResources.filter {
it.content.contains(searchQuery) || searchQuery in it.content || searchQuery in it.title
it.title.contains(searchQuery)
}, },
), ),
) )

@ -33,9 +33,8 @@ class TestTopicsRepository : TopicsRepository {
override fun getTopics(): Flow<List<Topic>> = topicsFlow override fun getTopics(): Flow<List<Topic>> = topicsFlow
override fun getTopic(id: String): Flow<Topic> { override fun getTopic(id: String): Flow<Topic> =
return topicsFlow.map { topics -> topics.find { it.id == id }!! } topicsFlow.map { topics -> topics.find { it.id == id }!! }
}
/** /**
* A test-only API to allow controlling the list of topics from tests. * A test-only API to allow controlling the list of topics from tests.

@ -32,11 +32,7 @@ import org.junit.runner.Description
class MainDispatcherRule( class MainDispatcherRule(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() { ) : TestWatcher() {
override fun starting(description: Description) { override fun starting(description: Description) = Dispatchers.setMain(testDispatcher)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) { override fun finished(description: Description) = Dispatchers.resetMain()
Dispatchers.resetMain()
}
} }

@ -26,5 +26,5 @@ class TestAnalyticsHelper : AnalyticsHelper {
events.add(event) events.add(event)
} }
fun hasLogged(event: AnalyticsEvent) = events.contains(event) fun hasLogged(event: AnalyticsEvent) = event in events
} }

@ -26,9 +26,7 @@ class TestSyncManager : SyncManager {
override val isSyncing: Flow<Boolean> = syncStatusFlow override val isSyncing: Flow<Boolean> = syncStatusFlow
override fun requestSync() { override fun requestSync(): Unit = TODO("Not yet implemented")
TODO("Not yet implemented")
}
/** /**
* A test-only API to set the sync status from tests. * A test-only API to set the sync status from tests.

@ -50,7 +50,7 @@ fun rememberMetricsStateHolder(): Holder {
*/ */
@Composable @Composable
fun TrackJank( fun TrackJank(
vararg keys: Any?, vararg keys: Any,
reportMetric: suspend CoroutineScope.(state: Holder) -> Unit, reportMetric: suspend CoroutineScope.(state: Holder) -> Unit,
) { ) {
val metrics = rememberMetricsStateHolder() val metrics = rememberMetricsStateHolder()
@ -65,7 +65,7 @@ fun TrackJank(
*/ */
@Composable @Composable
fun TrackDisposableJank( fun TrackDisposableJank(
vararg keys: Any?, vararg keys: Any,
reportMetric: DisposableEffectScope.(state: Holder) -> DisposableEffectResult, reportMetric: DisposableEffectScope.(state: Holder) -> DisposableEffectResult,
) { ) {
val metrics = rememberMetricsStateHolder() val metrics = rememberMetricsStateHolder()

@ -64,9 +64,7 @@ fun LazyStaggeredGridScope.newsFeed(
key = { it.id }, key = { it.id },
contentType = { "newsFeedItem" }, contentType = { "newsFeedItem" },
) { userNewsResource -> ) { userNewsResource ->
val resourceUrl by remember { val resourceUrl by remember { mutableStateOf(Uri.parse(userNewsResource.url)) }
mutableStateOf(Uri.parse(userNewsResource.url))
}
val context = LocalContext.current val context = LocalContext.current
val analyticsHelper = LocalAnalyticsHelper.current val analyticsHelper = LocalAnalyticsHelper.current
val backgroundColor = MaterialTheme.colorScheme.background.toArgb() val backgroundColor = MaterialTheme.colorScheme.background.toArgb()

@ -47,6 +47,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
@ -237,7 +238,7 @@ private fun EmptyState(modifier: Modifier = Modifier) {
Image( Image(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.img_empty_bookmarks), painter = painterResource(id = R.drawable.img_empty_bookmarks),
colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null, colorFilter = if (iconTint != Color.Unspecified) ColorFilter.tint(iconTint) else null,
contentDescription = null, contentDescription = null,
) )

@ -24,7 +24,7 @@ import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute
const val bookmarksRoute = "bookmarks_route" const val bookmarksRoute = "bookmarks_route"
fun NavController.navigateToBookmarks(navOptions: NavOptions? = null) { fun NavController.navigateToBookmarks(navOptions: NavOptions) {
this.navigate(bookmarksRoute, navOptions) this.navigate(bookmarksRoute, navOptions)
} }

@ -30,7 +30,7 @@ const val forYouNavigationRoute = "for_you_route/{$LINKED_NEWS_RESOURCE_ID}"
private const val DEEP_LINK_URI_PATTERN = private const val DEEP_LINK_URI_PATTERN =
"https://www.nowinandroid.apps.samples.google.com/foryou/{$LINKED_NEWS_RESOURCE_ID}" "https://www.nowinandroid.apps.samples.google.com/foryou/{$LINKED_NEWS_RESOURCE_ID}"
fun NavController.navigateToForYou(navOptions: NavOptions? = null) { fun NavController.navigateToForYou(navOptions: NavOptions) {
this.navigate(forYouNavigationRoute, navOptions) this.navigate(forYouNavigationRoute, navOptions)
} }

@ -26,7 +26,7 @@ import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
private const val INTERESTS_GRAPH_ROUTE_PATTERN = "interests_graph" private const val INTERESTS_GRAPH_ROUTE_PATTERN = "interests_graph"
const val interestsRoute = "interests_route" const val interestsRoute = "interests_route"
fun NavController.navigateToInterestsGraph(navOptions: NavOptions? = null) { fun NavController.navigateToInterestsGraph(navOptions: NavOptions) {
this.navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions) this.navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions)
} }

@ -264,9 +264,7 @@ fun EmptySearchResultBody(
) { offset -> ) { offset ->
tryAnotherSearchString.getStringAnnotations(start = offset, end = offset) tryAnotherSearchString.getStringAnnotations(start = offset, end = offset)
.firstOrNull() .firstOrNull()
?.let { ?.let { onInterestsClick() }
onInterestsClick()
}
} }
} }
} }
@ -519,9 +517,7 @@ private fun SearchTextField(
} }
}, },
onValueChange = { onValueChange = {
if (!it.contains("\n")) { if ("\n" !in it) onSearchQueryChanged(it)
onSearchQueryChanged(it)
}
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

@ -117,22 +117,16 @@ private fun topicUiState(
when (followedTopicToTopicResult) { when (followedTopicToTopicResult) {
is Result.Success -> { is Result.Success -> {
val (followedTopics, topic) = followedTopicToTopicResult.data val (followedTopics, topic) = followedTopicToTopicResult.data
val followed = followedTopics.contains(topicId)
TopicUiState.Success( TopicUiState.Success(
followableTopic = FollowableTopic( followableTopic = FollowableTopic(
topic = topic, topic = topic,
isFollowed = followed, isFollowed = topicId in followedTopics,
), ),
) )
} }
is Result.Loading -> { is Result.Loading -> TopicUiState.Loading
TopicUiState.Loading is Result.Error -> TopicUiState.Error
}
is Result.Error -> {
TopicUiState.Error
}
} }
} }
} }
@ -151,26 +145,13 @@ private fun newsUiState(
val bookmark: Flow<Set<String>> = userDataRepository.userData val bookmark: Flow<Set<String>> = userDataRepository.userData
.map { it.bookmarkedNewsResources } .map { it.bookmarkedNewsResources }
return combine( return combine(newsStream, bookmark, ::Pair)
newsStream,
bookmark,
::Pair,
)
.asResult() .asResult()
.map { newsToBookmarksResult -> .map { newsToBookmarksResult ->
when (newsToBookmarksResult) { when (newsToBookmarksResult) {
is Result.Success -> { is Result.Success -> NewsUiState.Success(newsToBookmarksResult.data.first)
val news = newsToBookmarksResult.data.first is Result.Loading -> NewsUiState.Loading
NewsUiState.Success(news) is Result.Error -> NewsUiState.Error
}
is Result.Loading -> {
NewsUiState.Loading
}
is Result.Error -> {
NewsUiState.Error
}
} }
} }
} }

@ -40,7 +40,7 @@ internal class TopicArgs(val topicId: String) {
fun NavController.navigateToTopic(topicId: String) { fun NavController.navigateToTopic(topicId: String) {
val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING) val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING)
this.navigate("topic_route/$encodedId") { navigate("topic_route/$encodedId") {
launchSingleTop = true launchSingleTop = true
} }
} }

@ -34,15 +34,13 @@ import org.jetbrains.uast.UQualifiedReferenceExpression
*/ */
class DesignSystemDetector : Detector(), Detector.UastScanner { class DesignSystemDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>> { override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(
return listOf( UCallExpression::class.java,
UCallExpression::class.java, UQualifiedReferenceExpression::class.java,
UQualifiedReferenceExpression::class.java, )
)
}
override fun createUastHandler(context: JavaContext): UElementHandler { override fun createUastHandler(context: JavaContext): UElementHandler =
return object : UElementHandler() { object : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) { override fun visitCallExpression(node: UCallExpression) {
val name = node.methodName ?: return val name = node.methodName ?: return
val preferredName = METHOD_NAMES[name] ?: return val preferredName = METHOD_NAMES[name] ?: return
@ -55,7 +53,6 @@ class DesignSystemDetector : Detector(), Detector.UastScanner {
reportIssue(context, node, name, preferredName) reportIssue(context, node, name, preferredName)
} }
} }
}
companion object { companion object {
@JvmField @JvmField

Loading…
Cancel
Save