Persist user bookmarks

Change-Id: Ib4422afe1625c360a8eb733a9a849db2984f25c9
pull/147/head
Adetunji Dahunsi 2 years ago
parent 3558001077
commit 62739d36cc

@ -39,4 +39,7 @@ class OfflineFirstUserDataRepository @Inject constructor(
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) =
niaPreferencesDataSource.toggleFollowedAuthorId(followedAuthorId, followed)
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) =
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
}

@ -45,4 +45,9 @@ interface UserDataRepository {
* Toggles the user's newly followed/unfollowed author
*/
suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean)
/**
* Updates the bookmarked status for a news resource
*/
suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
}

@ -49,4 +49,8 @@ class FakeUserDataRepository @Inject constructor(
override suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) {
niaPreferencesDataSource.toggleFollowedAuthorId(followedAuthorId, followed)
}
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
niaPreferencesDataSource.toggleNewsResourceBookmark(newsResourceId, bookmarked)
}
}

@ -47,7 +47,7 @@ class OfflineFirstUserDataRepositoryTest {
}
@Test
fun offlineFirstTopicsRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() =
fun offlineFirstUserDataRepository_toggle_followed_topics_logic_delegates_to_nia_preferences() =
runTest {
subject.toggleFollowedTopicId(followedTopicId = "0", followed = true)
@ -68,7 +68,8 @@ class OfflineFirstUserDataRepositoryTest {
)
assertEquals(
niaPreferencesDataSource.followedTopicIds
niaPreferencesDataSource.userDataStream
.map { it.followedTopics }
.first(),
subject.userDataStream
.map { it.followedTopics }
@ -77,7 +78,7 @@ class OfflineFirstUserDataRepositoryTest {
}
@Test
fun offlineFirstTopicsRepository_set_followed_topics_logic_delegates_to_nia_preferences() =
fun offlineFirstUserDataRepository_set_followed_topics_logic_delegates_to_nia_preferences() =
runTest {
subject.setFollowedTopicIds(followedTopicIds = setOf("1", "2"))
@ -89,11 +90,43 @@ class OfflineFirstUserDataRepositoryTest {
)
assertEquals(
niaPreferencesDataSource.followedTopicIds
niaPreferencesDataSource.userDataStream
.map { it.followedTopics }
.first(),
subject.userDataStream
.map { it.followedTopics }
.first()
)
}
@Test
fun offlineFirstUserDataRepository_bookmark_news_resource_logic_delegates_to_nia_preferences() =
runTest {
subject.updateNewsResourceBookmark(newsResourceId = "0", bookmarked = true)
assertEquals(
setOf("0"),
subject.userDataStream
.map { it.bookmarkedNewsResources }
.first()
)
subject.updateNewsResourceBookmark(newsResourceId = "1", bookmarked = true)
assertEquals(
setOf("0", "1"),
subject.userDataStream
.map { it.bookmarkedNewsResources }
.first()
)
assertEquals(
niaPreferencesDataSource.userDataStream
.map { it.bookmarkedNewsResources }
.first(),
subject.userDataStream
.map { it.bookmarkedNewsResources }
.first()
)
}
}

@ -18,55 +18,69 @@ package com.google.samples.apps.nowinandroid.core.datastore
import android.util.Log
import androidx.datastore.core.DataStore
import com.google.protobuf.kotlin.DslList
import com.google.protobuf.kotlin.DslProxy
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import java.io.IOException
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retry
class NiaPreferencesDataSource @Inject constructor(
private val userPreferences: DataStore<UserPreferences>
) {
suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) {
try {
userPreferences.updateData {
it.copy {
this.followedTopicIds.clear()
this.followedTopicIds.addAll(followedTopicIds)
}
}
} catch (ioException: IOException) {
Log.e("NiaPreferences", "Failed to update user preferences", ioException)
}
}
suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) {
try {
userPreferences.updateData {
it.copy {
val current =
if (followed) {
followedTopicIds + followedTopicId
} else {
followedTopicIds - followedTopicId
}
this.followedTopicIds.clear()
this.followedTopicIds.addAll(current)
}
}
} catch (ioException: IOException) {
Log.e("NiaPreferences", "Failed to update user preferences", ioException)
}
val userDataStream = userPreferences.data
.map {
UserData(
bookmarkedNewsResources = it.bookmarkedNewsResourceIdsList.toSet(),
followedTopics = it.followedTopicIdsList.toSet(),
followedAuthors = it.followedAuthorIdsList.toSet(),
)
}
val followedTopicIds: Flow<Set<String>> = userPreferences.data
.retry {
Log.e("NiaPreferences", "Failed to read user preferences", it)
true
}
.map { it.followedTopicIdsList.toSet() }
suspend fun setFollowedTopicIds(followedTopicIds: Set<String>) =
userPreferences.setList(
listGetter = { it.followedTopicIds },
listModifier = { followedTopicIds.toList() },
clear = { it.clear() },
addAll = { dslList, editedList -> dslList.addAll(editedList) }
)
suspend fun toggleFollowedTopicId(followedTopicId: String, followed: Boolean) =
userPreferences.editList(
add = followed,
value = followedTopicId,
listGetter = { it.followedTopicIds },
clear = { it.clear() },
addAll = { dslList, editedList -> dslList.addAll(editedList) }
)
suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) =
userPreferences.setList(
listGetter = { it.followedAuthorIds },
listModifier = { followedAuthorIds.toList() },
clear = { it.clear() },
addAll = { dslList, editedList -> dslList.addAll(editedList) }
)
suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) =
userPreferences.editList(
add = followed,
value = followedAuthorId,
listGetter = { it.followedAuthorIds },
clear = { it.clear() },
addAll = { dslList, editedList -> dslList.addAll(editedList) }
)
suspend fun toggleNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) =
userPreferences.editList(
add = bookmarked,
value = newsResourceId,
listGetter = { it.bookmarkedNewsResourceIds },
clear = { it.clear() },
addAll = { dslList, editedList -> dslList.addAll(editedList) }
)
suspend fun getChangeListVersions() = userPreferences.data
.map {
@ -106,51 +120,47 @@ class NiaPreferencesDataSource @Inject constructor(
}
}
suspend fun setFollowedAuthorIds(followedAuthorIds: Set<String>) {
try {
userPreferences.updateData {
it.copy {
this.followedAuthorIds.clear()
this.followedAuthorIds.addAll(followedAuthorIds)
}
}
} catch (ioException: IOException) {
Log.e("NiaPreferences", "Failed to update user preferences", ioException)
}
/**
* Adds or removes [value] from the [DslList] provided by [listGetter]
*/
private suspend fun <T : DslProxy> DataStore<UserPreferences>.editList(
add: Boolean,
value: String,
listGetter: (UserPreferencesKt.Dsl) -> DslList<String, T>,
clear: UserPreferencesKt.Dsl.(DslList<String, T>) -> Unit,
addAll: UserPreferencesKt.Dsl.(DslList<String, T>, Iterable<String>) -> Unit
) {
setList(
listGetter = listGetter,
listModifier = { currentList ->
if (add) currentList + value
else currentList - value
},
clear = clear,
addAll = addAll
)
}
suspend fun toggleFollowedAuthorId(followedAuthorId: String, followed: Boolean) {
/**
* Sets the value provided by [listModifier] into the [DslList] read by [listGetter]
*/
private suspend fun <T : DslProxy> DataStore<UserPreferences>.setList(
listGetter: (UserPreferencesKt.Dsl) -> DslList<String, T>,
listModifier: (DslList<String, T>) -> List<String>,
clear: UserPreferencesKt.Dsl.(DslList<String, T>) -> Unit,
addAll: UserPreferencesKt.Dsl.(DslList<String, T>, List<String>) -> Unit
) {
try {
userPreferences.updateData {
updateData {
it.copy {
val current =
if (followed) {
followedAuthorIds + followedAuthorId
} else {
followedAuthorIds - followedAuthorId
}
this.followedAuthorIds.clear()
this.followedAuthorIds.addAll(current)
val dslList = listGetter(this)
val newList = listModifier(dslList)
clear(dslList)
addAll(dslList, newList)
}
}
} catch (ioException: IOException) {
Log.e("NiaPreferences", "Failed to update user preferences", ioException)
}
}
val followedAuthorIds: Flow<Set<String>> = userPreferences.data
.retry {
Log.e("NiaPreferences", "Failed to read user preferences", it)
true
}
.map { it.followedAuthorIdsList.toSet() }
val userDataStream = userPreferences.data
.map {
UserData(
bookmarkedNewsResources = emptySet(),
followedTopics = it.followedTopicIdsList.toSet(),
followedAuthors = it.followedAuthorIdsList.toSet(),
)
}
}

@ -30,4 +30,5 @@ message UserPreferences {
bool has_done_int_to_string_id_migration = 8;
repeated string followed_topic_ids = 9;
repeated string followed_author_ids = 10;
repeated string bookmarked_news_resource_ids = 11;
}

@ -65,6 +65,15 @@ class TestUserDataRepository : UserDataRepository {
}
}
override suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean) {
currentUserData.let { current ->
val bookmarkedNews = if (bookmarked) current.bookmarkedNewsResources + newsResourceId
else current.bookmarkedNewsResources - newsResourceId
_userData.tryEmit(current.copy(bookmarkedNewsResources = bookmarkedNews))
}
}
/**
* A test-only API to allow querying the current followed topics.
*/

Loading…
Cancel
Save