-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bookmarking Stories to Read Later (#74)
With this diff, I'm taking a pass at bookmarking functionality. This required adding some infrastructure, namely [Room](https://developer.android.com/training/data-storage/room) as our local database. The action is wired up to a long press for now, which will allow you to bookmark / unbookmark a story. There is also a separate screen and bottom nav item for bookmarks so we can easily navigate there.
- Loading branch information
Showing
23 changed files
with
796 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
android/app/src/main/java/com/emergetools/hackernews/AppActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package com.emergetools.hackernews | ||
|
||
import android.os.Bundle | ||
import androidx.activity.ComponentActivity | ||
import androidx.activity.compose.setContent | ||
import androidx.activity.enableEdgeToEdge | ||
import androidx.browser.customtabs.CustomTabsIntent | ||
import androidx.compose.animation.fadeOut | ||
import androidx.compose.animation.slideIn | ||
import androidx.compose.animation.slideOut | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.material3.Icon | ||
import androidx.compose.material3.NavigationBar | ||
import androidx.compose.material3.NavigationBarItem | ||
import androidx.compose.material3.Scaffold | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.CompositionLocalProvider | ||
import androidx.compose.runtime.collectAsState | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.staticCompositionLocalOf | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.res.painterResource | ||
import androidx.compose.ui.unit.IntOffset | ||
import androidx.lifecycle.viewmodel.compose.viewModel | ||
import androidx.navigation.NavDestination | ||
import androidx.navigation.NavHostController | ||
import androidx.navigation.compose.NavHost | ||
import androidx.navigation.compose.rememberNavController | ||
import com.emergetools.hackernews.data.ChromeTabsProvider | ||
import com.emergetools.hackernews.data.LocalCustomTabsIntent | ||
import com.emergetools.hackernews.features.bookmarks.BookmarksNavigation | ||
import com.emergetools.hackernews.features.bookmarks.bookmarksRoutes | ||
import com.emergetools.hackernews.features.comments.commentsRoutes | ||
import com.emergetools.hackernews.features.settings.settingsRoutes | ||
import com.emergetools.hackernews.features.stories.Stories | ||
import com.emergetools.hackernews.features.stories.StoriesDestinations.Feed | ||
import com.emergetools.hackernews.features.stories.storiesGraph | ||
import com.emergetools.hackernews.ui.theme.HackerNewsTheme | ||
|
||
class MainActivity : ComponentActivity() { | ||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
enableEdgeToEdge() | ||
setContent { | ||
HackerNewsTheme { | ||
ChromeTabsProvider { | ||
App() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
fun rememberNavController( | ||
onDestinationChanged: (NavDestination) -> Unit | ||
): NavHostController { | ||
return rememberNavController().apply { | ||
addOnDestinationChangedListener { _, destination, _ -> | ||
onDestinationChanged(destination) | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
fun App() { | ||
val model = viewModel<AppViewModel>() | ||
val state by model.state.collectAsState() | ||
val navController = rememberNavController() { destination -> | ||
model.actions(AppAction.DestinationChanged(destination)) | ||
} | ||
Scaffold( | ||
bottomBar = { | ||
NavigationBar { | ||
state.navItems.forEach { navItem -> | ||
NavigationBarItem( | ||
selected = navItem.selected, | ||
onClick = { | ||
model.actions(AppAction.NavItemSelected(navItem)) | ||
|
||
navController.navigate(navItem.route) { | ||
popUpTo<Feed> { | ||
saveState = true | ||
} | ||
launchSingleTop = true | ||
restoreState = true | ||
} | ||
}, | ||
icon = { | ||
Icon( | ||
painter = painterResource(navItem.icon), | ||
contentDescription = navItem.label | ||
) | ||
}, | ||
) | ||
} | ||
} | ||
} | ||
) { innerPadding -> | ||
NavHost( | ||
modifier = Modifier | ||
.fillMaxSize() | ||
.padding(innerPadding), | ||
navController = navController, | ||
enterTransition = { slideIn { IntOffset(x = it.width, y = 0) } }, | ||
exitTransition = { slideOut { IntOffset(x = -it.width / 3, y = 0) } + fadeOut() }, | ||
popEnterTransition = { slideIn { IntOffset(x = -it.width, y = 0) } }, | ||
popExitTransition = { slideOut { IntOffset(x = it.width, y = 0) } }, | ||
startDestination = Stories | ||
) { | ||
storiesGraph(navController) | ||
commentsRoutes() | ||
bookmarksRoutes(navController) | ||
settingsRoutes() | ||
} | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
android/app/src/main/java/com/emergetools/hackernews/AppDomain.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package com.emergetools.hackernews | ||
|
||
import androidx.annotation.DrawableRes | ||
import androidx.compose.material.icons.Icons | ||
import androidx.compose.material.icons.rounded.Menu | ||
import androidx.compose.material.icons.rounded.Settings | ||
import androidx.compose.ui.graphics.vector.ImageVector | ||
import androidx.lifecycle.ViewModel | ||
import androidx.navigation.NavDestination | ||
import com.emergetools.hackernews.features.bookmarks.BookmarksDestinations | ||
import com.emergetools.hackernews.features.bookmarks.BookmarksDestinations.Bookmarks | ||
import com.emergetools.hackernews.features.settings.SettingsDestinations.Settings | ||
import com.emergetools.hackernews.features.stories.Stories | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.asStateFlow | ||
import kotlinx.coroutines.flow.update | ||
|
||
data class NavItem( | ||
@DrawableRes | ||
val icon: Int, | ||
val label: String, | ||
val route: Any, | ||
val selected: Boolean | ||
) | ||
|
||
data class AppState( | ||
val navItems: List<NavItem> = listOf( | ||
NavItem( | ||
icon = R.drawable.ic_feed, | ||
label = "Feed", | ||
route = Stories, | ||
selected = true | ||
), | ||
NavItem( | ||
icon = R.drawable.ic_bookmarks, | ||
label = "Bookmarks", | ||
route = Bookmarks, | ||
selected = false | ||
), | ||
NavItem( | ||
icon = R.drawable.ic_settings, | ||
label = "Settings", | ||
route = Settings, | ||
selected = false | ||
), | ||
) | ||
) { | ||
val selectedItem = navItems.first { it.selected } | ||
val topLevelRoutes = navItems.associateBy { it.route.javaClass.simpleName } | ||
} | ||
|
||
sealed interface AppAction { | ||
data class NavItemSelected(val item: NavItem) : AppAction | ||
data class DestinationChanged(val destination: NavDestination) : AppAction | ||
} | ||
|
||
class AppViewModel : ViewModel() { | ||
private val internalState = MutableStateFlow(AppState()) | ||
val state = internalState.asStateFlow() | ||
|
||
fun actions(action: AppAction) { | ||
when (action) { | ||
is AppAction.NavItemSelected -> { | ||
if (action.item != internalState.value.selectedItem) { | ||
internalState.update { current -> | ||
current.copy( | ||
navItems = current.navItems.map { item -> | ||
if (action.item == item) { | ||
item.copy(selected = true) | ||
} else { | ||
item.copy(selected = false) | ||
} | ||
} | ||
) | ||
} | ||
} | ||
} | ||
|
||
is AppAction.DestinationChanged -> { | ||
// TODO: figure out a better way to sync the current destination with bottom nav | ||
val currentState = internalState.value | ||
val parent = action.destination.parent?.route | ||
val route = parent ?: action.destination.route | ||
currentState.topLevelRoutes.keys.forEach { key -> | ||
if (route != null && route.contains(key)) { | ||
val item = currentState.topLevelRoutes[key] | ||
if (item != currentState.selectedItem) { | ||
// select bottom nav | ||
internalState.value = currentState.copy( | ||
navItems = currentState.navItems.map { navItem -> | ||
if (item == navItem) { | ||
navItem.copy(selected = true) | ||
} else { | ||
navItem.copy(selected = false) | ||
} | ||
} | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 0 additions & 56 deletions
56
android/app/src/main/java/com/emergetools/hackernews/MainActivity.kt
This file was deleted.
Oops, something went wrong.
23 changes: 23 additions & 0 deletions
23
android/app/src/main/java/com/emergetools/hackernews/data/ChromeTabs.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.emergetools.hackernews.data | ||
|
||
import androidx.browser.customtabs.CustomTabsIntent | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.CompositionLocalProvider | ||
import androidx.compose.runtime.staticCompositionLocalOf | ||
|
||
private val sharedIntent = CustomTabsIntent.Builder().build() | ||
val LocalCustomTabsIntent = staticCompositionLocalOf<CustomTabsIntent> { | ||
error("LocalCustomTabsIntent not provided") | ||
} | ||
|
||
@Composable | ||
fun ChromeTabsProvider( | ||
content: @Composable () -> Unit | ||
) { | ||
CompositionLocalProvider( | ||
LocalCustomTabsIntent provides sharedIntent | ||
) { | ||
content() | ||
} | ||
} | ||
|
Oops, something went wrong.