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.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.Text import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.Arrangement import androidx.compose.material.BottomNavigation
import androidx.compose.foundation.layout.Box import androidx.compose.material.BottomNavigationItem
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.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers import androidx.compose.runtime.Providers
import androidx.compose.runtime.ambientOf import androidx.compose.runtime.ambientOf
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.setContent import androidx.compose.ui.platform.setContent
import androidx.compose.ui.res.stringResource import androidx.navigation.compose.KEY_ROUTE
import androidx.compose.ui.unit.dp 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 dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.lobsters.compose.utils.IconResource import dev.msfjarvis.lobsters.compose.utils.IconResource
import dev.msfjarvis.lobsters.data.LobstersViewModel import dev.msfjarvis.lobsters.data.LobstersViewModel
import dev.msfjarvis.lobsters.model.LobstersPost import dev.msfjarvis.lobsters.ui.Destination
import dev.msfjarvis.lobsters.ui.LobstersItem import dev.msfjarvis.lobsters.ui.HottestPosts
import dev.msfjarvis.lobsters.ui.LobstersTheme 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 dev.msfjarvis.lobsters.urllauncher.UrlLauncher
import javax.inject.Inject import javax.inject.Inject
@ -63,126 +54,48 @@ fun LobstersApp(
) { ) {
val urlLauncher = UrlLauncherAmbient.current val urlLauncher = UrlLauncherAmbient.current
val posts = viewModel.posts.collectAsState() val posts = viewModel.posts.collectAsState()
val savedPosts = viewModel.savedPosts.collectAsState()
val lastIndex = posts.value.lastIndex val lastIndex = posts.value.lastIndex
val showSaved = remember { mutableStateOf(false) } val navController = rememberNavController()
val destinations = arrayOf(Destination.Hottest, Destination.Saved)
Scaffold( Scaffold(
topBar = { bottomBar = {
LobstersTopAppBar(showSaved.value) { BottomNavigation {
showSaved.value = !showSaved.value val navBackStackEntry by navController.currentBackStackEntryAsState()
} val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
}, destinations.forEach { screen ->
bodyContent = { BottomNavigationItem(
val saved = showSaved.value icon = {
if (saved && savedPosts.value.isEmpty()) { IconResource(
EmptyList(saved) resourceId = when (screen) {
} else if (!saved && posts.value.isEmpty()) { Destination.Hottest -> R.drawable.ic_whatshot_24px
EmptyList(saved) Destination.Saved -> R.drawable.ic_favorite_24px
} else { }
LobsterList( )
showSaved.value, },
savedPosts.value, label = { Text(screen.label) },
posts.value, selected = currentRoute == screen.route,
lastIndex, onClick = {
viewModel, // This is the equivalent to popUpTo the start destination
urlLauncher navController.popBackStack(navController.graph.startDestination, false)
)
}
},
floatingActionButton = { LobstersFAB(showSaved.value, viewModel) },
)
}
@Composable // This if check gives us a "singleTop" behavior where we do not create a
private fun LobstersFAB( // second instance of the composable if we are already on that destination
showSaved: Boolean, if (currentRoute != screen.route) {
viewModel: LobstersViewModel navController.navigate(screen.route)
) { }
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)
} }
}, }
) },
}
}
@Composable
private fun EmptyList(showSaved: Boolean) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
if (showSaved) { NavHost(navController, startDestination = Destination.Hottest.route) {
IconResource( composable(Destination.Hottest.route) {
R.drawable.ic_favorite_border_24px, HottestPosts(lastIndex = lastIndex, urlLauncher = urlLauncher , viewModel = viewModel)
tint = savedTitleColor, }
modifier = Modifier.padding(16.dp) composable(Destination.Saved.route) {
) SavedPosts(urlLauncher = urlLauncher, viewModel = viewModel)
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,
)
} }
} }
} }

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>