Merge pull request #146 from android/jun20merge

Internal > Public merge June 20th
pull/134/head
Jolanda Verhoef 2 years ago committed by GitHub
commit 1ac9cc0715
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -34,14 +34,7 @@
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="99" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="99" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="99" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="99" />
<option name="IMPORT_NESTED_CLASSES" value="true" /> <option name="IMPORT_NESTED_CLASSES" value="true" />
<option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="false" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="false" />
<option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="false" />
<option name="CONTINUATION_INDENT_FOR_CHAINED_CALLS" value="false" />
<option name="CONTINUATION_INDENT_IN_SUPERTYPE_LISTS" value="false" />
<option name="CONTINUATION_INDENT_IN_IF_CONDITIONS" value="false" />
<option name="WRAP_EXPRESSION_BODY_FUNCTIONS" value="1" />
<option name="IF_RPAREN_ON_NEW_LINE" value="true" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<Properties> <Properties>
<option name="KEEP_BLANK_LINES" value="true" /> <option name="KEEP_BLANK_LINES" value="true" />
@ -307,19 +300,11 @@
</arrangement> </arrangement>
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="kotlin"> <codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" /> <option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" /> <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="CALL_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="CALL_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<option name="FIELD_ANNOTATION_WRAP" value="1" /> <option name="FIELD_ANNOTATION_WRAP" value="1" />
<option name="PARAMETER_ANNOTATION_WRAP" value="1" /> <option name="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" /> <option name="VARIABLE_ANNOTATION_WRAP" value="1" />

@ -129,7 +129,7 @@ dependencies {
resolutionStrategy { resolutionStrategy {
force(libs.junit4) force(libs.junit4)
// Temporary workaround for https://issuetracker.google.com/174733673 // Temporary workaround for https://issuetracker.google.com/174733673
force("org.objenesis:objenesis:3.2") force("org.objenesis:objenesis:2.6")
} }
} }
} }

@ -20,9 +20,11 @@ import com.google.samples.apps.nowinandroid.core.data.di.DataModule
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeAuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeAuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeNewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeNewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeTopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeTopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.fake.FakeUserDataRepository
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
@ -48,4 +50,9 @@ interface TestDataModule {
fun bindsNewsResourceRepository( fun bindsNewsResourceRepository(
fakeNewsRepository: FakeNewsRepository fakeNewsRepository: FakeNewsRepository
): NewsRepository ): NewsRepository
@Binds
fun bindsUserDataRepository(
userDataRepository: FakeUserDataRepository
): UserDataRepository
} }

@ -21,7 +21,9 @@ import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstAuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstAuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstNewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstNewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstTopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstTopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstUserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -45,4 +47,9 @@ interface DataModule {
fun bindsNewsResourceRepository( fun bindsNewsResourceRepository(
newsRepository: OfflineFirstNewsRepository newsRepository: OfflineFirstNewsRepository
): NewsRepository ): NewsRepository
@Binds
fun bindsUserDataRepository(
userDataRepository: OfflineFirstUserDataRepository
): UserDataRepository
} }

@ -30,19 +30,4 @@ interface AuthorsRepository : Syncable {
* Gets data for a specific author * Gets data for a specific author
*/ */
fun getAuthorStream(id: String): Flow<Author> fun getAuthorStream(id: String): Flow<Author>
/**
* Sets the user's currently followed authors
*/
suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>)
/**
* Toggles the user's newly followed/unfollowed author
*/
suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean)
/**
* Returns the users currently followed authors
*/
fun getFollowedAuthorIdsStream(): Flow<Set<String>>
} }

