Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JetNews] feature/favorite developed. #1519

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions JetNews/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -18,6 +18,8 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose)
id("androidx.room")
id("com.google.devtools.ksp")
}

android {
@@ -69,6 +71,10 @@ android {
compose = true
}

room {
schemaDirectory("$projectDir/schemas")
}

packaging.resources {
// Multiple dependency bring these files in. Exclude them to enable
// our test APK to build (has no effect on our AARs)
@@ -108,6 +114,12 @@ dependencies {
implementation(libs.androidx.glance.appwidget)
implementation(libs.androidx.glance.material3)

implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)

implementation("com.google.code.gson:gson:2.10.1")

implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.livedata.ktx)
Original file line number Diff line number Diff line change
@@ -41,6 +41,11 @@ interface PostsRepository {
*/
fun observeFavorites(): Flow<Set<String>>

/**
* Observe the current marks
*/
fun observeMarks(): Flow<Set<String>>

/**
* Observe the posts feed.
*/
@@ -50,4 +55,9 @@ interface PostsRepository {
* Toggle a postId to be a favorite or not.
*/
suspend fun toggleFavorite(postId: String)

/**
* Toggle a postId to be a mark or not.
*/
suspend fun toggleMark(postId: String)
}
Original file line number Diff line number Diff line change
@@ -38,6 +38,8 @@ class BlockingFakePostsRepository : PostsRepository {
// for now, keep the favorites in memory
private val favorites = MutableStateFlow<Set<String>>(setOf())

private val marks = MutableStateFlow<Set<String>>(setOf())

private val postsFeed = MutableStateFlow<PostsFeed?>(null)

override suspend fun getPost(postId: String?): Result<Post> {
@@ -57,9 +59,16 @@ class BlockingFakePostsRepository : PostsRepository {
}

override fun observeFavorites(): Flow<Set<String>> = favorites

override fun observeMarks(): Flow<Set<String>> = marks

override fun observePostsFeed(): Flow<PostsFeed?> = postsFeed

override suspend fun toggleFavorite(postId: String) {
favorites.update { it.addOrRemove(postId) }
}

override suspend fun toggleMark(postId: String) {
marks.update { it.addOrRemove(postId) }
}
}
Original file line number Diff line number Diff line change
@@ -37,6 +37,8 @@ class FakePostsRepository : PostsRepository {
// for now, store these in memory
private val favorites = MutableStateFlow<Set<String>>(setOf())

private val marks = MutableStateFlow<Set<String>>(setOf())

private val postsFeed = MutableStateFlow<PostsFeed?>(null)

// Used to make suspend functions that read and update state safe to call from any thread
@@ -65,6 +67,7 @@ class FakePostsRepository : PostsRepository {
}

override fun observeFavorites(): Flow<Set<String>> = favorites
override fun observeMarks(): Flow<Set<String>> = marks
override fun observePostsFeed(): Flow<PostsFeed?> = postsFeed

override suspend fun toggleFavorite(postId: String) {
@@ -73,6 +76,12 @@ class FakePostsRepository : PostsRepository {
}
}

override suspend fun toggleMark(postId: String) {
marks.update {
it.addOrRemove(postId)
}
}

// used to drive "random" failure in a predictable pattern, making the first request always
// succeed
private var requestCount = 0
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ListAlt
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.ListAlt
import androidx.compose.material3.DrawerState
@@ -67,7 +68,7 @@ fun AppDrawer(
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.interests_title)) },
icon = { Icon(Icons.Filled.ListAlt, null) },
icon = { Icon(Icons.AutoMirrored.Filled.ListAlt, null) },
selected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE,
onClick = { navigateToInterests(); closeDrawer() },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar
@@ -86,6 +87,8 @@ fun ArticleScreen(
onBack: () -> Unit,
isFavorite: Boolean,
onToggleFavorite: () -> Unit,
isMarked: Boolean,
onToggleMark: () -> Unit,
modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState()
) {
@@ -103,7 +106,7 @@ fun ArticleScreen(
if (!isExpandedScreen) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.cd_navigate_up),
tint = MaterialTheme.colorScheme.primary
)
@@ -115,7 +118,7 @@ fun ArticleScreen(
if (!isExpandedScreen) {
BottomAppBar(
actions = {
FavoriteButton(onClick = { showUnimplementedActionDialog = true })
FavoriteButton(isFavorite = isMarked, onClick = onToggleMark)
BookmarkButton(isBookmarked = isFavorite, onClick = onToggleFavorite)
ShareButton(onClick = { sharePost(post, context) })
TextSettingsButton(onClick = { showUnimplementedActionDialog = true })
@@ -248,7 +251,7 @@ fun PreviewArticleDrawer() {
val post = runBlocking {
(BlockingFakePostsRepository().getPost(post3.id) as Result.Success).data
}
ArticleScreen(post, false, {}, false, {})
ArticleScreen(post, false, {}, false, {}, false, {})
}
}

@@ -265,6 +268,6 @@ fun PreviewArticleNavRail() {
val post = runBlocking {
(BlockingFakePostsRepository().getPost(post3.id) as Result.Success).data
}
ArticleScreen(post, true, {}, false, {})
ArticleScreen(post, true, {}, false, {}, false, {})
}
}
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ListAlt
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.ListAlt
import androidx.compose.material3.Icon
@@ -66,7 +67,7 @@ fun AppNavRail(
NavigationRailItem(
selected = currentRoute == JetnewsDestinations.INTERESTS_ROUTE,
onClick = navigateToInterests,
icon = { Icon(Icons.Filled.ListAlt, stringResource(R.string.interests_title)) },
icon = { Icon(Icons.AutoMirrored.Filled.ListAlt, stringResource(R.string.interests_title)) },
label = { Text(stringResource(R.string.interests_title)) },
alwaysShowLabel = false
)
Original file line number Diff line number Diff line change
@@ -54,6 +54,7 @@ fun HomeRoute(
uiState = uiState,
isExpandedScreen = isExpandedScreen,
onToggleFavorite = { homeViewModel.toggleFavourite(it) },
onToggleMark = { homeViewModel.toggleMark(it) },
onSelectPost = { homeViewModel.selectArticle(it) },
onRefreshPosts = { homeViewModel.refreshPosts() },
onErrorDismiss = { homeViewModel.errorShown(it) },
@@ -87,6 +88,7 @@ fun HomeRoute(
uiState: HomeUiState,
isExpandedScreen: Boolean,
onToggleFavorite: (String) -> Unit,
onToggleMark: (String) -> Unit,
onSelectPost: (String) -> Unit,
onRefreshPosts: () -> Unit,
onErrorDismiss: (Long) -> Unit,
@@ -116,6 +118,7 @@ fun HomeRoute(
uiState = uiState,
showTopAppBar = !isExpandedScreen,
onToggleFavorite = onToggleFavorite,
onToggleMark = onToggleMark,
onSelectPost = onSelectPost,
onRefreshPosts = onRefreshPosts,
onErrorDismiss = onErrorDismiss,
@@ -146,6 +149,7 @@ fun HomeRoute(
// Guaranteed by above condition for home screen type
check(uiState is HomeUiState.HasPosts)

// TODO: This
ArticleScreen(
post = uiState.selectedPost,
isExpandedScreen = isExpandedScreen,
@@ -154,6 +158,10 @@ fun HomeRoute(
onToggleFavorite = {
onToggleFavorite(uiState.selectedPost.id)
},
isMarked = uiState.marks.contains(uiState.selectedPost.id),
onToggleMark = {
onToggleMark(uiState.selectedPost.id)
},
lazyListState = articleDetailLazyListStates.getValue(
uiState.selectedPost.id
)
Original file line number Diff line number Diff line change
@@ -125,6 +125,7 @@ fun HomeFeedWithArticleDetailsScreen(
uiState: HomeUiState,
showTopAppBar: Boolean,
onToggleFavorite: (String) -> Unit,
onToggleMark: (String) -> Unit,
onSelectPost: (String) -> Unit,
onRefreshPosts: () -> Unit,
onErrorDismiss: (Long) -> Unit,
@@ -194,6 +195,8 @@ fun HomeFeedWithArticleDetailsScreen(
PostTopBar(
isFavorite = hasPostsUiState.favorites.contains(detailPost.id),
onToggleFavorite = { onToggleFavorite(detailPost.id) },
isMark = hasPostsUiState.marks.contains(detailPost.id),
onToggleMark = { onToggleMark(detailPost.id) },
onSharePost = { sharePost(detailPost, context) },
modifier = Modifier
.windowInsetsPadding(WindowInsets.safeDrawing)
@@ -667,6 +670,8 @@ private fun submitSearch(
private fun PostTopBar(
isFavorite: Boolean,
onToggleFavorite: () -> Unit,
isMark: Boolean,
onToggleMark: () -> Unit,
onSharePost: () -> Unit,
modifier: Modifier = Modifier
) {
@@ -676,7 +681,7 @@ private fun PostTopBar(
modifier = modifier.padding(end = 16.dp)
) {
Row(Modifier.padding(horizontal = 8.dp)) {
FavoriteButton(onClick = { /* Functionality not available */ })
FavoriteButton(isFavorite = isMark, onClick = onToggleMark)
BookmarkButton(isBookmarked = isFavorite, onClick = onToggleFavorite)
ShareButton(onClick = onSharePost)
TextSettingsButton(onClick = { /* Functionality not available */ })
@@ -750,6 +755,7 @@ fun PreviewHomeListDrawerScreen() {
selectedPost = postsFeed.highlightedPost,
isArticleOpen = false,
favorites = emptySet(),
marks = emptySet(),
isLoading = false,
errorMessages = emptyList(),
searchInput = ""
@@ -786,6 +792,7 @@ fun PreviewHomeListNavRailScreen() {
selectedPost = postsFeed.highlightedPost,
isArticleOpen = false,
favorites = emptySet(),
marks = emptySet(),
isLoading = false,
errorMessages = emptyList(),
searchInput = ""
@@ -818,12 +825,14 @@ fun PreviewHomeListDetailScreen() {
selectedPost = postsFeed.highlightedPost,
isArticleOpen = false,
favorites = emptySet(),
marks = emptySet(),
isLoading = false,
errorMessages = emptyList(),
searchInput = ""
),
showTopAppBar = true,
onToggleFavorite = {},
onToggleMark = {},
onSelectPost = {},
onRefreshPosts = {},
onErrorDismiss = {},
Original file line number Diff line number Diff line change
@@ -67,6 +67,7 @@ sealed interface HomeUiState {
val selectedPost: Post,
val isArticleOpen: Boolean,
val favorites: Set<String>,
val marks: Set<String>,
override val isLoading: Boolean,
override val errorMessages: List<ErrorMessage>,
override val searchInput: String
@@ -81,6 +82,7 @@ private data class HomeViewModelState(
val selectedPostId: String? = null, // TODO back selectedPostId in a SavedStateHandle
val isArticleOpen: Boolean = false,
val favorites: Set<String> = emptySet(),
val marks: Set<String> = emptySet(),
val isLoading: Boolean = false,
val errorMessages: List<ErrorMessage> = emptyList(),
val searchInput: String = "",
@@ -108,6 +110,7 @@ private data class HomeViewModelState(
} ?: postsFeed.highlightedPost,
isArticleOpen = isArticleOpen,
favorites = favorites,
marks = marks,
isLoading = isLoading,
errorMessages = errorMessages,
searchInput = searchInput
@@ -149,6 +152,12 @@ class HomeViewModel(
viewModelState.update { it.copy(favorites = favorites) }
}
}

viewModelScope.launch {
postsRepository.observeMarks().collect { marks ->
viewModelState.update { it.copy(marks = marks) }
}
}
}

/**
@@ -184,6 +193,15 @@ class HomeViewModel(
}
}

/**
* Toggle mark of a post
*/
fun toggleMark(postId: String) {
viewModelScope.launch {
postsRepository.toggleMark(postId)
}
}

/**
* Selects the given article to view more information about it.
*/
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.ThumbUpAlt
import androidx.compose.material.icons.filled.ThumbUpOffAlt
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -32,16 +33,32 @@ import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import com.example.jetnews.R

// FavoriteButton
@Composable
fun FavoriteButton(onClick: () -> Unit) {
IconButton(onClick) {
fun FavoriteButton(
isFavorite: Boolean,
onClick: () -> Unit
) {
val clickLabel = stringResource(
if (isFavorite) R.string.unfavorite else R.string.favorite
)
IconToggleButton(
checked = isFavorite,
onCheckedChange = { onClick() },
modifier = Modifier.semantics {
// Use a custom click label that accessibility services can communicate to the user.
// We only want to override the label, not the actual action, so for the action we pass null.
this.onClick(label = clickLabel, action = null)
}
) {
Icon(
imageVector = Icons.Filled.ThumbUpOffAlt,
contentDescription = stringResource(R.string.cd_add_to_favorites)
imageVector = if (isFavorite) Icons.Filled.ThumbUpAlt else Icons.Filled.ThumbUpOffAlt,
contentDescription = null // handled by click label of parent
)
}
}

// BookmarkButton
@Composable
fun BookmarkButton(
isBookmarked: Boolean,
2 changes: 2 additions & 0 deletions JetNews/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -23,6 +23,8 @@
<string name="cd_more_actions">More actions</string>
<string name="unbookmark">unbookmark</string>
<string name="bookmark">bookmark</string>
<string name="unfavorite">unfavorite</string>
<string name="favorite">favorite</string>
<string name="cd_search">Search</string>
<string name="cd_interests">Interests</string>
<string name="published_in">Published in: \n%1$s</string>
2 changes: 2 additions & 0 deletions JetNews/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@ plugins {
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.compose) apply false
id("androidx.room") version "2.6.0" apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}

apply("${project.rootDir}/buildscripts/toml-updater-config.gradle")