Add UserDataRepository

Change-Id: I96cd2e6d137ad1c26fe2a4fdd3ada733b5be06b0
pull/146/head
Adetunji Dahunsi 2 years ago
parent 45dd9d0ed6
commit 3a5e7b36db

@ -34,14 +34,7 @@
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="99" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="99" />
<option name="IMPORT_NESTED_CLASSES" value="true" />
<option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="false" />
<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" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<Properties>
<option name="KEEP_BLANK_LINES" value="true" />
@ -307,19 +300,11 @@
</arrangement>
</codeStyleSettings>
<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_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<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="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />

@ -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.NewsRepository
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.FakeNewsRepository
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.Module
import dagger.hilt.components.SingletonComponent
@ -48,4 +50,9 @@ interface TestDataModule {
fun bindsNewsResourceRepository(
fakeNewsRepository: FakeNewsRepository
): 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.OfflineFirstNewsRepository
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.UserDataRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -45,4 +47,9 @@ interface DataModule {
fun bindsNewsResourceRepository(
newsRepository: OfflineFirstNewsRepository
): NewsRepository
@Binds
fun bindsUserDataRepository(
userDataRepository: OfflineFirstUserDataRepository
): UserDataRepository
}

@ -30,19 +30,4 @@ interface AuthorsRepository : Syncable {
* Gets data for a specific 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.asExternalModel
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.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
@ -38,7 +37,6 @@ import kotlinx.coroutines.flow.map
class OfflineFirstAuthorsRepository @Inject constructor(
private val authorDao: AuthorDao,
private val network: NiaNetworkDataSource,
private val niaPreferences: NiaPreferencesDataSource,
) : AuthorsRepository {
override fun getAuthorStream(id: String): Flow<Author> =
@ -50,14 +48,6 @@ class OfflineFirstAuthorsRepository @Inject constructor(
authorDao.getAuthorEntitiesStream()
.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 =
synchronizer.changeListSync(
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.asExternalModel
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.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
@ -38,7 +37,6 @@ import kotlinx.coroutines.flow.map
class OfflineFirstTopicsRepository @Inject constructor(
private val topicDao: TopicDao,
private val network: NiaNetworkDataSource,
private val niaPreferences: NiaPreferencesDataSource,
) : TopicsRepository {
override fun getTopicsStream(): Flow<List<Topic>> =
@ -48,14 +46,6 @@ class OfflineFirstTopicsRepository @Inject constructor(
override fun getTopic(id: String): Flow<Topic> =
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 =
synchronizer.changeListSync(
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
*/
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.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.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
@ -41,7 +40,6 @@ import kotlinx.serialization.json.Json
*/
class FakeAuthorsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val niaPreferences: NiaPreferencesDataSource,
private val networkJson: Json,
) : AuthorsRepository {
@ -65,15 +63,5 @@ class FakeAuthorsRepository @Inject constructor(
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
}

@ -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.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.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
@ -43,7 +42,6 @@ import kotlinx.serialization.json.Json
class FakeTopicsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val networkJson: Json,
private val niaPreferences: NiaPreferencesDataSource
) : TopicsRepository {
override fun getTopicsStream(): Flow<List<Topic>> = flow<List<Topic>> {
emit(
@ -65,13 +63,5 @@ class FakeTopicsRepository @Inject constructor(
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
}

@ -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(
authorDao = authorDao,
network = network,
niaPreferences = niaPreferencesDataSource,
)
}

@ -62,8 +62,7 @@ class OfflineFirstTopicsRepositoryTest {
subject = OfflineFirstTopicsRepository(
topicDao = topicDao,
network = network,
niaPreferences = niaPreferences
network = network
)
}
@ -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
fun offlineFirstTopicsRepository_sync_pulls_from_network() =
runTest {
@ -183,50 +171,4 @@ class OfflineFirstTopicsRepositoryTest {
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 {
implementation(project(":core-common"))
implementation(project(":core-model"))
testImplementation(project(":core-testing"))

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.core.datastore
import android.util.Log
import androidx.datastore.core.DataStore
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import java.io.IOException
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@ -143,4 +144,13 @@ class NiaPreferencesDataSource @Inject constructor(
true
}
.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
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.
*/
@ -43,21 +37,6 @@ class TestAuthorsRepository : AuthorsRepository {
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
/**
@ -66,9 +45,4 @@ class TestAuthorsRepository : AuthorsRepository {
fun sendAuthors(authors: List<Author>) {
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
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.
*/
@ -43,21 +37,6 @@ class TestTopicsRepository : TopicsRepository {
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.
*/
@ -65,10 +44,5 @@ class TestTopicsRepository : TopicsRepository {
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
}

@ -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 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.UserDataRepository
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.NewsResource
@ -33,13 +34,15 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel
class AuthorViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val authorsRepository: AuthorsRepository,
private val userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
newsRepository: NewsRepository
) : ViewModel() {
@ -49,7 +52,9 @@ class AuthorViewModel @Inject constructor(
// Observe the followed authors, as they could change over time.
private val followedAuthorIdsStream: Flow<Result<Set<String>>> =
authorsRepository.getFollowedAuthorIdsStream().asResult()
userDataRepository.userDataStream
.map { it.followedAuthors }
.asResult()
// Observe author information
private val author: Flow<Result<Author>> = authorsRepository.getAuthorStream(
@ -102,7 +107,7 @@ class AuthorViewModel @Inject constructor(
fun followAuthorToggle(followed: Boolean) {
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.testing.repository.TestAuthorsRepository
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.feature.author.navigation.AuthorDestination
import kotlinx.coroutines.flow.first
@ -40,6 +41,7 @@ class AuthorViewModelTest {
@get:Rule
val dispatcherRule = TestDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val authorsRepository = TestAuthorsRepository()
private val newsRepository = TestNewsRepository()
private lateinit var viewModel: AuthorViewModel
@ -52,6 +54,7 @@ class AuthorViewModelTest {
AuthorDestination.authorIdArg to testInputAuthors[0].author.id
)
),
userDataRepository = userDataRepository,
authorsRepository = authorsRepository,
newsRepository = newsRepository
)
@ -63,7 +66,7 @@ class AuthorViewModelTest {
awaitItem()
// To make sure AuthorUiState is success
authorsRepository.sendAuthors(testInputAuthors.map(FollowableAuthor::author))
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
val item = awaitItem()
assertTrue(item.authorState is AuthorUiState.Success)
@ -99,7 +102,7 @@ class AuthorViewModelTest {
@Test
fun uiStateAuthor_whenFollowedIdsSuccessAndAuthorLoading_thenShowLoading() = runTest {
viewModel.uiState.test {
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
assertEquals(AuthorUiState.Loading, awaitItem().authorState)
cancel()
}
@ -111,7 +114,7 @@ class AuthorViewModelTest {
viewModel.uiState.test {
awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
val item = awaitItem()
assertTrue(item.authorState is AuthorUiState.Success)
assertTrue(item.newsState is NewsUiState.Loading)
@ -125,7 +128,7 @@ class AuthorViewModelTest {
viewModel.uiState.test {
awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[1].author.id))
newsRepository.sendNewsResources(sampleNewsResources)
val item = awaitItem()
assertTrue(item.authorState is AuthorUiState.Success)
@ -141,7 +144,7 @@ class AuthorViewModelTest {
awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author })
// 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)

@ -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.NewsRepository
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.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
@ -51,26 +52,25 @@ import kotlinx.coroutines.launch
@OptIn(SavedStateHandleSaveableApi::class)
@HiltViewModel
class ForYouViewModel @Inject constructor(
private val authorsRepository: AuthorsRepository,
private val topicsRepository: TopicsRepository,
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository,
private val newsRepository: NewsRepository,
private val userDataRepository: UserDataRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val followedInterestsState: StateFlow<FollowedInterestsState> =
combine(
authorsRepository.getFollowedAuthorIdsStream(),
topicsRepository.getFollowedTopicIdsStream(),
) { followedAuthors, followedTopics ->
if (followedAuthors.isEmpty() && followedTopics.isEmpty()) {
None
} else {
FollowedInterests(
authorIds = followedAuthors,
topicIds = followedTopics
)
userDataRepository.userDataStream
.map { userData ->
if (userData.followedAuthors.isEmpty() && userData.followedTopics.isEmpty()) {
None
} else {
FollowedInterests(
authorIds = userData.followedAuthors,
topicIds = userData.followedTopics
)
}
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
@ -232,8 +232,8 @@ class ForYouViewModel @Inject constructor(
}
viewModelScope.launch {
topicsRepository.setFollowedTopicIds(inProgressTopicSelection)
authorsRepository.setFollowedAuthorIds(inProgressAuthorSelection)
userDataRepository.setFollowedTopicIds(inProgressTopicSelection)
userDataRepository.setFollowedAuthorIds(inProgressAuthorSelection)
// Clear out the old selection, in case we return to onboarding
withMutableSnapshot {
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.TestNewsRepository
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 kotlinx.coroutines.flow.combine
import kotlinx.coroutines.test.advanceUntilIdle
@ -42,6 +43,7 @@ class ForYouViewModelTest {
@get:Rule
val dispatcherRule = TestDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val authorsRepository = TestAuthorsRepository()
private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository()
@ -50,6 +52,7 @@ class ForYouViewModelTest {
@Before
fun setup() {
viewModel = ForYouViewModel(
userDataRepository = userDataRepository,
authorsRepository = authorsRepository,
topicsRepository = topicsRepository,
newsRepository = newsRepository,
@ -130,7 +133,7 @@ class ForYouViewModelTest {
),
awaitItem()
)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
cancel()
}
@ -146,7 +149,7 @@ class ForYouViewModelTest {
),
awaitItem()
)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
cancel()
}
@ -158,9 +161,9 @@ class ForYouViewModelTest {
advanceUntilIdle()
expectMostRecentItem()
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
advanceUntilIdle()
assertEquals(
@ -255,8 +258,8 @@ class ForYouViewModelTest {
.test {
topicsRepository.sendTopics(sampleTopics)
authorsRepository.sendAuthors(sampleAuthors)
topicsRepository.setFollowedTopicIds(emptySet())
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
advanceUntilIdle()
@ -352,9 +355,9 @@ class ForYouViewModelTest {
advanceUntilIdle()
expectMostRecentItem()
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(setOf("0", "1"))
userDataRepository.setFollowedTopicIds(setOf("0", "1"))
assertEquals(
ForYouUiState(
@ -393,9 +396,9 @@ class ForYouViewModelTest {
advanceUntilIdle()
expectMostRecentItem()
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(setOf("0", "1"))
userDataRepository.setFollowedAuthorIds(setOf("0", "1"))
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
assertEquals(
ForYouUiState(
@ -432,9 +435,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
advanceUntilIdle()
@ -701,9 +704,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
advanceUntilIdle()
@ -970,9 +973,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateTopicSelection("1", isChecked = true)
viewModel.updateTopicSelection("1", isChecked = false)
@ -1068,9 +1071,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateAuthorSelection("1", isChecked = true)
viewModel.updateAuthorSelection("1", isChecked = false)
@ -1166,9 +1169,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateTopicSelection("1", isChecked = true)
@ -1197,8 +1200,8 @@ class ForYouViewModelTest {
),
expectMostRecentItem()
)
assertEquals(setOf("1"), topicsRepository.getCurrentFollowedTopics())
assertEquals(emptySet<Int>(), authorsRepository.getCurrentFollowedAuthors())
assertEquals(setOf("1"), userDataRepository.getCurrentFollowedTopics())
assertEquals(emptySet<Int>(), userDataRepository.getCurrentFollowedAuthors())
cancel()
}
}
@ -1208,9 +1211,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateAuthorSelection("0", isChecked = true)
@ -1235,8 +1238,8 @@ class ForYouViewModelTest {
),
expectMostRecentItem()
)
assertEquals(emptySet<Int>(), topicsRepository.getCurrentFollowedTopics())
assertEquals(setOf("0"), authorsRepository.getCurrentFollowedAuthors())
assertEquals(emptySet<Int>(), userDataRepository.getCurrentFollowedTopics())
assertEquals(setOf("0"), userDataRepository.getCurrentFollowedAuthors())
cancel()
}
}
@ -1246,9 +1249,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateAuthorSelection("1", isChecked = true)
viewModel.updateTopicSelection("1", isChecked = true)
@ -1278,8 +1281,8 @@ class ForYouViewModelTest {
),
expectMostRecentItem()
)
assertEquals(setOf("1"), topicsRepository.getCurrentFollowedTopics())
assertEquals(setOf("1"), authorsRepository.getCurrentFollowedAuthors())
assertEquals(setOf("1"), userDataRepository.getCurrentFollowedTopics())
assertEquals(setOf("1"), userDataRepository.getCurrentFollowedAuthors())
cancel()
}
}
@ -1289,9 +1292,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateTopicSelection("1", isChecked = true)
viewModel.saveFollowedInterests()
@ -1299,7 +1302,7 @@ class ForYouViewModelTest {
advanceUntilIdle()
expectMostRecentItem()
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
advanceUntilIdle()
assertEquals(
@ -1393,9 +1396,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedTopicIds(emptySet())
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateAuthorSelection("1", isChecked = true)
viewModel.saveFollowedInterests()
@ -1403,7 +1406,7 @@ class ForYouViewModelTest {
advanceUntilIdle()
expectMostRecentItem()
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
advanceUntilIdle()
assertEquals(
@ -1496,9 +1499,9 @@ class ForYouViewModelTest {
viewModel.uiState
.test {
topicsRepository.sendTopics(sampleTopics)
topicsRepository.setFollowedTopicIds(setOf("1"))
userDataRepository.setFollowedTopicIds(setOf("1"))
authorsRepository.sendAuthors(sampleAuthors)
authorsRepository.setFollowedAuthorIds(setOf("1"))
userDataRepository.setFollowedAuthorIds(setOf("1"))
newsRepository.sendNewsResources(sampleNewsResources)
viewModel.updateNewsResourceSaved("2", true)

@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import dagger.hilt.android.lifecycle.HiltViewModel
@ -35,8 +36,9 @@ import kotlinx.coroutines.launch
@HiltViewModel
class InterestsViewModel @Inject constructor(
private val authorsRepository: AuthorsRepository,
private val topicsRepository: TopicsRepository
private val userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
private val _tabState = MutableStateFlow(
@ -48,18 +50,17 @@ class InterestsViewModel @Inject constructor(
val tabState: StateFlow<InterestsTabState> = _tabState.asStateFlow()
val uiState: StateFlow<InterestsUiState> = combine(
userDataRepository.userDataStream,
authorsRepository.getAuthorsStream(),
authorsRepository.getFollowedAuthorIdsStream(),
topicsRepository.getTopicsStream(),
topicsRepository.getFollowedTopicIdsStream(),
) { availableAuthors, followedAuthorIdsState, availableTopics, followedTopicIdsState ->
) { userData, availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors
.map { author ->
FollowableAuthor(
author = author,
isFollowed = author.id in followedAuthorIdsState
isFollowed = author.id in userData.followedAuthors
)
}
.sortedBy { it.author.name },
@ -67,7 +68,7 @@ class InterestsViewModel @Inject constructor(
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in followedTopicIdsState
isFollowed = topic.id in userData.followedTopics
)
}
.sortedBy { it.topic.name }
@ -81,13 +82,13 @@ class InterestsViewModel @Inject constructor(
fun followTopic(followedTopicId: String, followed: Boolean) {
viewModelScope.launch {
topicsRepository.toggleFollowedTopicId(followedTopicId, followed)
userDataRepository.toggleFollowedTopicId(followedTopicId, followed)
}
}
fun followAuthor(followedAuthorId: String, followed: Boolean) {
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.testing.repository.TestAuthorsRepository
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.feature.interests.InterestsUiState
import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel
@ -37,13 +38,18 @@ class InterestsViewModelTest {
@get:Rule
val dispatcherRule = TestDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val authorsRepository = TestAuthorsRepository()
private val topicsRepository = TestTopicsRepository()
private lateinit var viewModel: InterestsViewModel
@Before
fun setup() {
viewModel = InterestsViewModel(authorsRepository, topicsRepository)
viewModel = InterestsViewModel(
userDataRepository = userDataRepository,
authorsRepository = authorsRepository,
topicsRepository = topicsRepository,
)
}
@Test
@ -58,8 +64,8 @@ class InterestsViewModelTest {
fun uiState_whenFollowedTopicsAreLoading_thenShowLoading() = runTest {
viewModel.uiState.test {
assertEquals(InterestsUiState.Loading, awaitItem())
authorsRepository.setFollowedAuthorIds(setOf("1"))
topicsRepository.setFollowedTopicIds(emptySet())
userDataRepository.setFollowedAuthorIds(setOf("1"))
userDataRepository.setFollowedTopicIds(emptySet())
cancel()
}
}
@ -68,8 +74,8 @@ class InterestsViewModelTest {
fun uiState_whenFollowedAuthorsAreLoading_thenShowLoading() = runTest {
viewModel.uiState.test {
assertEquals(InterestsUiState.Loading, awaitItem())
authorsRepository.setFollowedAuthorIds(emptySet())
topicsRepository.setFollowedTopicIds(setOf("1"))
userDataRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedTopicIds(setOf("1"))
cancel()
}
}
@ -81,9 +87,9 @@ class InterestsViewModelTest {
.test {
awaitItem()
authorsRepository.sendAuthors(emptyList())
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(testInputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[0].topic.id))
userDataRepository.setFollowedTopicIds(setOf(testInputTopics[0].topic.id))
assertEquals(
false,
@ -110,9 +116,9 @@ class InterestsViewModelTest {
.test {
awaitItem()
authorsRepository.sendAuthors(testInputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(setOf(testInputAuthors[0].author.id))
userDataRepository.setFollowedAuthorIds(setOf(testInputAuthors[0].author.id))
topicsRepository.sendTopics(listOf())
topicsRepository.setFollowedTopicIds(setOf())
userDataRepository.setFollowedTopicIds(setOf())
awaitItem()
viewModel.followAuthor(
@ -135,9 +141,9 @@ class InterestsViewModelTest {
.test {
awaitItem()
authorsRepository.sendAuthors(emptyList())
authorsRepository.setFollowedAuthorIds(emptySet())
userDataRepository.setFollowedAuthorIds(emptySet())
topicsRepository.sendTopics(testOutputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(
userDataRepository.setFollowedTopicIds(
setOf(testOutputTopics[0].topic.id, testOutputTopics[1].topic.id)
)
@ -166,11 +172,11 @@ class InterestsViewModelTest {
.test {
awaitItem()
authorsRepository.sendAuthors(testOutputAuthors.map { it.author })
authorsRepository.setFollowedAuthorIds(
userDataRepository.setFollowedAuthorIds(
setOf(testOutputAuthors[0].author.id, testOutputAuthors[1].author.id)
)
topicsRepository.sendTopics(listOf())
topicsRepository.setFollowedTopicIds(setOf())
userDataRepository.setFollowedTopicIds(setOf())
awaitItem()
viewModel.followAuthor(

@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.UserDataRepository
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.Topic
@ -33,13 +34,15 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel
class TopicViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val topicsRepository: TopicsRepository,
private val userDataRepository: UserDataRepository,
topicsRepository: TopicsRepository,
newsRepository: NewsRepository
) : ViewModel() {
@ -47,7 +50,9 @@ class TopicViewModel @Inject constructor(
// Observe the followed topics, as they could change over time.
private val followedTopicIdsStream: Flow<Result<Set<String>>> =
topicsRepository.getFollowedTopicIdsStream().asResult()
userDataRepository.userDataStream
.map { it.followedTopics }
.asResult()
// Observe topic information
private val topic: Flow<Result<Topic>> = topicsRepository.getTopic(topicId).asResult()
@ -97,7 +102,7 @@ class TopicViewModel @Inject constructor(
fun followTopicToggle(followed: Boolean) {
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.testing.repository.TestNewsRepository
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.feature.topic.navigation.TopicDestination
import kotlinx.coroutines.flow.first
@ -40,6 +41,7 @@ class TopicViewModelTest {
@get:Rule
val dispatcherRule = TestDispatcherRule()
private val userDataRepository = TestUserDataRepository()
private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository()
private lateinit var viewModel: TopicViewModel
@ -49,6 +51,7 @@ class TopicViewModelTest {
viewModel = TopicViewModel(
savedStateHandle =
SavedStateHandle(mapOf(TopicDestination.topicIdArg to testInputTopics[0].topic.id)),
userDataRepository = userDataRepository,
topicsRepository = topicsRepository,
newsRepository = newsRepository
)
@ -59,7 +62,7 @@ class TopicViewModelTest {
viewModel.uiState.test {
awaitItem()
topicsRepository.sendTopics(testInputTopics.map(FollowableTopic::topic))
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
val item = awaitItem()
assertTrue(item.topicState is TopicUiState.Success)
@ -92,7 +95,7 @@ class TopicViewModelTest {
@Test
fun uiStateTopic_whenFollowedIdsSuccessAndTopicLoading_thenShowLoading() = runTest {
viewModel.uiState.test {
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
assertEquals(TopicUiState.Loading, awaitItem().topicState)
cancel()
}
@ -104,7 +107,7 @@ class TopicViewModelTest {
viewModel.uiState.test {
awaitItem()
topicsRepository.sendTopics(testInputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
val item = awaitItem()
assertTrue(item.topicState is TopicUiState.Success)
assertTrue(item.newsState is NewsUiState.Loading)
@ -118,7 +121,7 @@ class TopicViewModelTest {
viewModel.uiState.test {
awaitItem()
topicsRepository.sendTopics(testInputTopics.map { it.topic })
topicsRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
userDataRepository.setFollowedTopicIds(setOf(testInputTopics[1].topic.id))
newsRepository.sendNewsResources(sampleNewsResources)
val item = awaitItem()
assertTrue(item.topicState is TopicUiState.Success)
@ -134,7 +137,7 @@ class TopicViewModelTest {
awaitItem()
topicsRepository.sendTopics(testInputTopics.map { it.topic })
// 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)

Loading…
Cancel
Save