@ -23,7 +23,6 @@ import com.google.samples.apps.nowinandroid.core.database.dao.AuthorDao
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
@ -38,7 +37,6 @@ import kotlinx.coroutines.flow.map
class OfflineFirstAuthorsRepository @Inject constructor( class OfflineFirstAuthorsRepository @Inject constructor(
private val authorDao: AuthorDao, private val authorDao: AuthorDao,
private val network: NiaNetworkDataSource, private val network: NiaNetworkDataSource,
private val niaPreferences: NiaPreferencesDataSource,
) : AuthorsRepository { ) : AuthorsRepository {
override fun getAuthorStream(id: String): Flow<Author> = override fun getAuthorStream(id: String): Flow<Author> =
@ -50,14 +48,6 @@ class OfflineFirstAuthorsRepository @Inject constructor(
authorDao.getAuthorEntitiesStream() authorDao.getAuthorEntitiesStream()
.map { it.map(AuthorEntity::asExternalModel) } .map { it.map(AuthorEntity::asExternalModel) }
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) =
niaPreferences.setFollowedAuthorIds(followedAuthorIds)
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) =
niaPreferences.toggleFollowedAuthorId(followedAuthorId, followed)
override fun getFollowedAuthorIdsStream(): Flow<Set<String>> = niaPreferences.followedAuthorIds
override suspend fun syncWith(synchronizer: Synchronizer): Boolean = override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
synchronizer.changeListSync( synchronizer.changeListSync(
versionReader = ChangeListVersions::authorVersion, versionReader = ChangeListVersions::authorVersion,

@ -23,7 +23,6 @@ import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
@ -38,7 +37,6 @@ import kotlinx.coroutines.flow.map
class OfflineFirstTopicsRepository @Inject constructor( class OfflineFirstTopicsRepository @Inject constructor(
private val topicDao: TopicDao, private val topicDao: TopicDao,
private val network: NiaNetworkDataSource, private val network: NiaNetworkDataSource,
private val niaPreferences: NiaPreferencesDataSource,
) : TopicsRepository { ) : TopicsRepository {
override fun getTopicsStream(): Flow<List<Topic>> = override fun getTopicsStream(): Flow<List<Topic>> =
@ -48,14 +46,6 @@ class OfflineFirstTopicsRepository @Inject constructor(
override fun getTopic(id: String): Flow<Topic> = override fun getTopic(id: String): Flow<Topic> =
topicDao.getTopicEntity(id).map { it.asExternalModel() } topicDao.getTopicEntity(id).map { it.asExternalModel() }
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
niaPreferences.setFollowedTopicIds(followedTopicIds)
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) =
niaPreferences.toggleFollowedTopicId(followedTopicId, followed)
override fun getFollowedTopicIdsStream() = niaPreferences.followedTopicIds
override suspend fun syncWith(synchronizer: Synchronizer): Boolean = override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
synchronizer.changeListSync( synchronizer.changeListSync(
versionReader = ChangeListVersions::topicVersion, versionReader = ChangeListVersions::topicVersion,

@ -0,0 +1,42 @@
/*
* 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.data.repository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
class OfflineFirstUserDataRepository @Inject constructor(
private val niaPreferencesDataSource: NiaPreferencesDataSource
) : UserDataRepository {
override val userDataStream: Flow<UserData> =
niaPreferencesDataSource.userDataStream
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
niaPreferencesDataSource.setFollowedTopicIds(followedTopicIds)
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) =
niaPreferencesDataSource.toggleFollowedTopicId(followedTopicId, followed)
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) =
niaPreferencesDataSource.setFollowedAuthorIds(followedAuthorIds)
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) =
niaPreferencesDataSource.toggleFollowedAuthorId(followedAuthorId, followed)
}

@ -30,19 +30,4 @@ interface TopicsRepository : Syncable {
* Gets data for a specific topic * Gets data for a specific topic
*/ */
fun getTopic(id: String): Flow<Topic> fun getTopic(id: String): Flow<Topic>
/**
* Sets the user's currently followed topics
*/
suspend fun setFollowedTopicIds(followedTopicIds: Set<String>)
/**
* Toggles the user's newly followed/unfollowed topic
*/
suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean)
/**
* Returns the users currently followed topics
*/
fun getFollowedTopicIdsStream(): Flow<Set<String>>
} }

@ -0,0 +1,48 @@
/*
* 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.data.repository
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.coroutines.flow.Flow
interface UserDataRepository {
/**
* Stream of [UserData]
*/
val userDataStream: Flow<UserData>
/**
* Sets the user's currently followed topics
*/
suspend fun setFollowedTopicIds(followedTopicIds: Set<String>)
/**
* Toggles the user's newly followed/unfollowed topic
*/
suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean)
/**
* Sets the user's currently followed authors
*/
suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>)
/**
* Toggles the user's newly followed/unfollowed author
*/
suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean)
}

@ -18,7 +18,6 @@ package com.google.samples.apps.nowinandroid.core.data.repository.fake
import com.google.samples.apps.nowinandroid.core.data.Synchronizer import com.google.samples.apps.nowinandroid.core.data.Synchronizer
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
@ -41,7 +40,6 @@ import kotlinx.serialization.json.Json
*/ */
class FakeAuthorsRepository @Inject constructor( class FakeAuthorsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val niaPreferences: NiaPreferencesDataSource,
private val networkJson: Json, private val networkJson: Json,
) : AuthorsRepository { ) : AuthorsRepository {
@ -65,15 +63,5 @@ class FakeAuthorsRepository @Inject constructor(
return getAuthorsStream().map { it.first { author -> author.id == id } } return getAuthorsStream().map { it.first { author -> author.id == id } }
} }
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) {
niaPreferences.setFollowedAuthorIds(followedAuthorIds)
}
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) {
niaPreferences.toggleFollowedAuthorId(followedAuthorId, followed)
}
override fun getFollowedAuthorIdsStream(): Flow<Set<String>> = niaPreferences.followedAuthorIds
override suspend fun syncWith(synchronizer: Synchronizer) = true override suspend fun syncWith(synchronizer: Synchronizer) = true
} }

@ -18,7 +18,6 @@ package com.google.samples.apps.nowinandroid.core.data.repository.fake
import com.google.samples.apps.nowinandroid.core.data.Synchronizer import com.google.samples.apps.nowinandroid.core.data.Synchronizer
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.Dispatcher import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
@ -43,7 +42,6 @@ import kotlinx.serialization.json.Json
class FakeTopicsRepository @Inject constructor( class FakeTopicsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val networkJson: Json, private val networkJson: Json,
private val niaPreferences: NiaPreferencesDataSource
) : TopicsRepository { ) : TopicsRepository {
override fun getTopicsStream(): Flow<List<Topic>> = flow<List<Topic>> { override fun getTopicsStream(): Flow<List<Topic>> = flow<List<Topic>> {
emit( emit(
@ -65,13 +63,5 @@ class FakeTopicsRepository @Inject constructor(
return getTopicsStream().map { it.first { topic -> topic.id == id } } return getTopicsStream().map { it.first { topic -> topic.id == id } }
} }
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
niaPreferences.setFollowedTopicIds(followedTopicIds)
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) =
niaPreferences.toggleFollowedTopicId(followedTopicId, followed)
override fun getFollowedTopicIdsStream() = niaPreferences.followedTopicIds
override suspend fun syncWith(synchronizer: Synchronizer) = true override suspend fun syncWith(synchronizer: Synchronizer) = true
} }

@ -0,0 +1,52 @@
/*
* 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.data.repository.fake
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
/**
* Fake implementation of the [AuthorsRepository] that returns hardcoded authors.
*
* This allows us to run the app with fake data, without needing an internet connection or working
* backend.
*/
class FakeUserDataRepository @Inject constructor(
private val niaPreferencesDataSource: NiaPreferencesDataSource,
) : UserDataRepository {
override val userDataStream: Flow<UserData> =
niaPreferencesDataSource.userDataStream
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
niaPreferencesDataSource.setFollowedTopicIds(followedTopicIds)
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) =
niaPreferencesDataSource.toggleFollowedTopicId(followedTopicId, followed)
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) {
niaPreferencesDataSource.setFollowedAuthorIds(followedAuthorIds)
}
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) {
niaPreferencesDataSource.toggleFollowedAuthorId(followedAuthorId, followed)
}
}

