52: Switch to BottomNav and use AndroidX navigation r=msfjarvis a=msfjarvis

This couldn't have been any easier to implement. Everything worked in the very first try, just amazing, amazing work from the Compose and Navigation teams.

bors r+


Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
bors[bot] 2020-10-29 09:33:17 +00:00 committed by GitHub
commit 20a38c662e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 189 additions and 136 deletions

View file

@ -81,6 +81,7 @@ dependencies {
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_androidx_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.navigation:navigation-compose:$nav_compose_version"
implementation "androidx.ui:ui-tooling:$compose_version"
implementation "com.google.android.material:material:$material_version"
implementation "com.google.dagger:hilt-android:$hilt_dagger_version"

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
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
}
},
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) },
)
}
@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()
label = { Text(screen.label) },
selected = currentRoute == screen.route,
onClick = {
// This is the equivalent to popUpTo the start destination
navController.popBackStack(navController.graph.startDestination, false)
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()
// 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)
}
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) {
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))
}
NavHost(navController, startDestination = Destination.Hottest.route) {
composable(Destination.Hottest.route) {
HottestPosts(lastIndex = lastIndex, urlLauncher = urlLauncher , viewModel = viewModel)
}
}
@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,
)
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>

View file

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
accompanist_version = "0.3.1"
accompanist_version = "0.3.2"
activity_version = "1.2.0-beta01"
agp_version = "4.2.0-alpha15"
appcompat_version = "1.3.0-alpha02"
@ -19,6 +19,7 @@ buildscript {
lifecycle_version = "2.3.0-beta01"
material_version = "1.3.0-alpha03"
moshi_version = "1.11.0"
nav_compose_version = "1.0.0-alpha01"
okhttp_version = "4.10.0-RC1"
retrofit_version = "2.9.0"
room_version = "2.3.0-alpha03"