mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-18 04:27:02 +05:30
Switch to BottomNav backed by AndroidX navigation
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
parent
415d9e075d
commit
1aa0934104
6 changed files with 186 additions and 135 deletions
|
@ -4,37 +4,28 @@ import android.os.Bundle
|
|||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.Text
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumnForIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.FloatingActionButton
|
||||
import androidx.compose.material.IconToggleButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.BottomNavigation
|
||||
import androidx.compose.material.BottomNavigationItem
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Providers
|
||||
import androidx.compose.runtime.ambientOf
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.setContent
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.compose.KEY_ROUTE
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.navigate
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.msfjarvis.lobsters.compose.utils.IconResource
|
||||
import dev.msfjarvis.lobsters.data.LobstersViewModel
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.ui.LobstersItem
|
||||
import dev.msfjarvis.lobsters.ui.Destination
|
||||
import dev.msfjarvis.lobsters.ui.HottestPosts
|
||||
import dev.msfjarvis.lobsters.ui.LobstersTheme
|
||||
import dev.msfjarvis.lobsters.ui.savedTitleColor
|
||||
import dev.msfjarvis.lobsters.ui.SavedPosts
|
||||
import dev.msfjarvis.lobsters.urllauncher.UrlLauncher
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -63,126 +54,48 @@ fun LobstersApp(
|
|||
) {
|
||||
val urlLauncher = UrlLauncherAmbient.current
|
||||
val posts = viewModel.posts.collectAsState()
|
||||
val savedPosts = viewModel.savedPosts.collectAsState()
|
||||
val lastIndex = posts.value.lastIndex
|
||||
val showSaved = remember { mutableStateOf(false) }
|
||||
val navController = rememberNavController()
|
||||
val destinations = arrayOf(Destination.Hottest, Destination.Saved)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
LobstersTopAppBar(showSaved.value) {
|
||||
showSaved.value = !showSaved.value
|
||||
}
|
||||
},
|
||||
bodyContent = {
|
||||
val saved = showSaved.value
|
||||
if (saved && savedPosts.value.isEmpty()) {
|
||||
EmptyList(saved)
|
||||
} else if (!saved && posts.value.isEmpty()) {
|
||||
EmptyList(saved)
|
||||
} else {
|
||||
LobsterList(
|
||||
showSaved.value,
|
||||
savedPosts.value,
|
||||
posts.value,
|
||||
lastIndex,
|
||||
viewModel,
|
||||
urlLauncher
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = { LobstersFAB(showSaved.value, viewModel) },
|
||||
)
|
||||
}
|
||||
bottomBar = {
|
||||
BottomNavigation {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
|
||||
destinations.forEach { screen ->
|
||||
BottomNavigationItem(
|
||||
icon = {
|
||||
IconResource(
|
||||
resourceId = when (screen) {
|
||||
Destination.Hottest -> R.drawable.ic_whatshot_24px
|
||||
Destination.Saved -> R.drawable.ic_favorite_24px
|
||||
}
|
||||
)
|
||||
},
|
||||
label = { Text(screen.label) },
|
||||
selected = currentRoute == screen.route,
|
||||
onClick = {
|
||||
// This is the equivalent to popUpTo the start destination
|
||||
navController.popBackStack(navController.graph.startDestination, false)
|
||||
|
||||
@Composable
|
||||
private fun LobstersFAB(
|
||||
showSaved: Boolean,
|
||||
viewModel: LobstersViewModel
|
||||
) {
|
||||
if (!showSaved) {
|
||||
FloatingActionButton(
|
||||
onClick = { viewModel.refreshPosts() },
|
||||
modifier = Modifier
|
||||
) {
|
||||
IconResource(resourceId = R.drawable.ic_refresh_24px)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LobsterList(
|
||||
showSaved: Boolean,
|
||||
savedPosts: List<LobstersPost>,
|
||||
hottestPosts: List<LobstersPost>,
|
||||
lastIndex: Int,
|
||||
viewModel: LobstersViewModel,
|
||||
urlLauncher: UrlLauncher
|
||||
) {
|
||||
val hottestPostsListState = rememberLazyListState()
|
||||
val savedPostsListState = rememberLazyListState()
|
||||
|
||||
LazyColumnForIndexed(
|
||||
items = if (showSaved) savedPosts else hottestPosts,
|
||||
state = if (showSaved) savedPostsListState else hottestPostsListState,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) { index, item ->
|
||||
if (lastIndex == index && !showSaved) {
|
||||
viewModel.getMorePosts()
|
||||
}
|
||||
LobstersItem(
|
||||
item,
|
||||
linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) },
|
||||
commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) },
|
||||
saveAction = { post ->
|
||||
if (showSaved) {
|
||||
viewModel.removeSavedPost(post)
|
||||
} else {
|
||||
viewModel.savePost(post)
|
||||
// This if check gives us a "singleTop" behavior where we do not create a
|
||||
// second instance of the composable if we are already on that destination
|
||||
if (currentRoute != screen.route) {
|
||||
navController.navigate(screen.route)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyList(showSaved: Boolean) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (showSaved) {
|
||||
IconResource(
|
||||
R.drawable.ic_favorite_border_24px,
|
||||
tint = savedTitleColor,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
Text(stringResource(R.string.no_saved_posts))
|
||||
} else {
|
||||
IconResource(R.drawable.ic_sync_problem_24px, modifier = Modifier.padding(16.dp))
|
||||
Text(stringResource(R.string.loading))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LobstersTopAppBar(showSaved: Boolean, toggleAction: () -> Unit) {
|
||||
TopAppBar {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = if (showSaved) "Saved" else "Home",
|
||||
modifier = Modifier.padding(16.dp).align(Alignment.CenterStart),
|
||||
style = MaterialTheme.typography.h6,
|
||||
)
|
||||
IconToggleButton(
|
||||
checked = showSaved,
|
||||
onCheckedChange = { toggleAction.invoke() },
|
||||
modifier = Modifier.padding(8.dp).align(Alignment.CenterEnd),
|
||||
) {
|
||||
IconResource(
|
||||
resourceId = if (showSaved) R.drawable.ic_favorite_24px else R.drawable.ic_favorite_border_24px,
|
||||
tint = savedTitleColor,
|
||||
)
|
||||
NavHost(navController, startDestination = Destination.Hottest.route) {
|
||||
composable(Destination.Hottest.route) {
|
||||
HottestPosts(lastIndex = lastIndex, urlLauncher = urlLauncher , viewModel = viewModel)
|
||||
}
|
||||
composable(Destination.Saved.route) {
|
||||
SavedPosts(urlLauncher = urlLauncher, viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
app/src/main/java/dev/msfjarvis/lobsters/ui/Destination.kt
Normal file
12
app/src/main/java/dev/msfjarvis/lobsters/ui/Destination.kt
Normal file
|
@ -0,0 +1,12 @@
|
|||
package dev.msfjarvis.lobsters.ui
|
||||
|
||||
/**
|
||||
* Destinations for navigation within the app.
|
||||
*/
|
||||
sealed class Destination(
|
||||
val route: String,
|
||||
val label: String,
|
||||
) {
|
||||
object Hottest : Destination("hottest", "Hottest")
|
||||
object Saved : Destination("saved", "Saved")
|
||||
}
|
35
app/src/main/java/dev/msfjarvis/lobsters/ui/EmptyList.kt
Normal file
35
app/src/main/java/dev/msfjarvis/lobsters/ui/EmptyList.kt
Normal file
|
@ -0,0 +1,35 @@
|
|||
package dev.msfjarvis.lobsters.ui
|
||||
|
||||
import androidx.compose.foundation.Text
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.lobsters.R
|
||||
import dev.msfjarvis.lobsters.compose.utils.IconResource
|
||||
|
||||
@Composable
|
||||
fun EmptyList(saved: Boolean) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (saved) {
|
||||
IconResource(
|
||||
R.drawable.ic_favorite_border_24px,
|
||||
tint = savedTitleColor,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
Text(stringResource(R.string.no_saved_posts))
|
||||
} else {
|
||||
IconResource(R.drawable.ic_sync_problem_24px, modifier = Modifier.padding(16.dp))
|
||||
Text(stringResource(R.string.loading))
|
||||
}
|
||||
}
|
||||
}
|
42
app/src/main/java/dev/msfjarvis/lobsters/ui/HottestPosts.kt
Normal file
42
app/src/main/java/dev/msfjarvis/lobsters/ui/HottestPosts.kt
Normal file
|
@ -0,0 +1,42 @@
|
|||
package dev.msfjarvis.lobsters.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumnForIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.lobsters.data.LobstersViewModel
|
||||
import dev.msfjarvis.lobsters.urllauncher.UrlLauncher
|
||||
|
||||
@Composable
|
||||
fun HottestPosts(
|
||||
lastIndex: Int,
|
||||
urlLauncher: UrlLauncher,
|
||||
viewModel: LobstersViewModel,
|
||||
) {
|
||||
val posts by viewModel.posts.collectAsState()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
if (posts.isEmpty()) {
|
||||
EmptyList(saved = false)
|
||||
} else {
|
||||
LazyColumnForIndexed(
|
||||
items = posts,
|
||||
state = listState,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) { index, item ->
|
||||
if (lastIndex == index) {
|
||||
viewModel.getMorePosts()
|
||||
}
|
||||
LobstersItem(
|
||||
post = item,
|
||||
linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) },
|
||||
commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) },
|
||||
saveAction = viewModel::savePost
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
40
app/src/main/java/dev/msfjarvis/lobsters/ui/SavedPosts.kt
Normal file
40
app/src/main/java/dev/msfjarvis/lobsters/ui/SavedPosts.kt
Normal file
|
@ -0,0 +1,40 @@
|
|||
package dev.msfjarvis.lobsters.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumnFor
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.lobsters.data.LobstersViewModel
|
||||
import dev.msfjarvis.lobsters.urllauncher.UrlLauncher
|
||||
|
||||
@Composable
|
||||
fun SavedPosts(
|
||||
urlLauncher: UrlLauncher,
|
||||
viewModel: LobstersViewModel,
|
||||
) {
|
||||
val posts by viewModel.savedPosts.collectAsState()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
if (posts.isEmpty()) {
|
||||
EmptyList(saved = true)
|
||||
} else {
|
||||
LazyColumnFor(
|
||||
items = posts,
|
||||
state = listState,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) { item ->
|
||||
LobstersItem(
|
||||
post = item,
|
||||
linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) },
|
||||
commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) },
|
||||
saveAction = viewModel::removeSavedPost
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
9
app/src/main/res/drawable/ic_whatshot_24px.xml
Normal file
9
app/src/main/res/drawable/ic_whatshot_24px.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.5,0.67s0.74,2.65 0.74,4.8c0,2.06 -1.35,3.73 -3.41,3.73 -2.07,0 -3.63,-1.67 -3.63,-3.73l0.03,-0.36C5.21,7.51 4,10.62 4,14c0,4.42 3.58,8 8,8s8,-3.58 8,-8C20,8.61 17.41,3.8 13.5,0.67zM11.71,19c-1.78,0 -3.22,-1.4 -3.22,-3.14 0,-1.62 1.05,-2.76 2.81,-3.12 1.77,-0.36 3.6,-1.21 4.62,-2.58 0.39,1.29 0.59,2.65 0.59,4.04 0,2.65 -2.15,4.8 -4.8,4.8z"/>
|
||||
</vector>
|
Loading…
Add table
Add a link
Reference in a new issue