@ -62,7 +62,6 @@ class OfflineFirstAuthorsRepositoryTest {
subject = OfflineFirstAuthorsRepository( subject = OfflineFirstAuthorsRepository(
authorDao = authorDao, authorDao = authorDao,
network = network, network = network,
niaPreferences = niaPreferencesDataSource,
) )
} }

@ -62,8 +62,7 @@ class OfflineFirstTopicsRepositoryTest {
subject = OfflineFirstTopicsRepository( subject = OfflineFirstTopicsRepository(
topicDao = topicDao, topicDao = topicDao,
network = network, network = network
niaPreferences = niaPreferences
) )
} }
@ -79,17 +78,6 @@ class OfflineFirstTopicsRepositoryTest {
) )
} }
@Test
fun offlineFirstTopicsRepository_news_resources_for_interests_is_backed_by_news_resource_dao() =
runTest {
Assert.assertEquals(
niaPreferences.followedTopicIds
.first(),
subject.getFollowedTopicIdsStream()
.first()
)
}
@Test @Test
fun offlineFirstTopicsRepository_sync_pulls_from_network() = fun offlineFirstTopicsRepository_sync_pulls_from_network() =
runTest { runTest {
@ -183,50 +171,4 @@ class OfflineFirstTopicsRepositoryTest {
synchronizer.getChangeListVersions().topicVersion synchronizer.getChangeListVersions().topicVersion
) )
} }
@Test
fun offlineFirstTopicsRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() =
runTest {
subject.toggleFollowedTopicId(followedTopicId = "0", followed = true)
Assert.assertEquals(
setOf("0"),
subject.getFollowedTopicIdsStream()
.first()
)
subject.toggleFollowedTopicId(followedTopicId = "1", followed = true)
Assert.assertEquals(
setOf("0", "1"),
subject.getFollowedTopicIdsStream()
.first()
)
Assert.assertEquals(
niaPreferences.followedTopicIds
.first(),
subject.getFollowedTopicIdsStream()
.first()
)
}
@Test
fun offlineFirstTopicsRepository_set_followed_topics_logic_delegates_to_nia_preferences() =
runTest {
subject.setFollowedTopicIds(followedTopicIds = setOf("1", "2"))
Assert.assertEquals(
setOf("1", "2"),
subject.getFollowedTopicIdsStream()
.first()
)
Assert.assertEquals(
niaPreferences.followedTopicIds
.first(),
subject.getFollowedTopicIdsStream()
.first()
)
}
} }

@ -0,0 +1,99 @@
/*
* 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.data.repository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class OfflineFirstUserDataRepositoryTest {
private lateinit var subject: OfflineFirstUserDataRepository
private lateinit var niaPreferencesDataSource: NiaPreferencesDataSource
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before
fun setup() {
niaPreferencesDataSource = NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore()
)
subject = OfflineFirstUserDataRepository(
niaPreferencesDataSource = niaPreferencesDataSource
)
}
@Test
fun offlineFirstTopicsRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() =
runTest {
subject.toggleFollowedTopicId(followedTopicId = "0", followed = true)
assertEquals(
setOf("0"),
subject.userDataStream
.map { it.followedTopics }
.first()
)
subject.toggleFollowedTopicId(followedTopicId = "1", followed = true)
assertEquals(
setOf("0", "1"),
subject.userDataStream
.map { it.followedTopics }
.first()
)
assertEquals(
niaPreferencesDataSource.followedTopicIds
.first(),
subject.userDataStream
.map { it.followedTopics }
.first()
)
}
@Test
fun offlineFirstTopicsRepository_set_followed_topics_logic_delegates_to_nia_preferences() =
runTest {
subject.setFollowedTopicIds(followedTopicIds = setOf("1", "2"))
assertEquals(
setOf("1", "2"),
subject.userDataStream
.map { it.followedTopics }
.first()
)
assertEquals(
niaPreferencesDataSource.followedTopicIds
.first(),
subject.userDataStream
.map { it.followedTopics }
.first()
)
}
}

@ -57,6 +57,7 @@ protobuf {
dependencies { dependencies {
implementation(project(":core-common")) implementation(project(":core-common"))
implementation(project(":core-model"))
testImplementation(project(":core-testing")) testImplementation(project(":core-testing"))

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.core.datastore
import android.util.Log import android.util.Log
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -143,4 +144,13 @@ class NiaPreferencesDataSource @Inject constructor(
true true
} }
.map { it.followedAuthorIdsList.toSet() } .map { it.followedAuthorIdsList.toSet() }
val userDataStream = userPreferences.data
.map {
UserData(
bookmarkedNewsResources = emptySet(),
followedTopics = it.followedTopicIdsList.toSet(),
followedAuthors = it.followedAuthorIdsList.toSet(),
)
}
} }

@ -0,0 +1,26 @@
/*
* 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.model.data
/**
* Class summarizing user interest data
*/
data class UserData(
val bookmarkedNewsResources: Set<String>,
val followedTopics: Set<String>,
val followedAuthors: Set<String>,
)

