Switch to BottomNav backed by AndroidX navigation

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2020-10-29 14:59:54 +05:30
parent 415d9e075d
commit 1aa0934104
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
6 changed files with 186 additions and 135 deletions

View file

@ -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)
}
}
}

View 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")
}

View 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))
}
}
}

View 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
)
}
}
}

View 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
)
}
}
}

View 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>