@ -25,12 +25,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class TestAuthorsRepository : AuthorsRepository { class TestAuthorsRepository : AuthorsRepository {
/**
* The backing hot flow for the list of followed author ids for testing.
*/
private val _followedAuthorIds: MutableSharedFlow<Set<String>> =
MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
/** /**
* The backing hot flow for the list of author ids for testing. * The backing hot flow for the list of author ids for testing.
*/ */
@ -43,21 +37,6 @@ class TestAuthorsRepository : AuthorsRepository {
return authorsFlow.map { authors -> authors.find { it.id == id }!! } return authorsFlow.map { authors -> authors.find { it.id == id }!! }
} }
override fun getFollowedAuthorIdsStream(): Flow<Set<String>> = _followedAuthorIds
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) {
_followedAuthorIds.tryEmit(followedAuthorIds)
}
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) {
getCurrentFollowedAuthors()?.let { current ->
_followedAuthorIds.tryEmit(
if (followed) current.plus(followedAuthorId)
else current.minus(followedAuthorId)
)
}
}
override suspend fun syncWith(synchronizer: Synchronizer) = true override suspend fun syncWith(synchronizer: Synchronizer) = true
/** /**
@ -66,9 +45,4 @@ class TestAuthorsRepository : AuthorsRepository {
fun sendAuthors(authors: List<Author>) { fun sendAuthors(authors: List<Author>) {
authorsFlow.tryEmit(authors) authorsFlow.tryEmit(authors)
} }
/**
* A test-only API to allow querying the current followed authors.
*/
fun getCurrentFollowedAuthors(): Set<String>? = _followedAuthorIds.replayCache.firstOrNull()
} }

@ -25,12 +25,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class TestTopicsRepository : TopicsRepository { class TestTopicsRepository : TopicsRepository {
/**
* The backing hot flow for the list of followed topic ids for testing.
*/
private val _followedTopicIds: MutableSharedFlow<Set<String>> =
MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
/** /**
* The backing hot flow for the list of topics ids for testing. * The backing hot flow for the list of topics ids for testing.
*/ */
@ -43,21 +37,6 @@ class TestTopicsRepository : TopicsRepository {
return topicsFlow.map { topics -> topics.find { it.id == id }!! } return topicsFlow.map { topics -> topics.find { it.id == id }!! }
} }
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) {
_followedTopicIds.tryEmit(followedTopicIds)
}
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) {
getCurrentFollowedTopics()?.let { current ->
_followedTopicIds.tryEmit(
if (followed) current.plus(followedTopicId)
else current.minus(followedTopicId)
)
}
}
override fun getFollowedTopicIdsStream(): Flow<Set<String>> = _followedTopicIds
/** /**
* 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.
*/ */
@ -65,10 +44,5 @@ class TestTopicsRepository : TopicsRepository {
topicsFlow.tryEmit(topics) topicsFlow.tryEmit(topics)
} }
/**
* A test-only API to allow querying the current followed topics.
*/
fun getCurrentFollowedTopics(): Set<String>? = _followedTopicIds.replayCache.firstOrNull()
override suspend fun syncWith(synchronizer: Synchronizer) = true override suspend fun syncWith(synchronizer: Synchronizer) = true
} }

@ -0,0 +1,79 @@
/*
* 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.testing.repository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterNotNull
private val emptyUserData = UserData(
bookmarkedNewsResources = emptySet(),
followedTopics = emptySet(),
followedAuthors = emptySet()
)
class TestUserDataRepository : UserDataRepository {
/**
* The backing hot flow for the list of followed topic ids for testing.
*/
private val _userData = MutableSharedFlow<UserData>(replay = 1, onBufferOverflow = DROP_OLDEST)
private val currentUserData get() = _userData.replayCache.firstOrNull() ?: emptyUserData
override val userDataStream: Flow<UserData> = _userData.filterNotNull()
override suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) {
_userData.tryEmit(currentUserData.copy(followedTopics = followedTopicIds))
}
override suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) {
currentUserData.let { current ->
val followedTopics = if (followed) current.followedTopics + followedTopicId
else current.followedTopics - followedTopicId
_userData.tryEmit(current.copy(followedTopics = followedTopics))
}
}
override suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) {
_userData.tryEmit(currentUserData.copy(followedAuthors = followedAuthorIds))
}
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) {
currentUserData.let { current ->
val followedAuthors = if (followed) current.followedAuthors + followedAuthorId
else current.followedAuthors - followedAuthorId
_userData.tryEmit(current.copy(followedAuthors = followedAuthors))
}
}
/**
* A test-only API to allow querying the current followed topics.
*/
fun getCurrentFollowedTopics(): Set<String>? =
_userData.replayCache.firstOrNull()?.followedTopics
/**
* A test-only API to allow querying the current followed authors.
*/
fun getCurrentFollowedAuthors(): Set<String>? =
_userData.replayCache.firstOrNull()?.followedAuthors
}

@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.Author import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
@ -33,13 +34,15 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class AuthorViewModel @Inject constructor( class AuthorViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val authorsRepository: AuthorsRepository, private val userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
newsRepository: NewsRepository newsRepository: NewsRepository
) : ViewModel() { ) : ViewModel() {
@ -49,7 +52,9 @@ class AuthorViewModel @Inject constructor(
// Observe the followed authors, as they could change over time. // Observe the followed authors, as they could change over time.
private val followedAuthorIdsStream: Flow<Result<Set<String>>> = private val followedAuthorIdsStream: Flow<Result<Set<String>>> =
authorsRepository.getFollowedAuthorIdsStream().asResult() userDataRepository.userDataStream
.map { it.followedAuthors }
.asResult()
// Observe author information // Observe author information
private val author: Flow<Result<Author>> = authorsRepository.getAuthorStream( private val author: Flow<Result<Author>> = authorsRepository.getAuthorStream(
@ -102,7 +107,7 @@ class AuthorViewModel @Inject constructor(
fun followAuthorToggle(followed: Boolean) { fun followAuthorToggle(followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
authorsRepository.toggleFollowedAuthorId(authorId, followed) userDataRepository.toggleFollowedAuthorId(authorId, followed)
} }
} }
} }

@ -24,6 +24,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository 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.TestDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorDestination
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -40,6 +41,7 @@ class AuthorViewModelTest {
@get:Rule @get:Rule
val dispatcherRule = TestDispatcherRule() val dispatcherRule = TestDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val authorsRepository = TestAuthorsRepository() private val authorsRepository = TestAuthorsRepository()
private val newsRepository = TestNewsRepository() private val newsRepository = TestNewsRepository()
private lateinit var viewModel: AuthorViewModel private lateinit var viewModel: AuthorViewModel
@ -52,6 +54,7 @@ class AuthorViewModelTest {
AuthorDestination.authorIdArg to testInputAuthors[0].author.id AuthorDestination.authorIdArg to testInputAuthors[0].author.id
) )
), ),
userDataRepository = userDataRepository,
authorsRepository = authorsRepository, authorsRepository = authorsRepository,
newsRepository = newsRepository newsRepository = newsRepository
) )
@ -63,7 +66,7 @@ class AuthorViewModelTest {
awaitItem() awaitItem()
// To make sure AuthorUiState is success // To make sure AuthorUiState is success
authorsRepository.sendAuthors(testInputAuthors.map(FollowableAuthor::author)) authorsRepository.sendAuthors(testInputAuthors.map(FollowableAuthor::author))
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
val item = awaitItem() val item = awaitItem()
assertTrue(item.authorState is AuthorUiState.Success) assertTrue(item.authorState is AuthorUiState.Success)
@ -95,7 +98,7 @@ class AuthorViewModelTest {
@Test @Test
fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorLoading_thenShowLoading() = runTest { fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorLoading_thenShowLoading() = runTest {
viewModel.uiState.test { viewModel.uiState.test {
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
assertEquals(AuthorUiState.Loading, awaitItem().authorState) assertEquals(AuthorUiState.Loading, awaitItem().authorState)
} }
} }
@ -106,7 +109,7 @@ class AuthorViewModelTest {
viewModel.uiState.test { viewModel.uiState.test {
awaitItem() awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author }) authorsRepository.sendAuthors(testInputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
val item = awaitItem() val item = awaitItem()
assertTrue(item.authorState is AuthorUiState.Success) assertTrue(item.authorState is AuthorUiState.Success)
assertTrue(item.newsState is NewsUiState.Loading) assertTrue(item.newsState is NewsUiState.Loading)
@ -119,7 +122,7 @@ class AuthorViewModelTest {
viewModel.uiState.test { viewModel.uiState.test {
awaitItem() awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author }) authorsRepository.sendAuthors(testInputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
val item = awaitItem() val item = awaitItem()
assertTrue(item.authorState is AuthorUiState.Success) assertTrue(item.authorState is AuthorUiState.Success)
@ -134,7 +137,7 @@ class AuthorViewModelTest {
awaitItem() awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author }) authorsRepository.sendAuthors(testInputAuthors.map { it.author })
// Set which author IDs are followed, not including 0. // Set which author IDs are followed, not including 0.
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
viewModel.followAuthorToggle(true) viewModel.followAuthorToggle(true)

@ -27,6 +27,7 @@ import androidx.lifecycle.viewmodel.compose.saveable
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
@ -51,26 +52,25 @@ import kotlinx.coroutines.launch
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
@HiltViewModel @HiltViewModel
class ForYouViewModel @Inject constructor( class ForYouViewModel @Inject constructor(
private val authorsRepository: AuthorsRepository, authorsRepository: AuthorsRepository,
private val topicsRepository: TopicsRepository, topicsRepository: TopicsRepository,
private val newsRepository: NewsRepository, private val newsRepository: NewsRepository,
private val userDataRepository: UserDataRepository,
savedStateHandle: SavedStateHandle savedStateHandle: SavedStateHandle
) : ViewModel() { ) : ViewModel() {
private val followedInterestsState: StateFlow<FollowedInterestsState> = private val followedInterestsState: StateFlow<FollowedInterestsState> =
combine( userDataRepository.userDataStream
authorsRepository.getFollowedAuthorIdsStream(), .map { userData ->
topicsRepository.getFollowedTopicIdsStream(), if (userData.followedAuthors.isEmpty() && userData.followedTopics.isEmpty()) {
) { followedAuthors, followedTopics -> None
if (followedAuthors.isEmpty() && followedTopics.isEmpty()) { } else {
None FollowedInterests(
} else { authorIds = userData.followedAuthors,
FollowedInterests( topicIds = userData.followedTopics
authorIds = followedAuthors, )
topicIds = followedTopics }
)
} }
}
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000), started = SharingStarted.WhileSubscribed(5_000),
@ -232,8 +232,8 @@ class ForYouViewModel @Inject constructor(
} }
viewModelScope.launch { viewModelScope.launch {
topicsRepository.setFollowedTopicIds(inProgressTopicSelection) userDataRepository.setFollowedTopicIds(inProgressTopicSelection)
authorsRepository.setFollowedAuthorIds(inProgressAuthorSelection) userDataRepository.setFollowedAuthorIds(inProgressAuthorSelection)
// Clear out the old selection, in case we return to onboarding // Clear out the old selection, in case we return to onboarding
withMutableSnapshot { withMutableSnapshot {
inProgressTopicSelection = emptySet() inProgressTopicSelection = emptySet()

@ -28,6 +28,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceUntilIdle
@ -42,6 +43,7 @@ class ForYouViewModelTest {
@get:Rule @get:Rule
val dispatcherRule = TestDispatcherRule() val dispatcherRule = TestDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val authorsRepository = TestAuthorsRepository() private val authorsRepository = TestAuthorsRepository()
private val topicsRepository = TestTopicsRepository() private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository() private val newsRepository = TestNewsRepository()
@ -50,6 +52,7 @@ class ForYouViewModelTest {
@Before @Before
fun setup() { fun setup() {
viewModel = ForYouViewModel( viewModel = ForYouViewModel(
userDataRepository = userDataRepository,
authorsRepository = authorsRepository, authorsRepository = authorsRepository,
topicsRepository = topicsRepository, topicsRepository = topicsRepository,
newsRepository = newsRepository, newsRepository = newsRepository,
@ -125,7 +128,7 @@ class ForYouViewModelTest {
), ),
awaitItem() awaitItem()
) )
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
} }
} }
@ -139,7 +142,7 @@ class ForYouViewModelTest {
), ),
awaitItem() awaitItem()
) )
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
} }
} }
@ -149,9 +152,9 @@ class ForYouViewModelTest {
advanceUntilIdle() advanceUntilIdle()
expectMostRecentItem() expectMostRecentItem()
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
advanceUntilIdle() advanceUntilIdle()
assertEquals( assertEquals(
@ -244,8 +247,8 @@ class ForYouViewModelTest {
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
advanceUntilIdle() advanceUntilIdle()
@ -340,9 +343,9 @@ class ForYouViewModelTest {
advanceUntilIdle() advanceUntilIdle()
expectMostRecentItem() expectMostRecentItem()
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(setOf("0", "1")) userDataRepository.setFollowedTopicIds(setOf("0", "1"))
assertEquals( assertEquals(
ForYouUiState( ForYouUiState(
@ -380,9 +383,9 @@ class ForYouViewModelTest {
advanceUntilIdle() advanceUntilIdle()
expectMostRecentItem() expectMostRecentItem()
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(setOf("0", "1")) userDataRepository.setFollowedAuthorIds(setOf("0", "1"))
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
assertEquals( assertEquals(
ForYouUiState( ForYouUiState(
@ -418,9 +421,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
advanceUntilIdle() advanceUntilIdle()
@ -686,9 +689,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
advanceUntilIdle() advanceUntilIdle()
@ -954,9 +957,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateTopicSelection("1", isChecked = true) viewModel.updateTopicSelection("1", isChecked = true)
viewModel.updateTopicSelection("1", isChecked = false) viewModel.updateTopicSelection("1", isChecked = false)
@ -1051,9 +1054,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateAuthorSelection("1", isChecked = true) viewModel.updateAuthorSelection("1", isChecked = true)
viewModel.updateAuthorSelection("1", isChecked = false) viewModel.updateAuthorSelection("1", isChecked = false)
@ -1148,9 +1151,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateTopicSelection("1", isChecked = true) viewModel.updateTopicSelection("1", isChecked = true)
@ -1179,8 +1182,8 @@ class ForYouViewModelTest {
), ),
expectMostRecentItem() expectMostRecentItem()
) )
assertEquals(setOf("1"), topicsRepository.getCurrentFollowedTopics()) assertEquals(setOf("1"), userDataRepository.getCurrentFollowedTopics())
assertEquals(emptySet<Int>(), authorsRepository.getCurrentFollowedAuthors()) assertEquals(emptySet<Int>(), userDataRepository.getCurrentFollowedAuthors())
} }
} }
@ -1189,9 +1192,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateAuthorSelection("0", isChecked = true) viewModel.updateAuthorSelection("0", isChecked = true)
@ -1216,8 +1219,8 @@ class ForYouViewModelTest {
), ),
expectMostRecentItem() expectMostRecentItem()
) )
assertEquals(emptySet<Int>(), topicsRepository.getCurrentFollowedTopics()) assertEquals(emptySet<Int>(), userDataRepository.getCurrentFollowedTopics())
assertEquals(setOf("0"), authorsRepository.getCurrentFollowedAuthors()) assertEquals(setOf("0"), userDataRepository.getCurrentFollowedAuthors())
} }
} }
@ -1226,9 +1229,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateAuthorSelection("1", isChecked = true) viewModel.updateAuthorSelection("1", isChecked = true)
viewModel.updateTopicSelection("1", isChecked = true) viewModel.updateTopicSelection("1", isChecked = true)
@ -1258,8 +1261,8 @@ class ForYouViewModelTest {
), ),
expectMostRecentItem() expectMostRecentItem()
) )
assertEquals(setOf("1"), topicsRepository.getCurrentFollowedTopics()) assertEquals(setOf("1"), userDataRepository.getCurrentFollowedTopics())
assertEquals(setOf("1"), authorsRepository.getCurrentFollowedAuthors()) assertEquals(setOf("1"), userDataRepository.getCurrentFollowedAuthors())
} }
} }
@ -1268,9 +1271,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateTopicSelection("1", isChecked = true) viewModel.updateTopicSelection("1", isChecked = true)
viewModel.saveFollowedInterests() viewModel.saveFollowedInterests()
@ -1278,7 +1281,7 @@ class ForYouViewModelTest {
advanceUntilIdle() advanceUntilIdle()
expectMostRecentItem() expectMostRecentItem()
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
advanceUntilIdle() advanceUntilIdle()
assertEquals( assertEquals(
@ -1371,9 +1374,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateAuthorSelection("1", isChecked = true) viewModel.updateAuthorSelection("1", isChecked = true)
viewModel.saveFollowedInterests() viewModel.saveFollowedInterests()
@ -1381,7 +1384,7 @@ class ForYouViewModelTest {
advanceUntilIdle() advanceUntilIdle()
expectMostRecentItem() expectMostRecentItem()
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
advanceUntilIdle() advanceUntilIdle()
assertEquals( assertEquals(
@ -1473,9 +1476,9 @@ class ForYouViewModelTest {
viewModel.uiState viewModel.uiState
.test { .test {
topicsRepository.sendTopics(sampleTopics) topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(setOf("1")) userDataRepository.setFollowedTopicIds(setOf("1"))
authorsRepository.sendAuthors(sampleAuthors) authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(setOf("1")) userDataRepository.setFollowedAuthorIds(setOf("1"))
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateNewsResourceSaved("2", true) viewModel.updateNewsResourceSaved("2", true)

@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -35,8 +36,9 @@ import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class InterestsViewModel @Inject constructor( class InterestsViewModel @Inject constructor(
private val authorsRepository: AuthorsRepository, private val userDataRepository: UserDataRepository,
private val topicsRepository: TopicsRepository authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() { ) : ViewModel() {
private val _tabState = MutableStateFlow( private val _tabState = MutableStateFlow(
@ -48,18 +50,17 @@ class InterestsViewModel @Inject constructor(
val tabState: StateFlow<InterestsTabState> = _tabState.asStateFlow() val tabState: StateFlow<InterestsTabState> = _tabState.asStateFlow()
val uiState: StateFlow<InterestsUiState> = combine( val uiState: StateFlow<InterestsUiState> = combine(
userDataRepository.userDataStream,
authorsRepository.getAuthorsStream(), authorsRepository.getAuthorsStream(),
authorsRepository.getFollowedAuthorIdsStream(),
topicsRepository.getTopicsStream(), topicsRepository.getTopicsStream(),
topicsRepository.getFollowedTopicIdsStream(), ) { userData, availableAuthors, availableTopics ->
) { availableAuthors, followedAuthorIdsState, availableTopics, followedTopicIdsState ->
InterestsUiState.Interests( InterestsUiState.Interests(
authors = availableAuthors authors = availableAuthors
.map { author -> .map { author ->
FollowableAuthor( FollowableAuthor(
author = author, author = author,
isFollowed = author.id in followedAuthorIdsState isFollowed = author.id in userData.followedAuthors
) )
} }
.sortedBy { it.author.name }, .sortedBy { it.author.name },
@ -67,7 +68,7 @@ class InterestsViewModel @Inject constructor(
.map { topic -> .map { topic ->
FollowableTopic( FollowableTopic(
topic = topic, topic = topic,
isFollowed = topic.id in followedTopicIdsState isFollowed = topic.id in userData.followedTopics
) )
} }
.sortedBy { it.topic.name } .sortedBy { it.topic.name }
@ -81,13 +82,13 @@ class InterestsViewModel @Inject constructor(
fun followTopic(followedTopicId: String, followed: Boolean) { fun followTopic(followedTopicId: String, followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
topicsRepository.toggleFollowedTopicId(followedTopicId, followed) userDataRepository.toggleFollowedTopicId(followedTopicId, followed)
} }
} }
fun followAuthor(followedAuthorId: String, followed: Boolean) { fun followAuthor(followedAuthorId: String, followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
authorsRepository.toggleFollowedAuthorId(followedAuthorId, followed) userDataRepository.toggleFollowedAuthorId(followedAuthorId, followed)
} }
} }

@ -23,6 +23,7 @@ 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.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestAuthorsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule
import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState import com.google.samples.apps.nowinandroid.feature.interests.InterestsUiState
import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel
@ -37,13 +38,18 @@ class InterestsViewModelTest {
@get:Rule @get:Rule
val dispatcherRule = TestDispatcherRule() val dispatcherRule = TestDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val authorsRepository = TestAuthorsRepository() private val authorsRepository = TestAuthorsRepository()
private val topicsRepository = TestTopicsRepository() private val topicsRepository = TestTopicsRepository()
private lateinit var viewModel: InterestsViewModel private lateinit var viewModel: InterestsViewModel
@Before @Before
fun setup() { fun setup() {
viewModel = InterestsViewModel(authorsRepository, topicsRepository) viewModel = InterestsViewModel(
userDataRepository = userDataRepository,
authorsRepository = authorsRepository,
topicsRepository = topicsRepository,
)
} }
@Test @Test
@ -57,8 +63,8 @@ class InterestsViewModelTest {
fun uiState_whenFollowedTopicsAreLoading_thenShowLoading() = runTest { fun uiState_whenFollowedTopicsAreLoading_thenShowLoading() = runTest {
viewModel.uiState.test { viewModel.uiState.test {
assertEquals(InterestsUiState.Loading, awaitItem()) assertEquals(InterestsUiState.Loading, awaitItem())
authorsRepository.setFollowedAuthorIds(setOf("1")) userDataRepository.setFollowedAuthorIds(setOf("1"))
topicsRepository.setFollowedTopicIds(emptySet()) userDataRepository.setFollowedTopicIds(emptySet())
} }
} }
@ -66,8 +72,8 @@ class InterestsViewModelTest {
fun uiState_whenFollowedAuthorsAreLoading_thenShowLoading() = runTest { fun uiState_whenFollowedAuthorsAreLoading_thenShowLoading() = runTest {
viewModel.uiState.test { viewModel.uiState.test {
assertEquals(InterestsUiState.Loading, awaitItem()) assertEquals(InterestsUiState.Loading, awaitItem())
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
topicsRepository.setFollowedTopicIds(setOf("1")) userDataRepository.setFollowedTopicIds(setOf("1"))
} }
} }
@ -78,9 +84,9 @@ class InterestsViewModelTest {
.test { .test {
awaitItem() awaitItem()
authorsRepository.sendAuthors(emptyList()) authorsRepository.sendAuthors(emptyList())
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(testInputTopics.map { it.topic }) topicsRepository.sendTopics(testInputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[0].topic.id)) userDataRepository.setFollowedTopicIds(setOf(testInputTopics[0].topic.id))
assertEquals( assertEquals(
false, false,
@ -106,9 +112,9 @@ class InterestsViewModelTest {
.test { .test {
awaitItem() awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author }) authorsRepository.sendAuthors(testInputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[0].author.id)) userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[0].author.id))
topicsRepository.sendTopics(listOf()) topicsRepository.sendTopics(listOf())
topicsRepository.setFollowedTopicIds(setOf()) userDataRepository.setFollowedTopicIds(setOf())
awaitItem() awaitItem()
viewModel.followAuthor( viewModel.followAuthor(
@ -130,9 +136,9 @@ class InterestsViewModelTest {
.test { .test {
awaitItem() awaitItem()
authorsRepository.sendAuthors(emptyList()) authorsRepository.sendAuthors(emptyList())
authorsRepository.setFollowedAuthorIds(emptySet()) userDataRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(testOutputTopics.map { it.topic }) topicsRepository.sendTopics(testOutputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds( userDataRepository.setFollowedTopicIds(
setOf(testOutputTopics[0].topic.id, testOutputTopics[1].topic.id) setOf(testOutputTopics[0].topic.id, testOutputTopics[1].topic.id)
) )
@ -160,11 +166,11 @@ class InterestsViewModelTest {
.test { .test {
awaitItem() awaitItem()
authorsRepository.sendAuthors(testOutputAuthors.map { it.author }) authorsRepository.sendAuthors(testOutputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds( userDataRepository.setFollowedAuthorIds(
setOf(testOutputAuthors[0].author.id, testOutputAuthors[1].author.id) setOf(testOutputAuthors[0].author.id, testOutputAuthors[1].author.id)
) )
topicsRepository.sendTopics(listOf()) topicsRepository.sendTopics(listOf())
topicsRepository.setFollowedTopicIds(setOf()) userDataRepository.setFollowedTopicIds(setOf())
awaitItem() awaitItem()
viewModel.followAuthor( viewModel.followAuthor(

@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
@ -33,13 +34,15 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class TopicViewModel @Inject constructor( class TopicViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val topicsRepository: TopicsRepository, private val userDataRepository: UserDataRepository,
topicsRepository: TopicsRepository,
newsRepository: NewsRepository newsRepository: NewsRepository
) : ViewModel() { ) : ViewModel() {
@ -47,7 +50,9 @@ class TopicViewModel @Inject constructor(
// Observe the followed topics, as they could change over time. // Observe the followed topics, as they could change over time.
private val followedTopicIdsStream: Flow<Result<Set<String>>> = private val followedTopicIdsStream: Flow<Result<Set<String>>> =
topicsRepository.getFollowedTopicIdsStream().asResult() userDataRepository.userDataStream
.map { it.followedTopics }
.asResult()
// Observe topic information // Observe topic information
private val topic: Flow<Result<Topic>> = topicsRepository.getTopic(topicId).asResult() private val topic: Flow<Result<Topic>> = topicsRepository.getTopic(topicId).asResult()
@ -97,7 +102,7 @@ class TopicViewModel @Inject constructor(
fun followTopicToggle(followed: Boolean) { fun followTopicToggle(followed: Boolean) {
viewModelScope.launch { viewModelScope.launch {
topicsRepository.toggleFollowedTopicId(topicId, followed) userDataRepository.toggleFollowedTopicId(topicId, followed)
} }
} }
} }

@ -24,6 +24,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Vid
import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestTopicsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule import com.google.samples.apps.nowinandroid.core.testing.util.TestDispatcherRule
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicDestination
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -40,6 +41,7 @@ class TopicViewModelTest {
@get:Rule @get:Rule
val dispatcherRule = TestDispatcherRule() val dispatcherRule = TestDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val topicsRepository = TestTopicsRepository() private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository() private val newsRepository = TestNewsRepository()
private lateinit var viewModel: TopicViewModel private lateinit var viewModel: TopicViewModel
@ -49,6 +51,7 @@ class TopicViewModelTest {
viewModel = TopicViewModel( viewModel = TopicViewModel(
savedStateHandle = savedStateHandle =
SavedStateHandle(mapOf(TopicDestination.topicIdArg to testInputTopics[0].topic.id)), SavedStateHandle(mapOf(TopicDestination.topicIdArg to testInputTopics[0].topic.id)),
userDataRepository = userDataRepository,
topicsRepository = topicsRepository, topicsRepository = topicsRepository,
newsRepository = newsRepository newsRepository = newsRepository
) )
@ -59,7 +62,7 @@ class TopicViewModelTest {
viewModel.uiState.test { viewModel.uiState.test {
awaitItem() awaitItem()
topicsRepository.sendTopics(testInputTopics.map(FollowableTopic::topic)) topicsRepository.sendTopics(testInputTopics.map(FollowableTopic::topic))
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id)) userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
val item = awaitItem() val item = awaitItem()
assertTrue(item.topicState is TopicUiState.Success) assertTrue(item.topicState is TopicUiState.Success)
@ -89,7 +92,7 @@ class TopicViewModelTest {
@Test @Test
fun uiStateTopic_whenFollowedIdsSuccessAndTopicLoading_thenShowLoading() = runTest { fun uiStateTopic_whenFollowedIdsSuccessAndTopicLoading_thenShowLoading() = runTest {
viewModel.uiState.test { viewModel.uiState.test {
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id)) userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
assertEquals(TopicUiState.Loading, awaitItem().topicState) assertEquals(TopicUiState.Loading, awaitItem().topicState)
} }
} }
@ -100,7 +103,7 @@ class TopicViewModelTest {
viewModel.uiState.test { viewModel.uiState.test {
awaitItem() awaitItem()
topicsRepository.sendTopics(testInputTopics.map { it.topic }) topicsRepository.sendTopics(testInputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id)) userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
val item = awaitItem() val item = awaitItem()
assertTrue(item.topicState is TopicUiState.Success) assertTrue(item.topicState is TopicUiState.Success)
assertTrue(item.newsState is NewsUiState.Loading) assertTrue(item.newsState is NewsUiState.Loading)
@ -113,7 +116,7 @@ class TopicViewModelTest {
viewModel.uiState.test { viewModel.uiState.test {
awaitItem() awaitItem()
topicsRepository.sendTopics(testInputTopics.map { it.topic }) topicsRepository.sendTopics(testInputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id)) userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
newsRepository.sendNewsResources(sampleNewsResources) newsRepository.sendNewsResources(sampleNewsResources)
val item = awaitItem() val item = awaitItem()
assertTrue(item.topicState is TopicUiState.Success) assertTrue(item.topicState is TopicUiState.Success)
@ -128,7 +131,7 @@ class TopicViewModelTest {
awaitItem() awaitItem()
topicsRepository.sendTopics(testInputTopics.map { it.topic }) topicsRepository.sendTopics(testInputTopics.map { it.topic })
// Set which topic IDs are followed, not including 0. // Set which topic IDs are followed, not including 0.
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id)) userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
viewModel.followTopicToggle(true) viewModel.followTopicToggle(true)

Loading…
Cancel
Save