111: Switch to BackdropScaffold for the main screen r=msfjarvis a=msfjarvis

This is peak fanciness, we can't do any better

bors r+

Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
bors[bot] 2021-02-16 18:57:27 +00:00 committed by GitHub
commit a917917166
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 78 additions and 256 deletions

View file

@ -35,7 +35,6 @@ dependencies {
implementation(Dependencies.AndroidX.Compose.foundationLayout)
implementation(Dependencies.AndroidX.Compose.lifecycleViewModel)
implementation(Dependencies.AndroidX.Compose.material)
implementation(Dependencies.AndroidX.Compose.navigation)
implementation(Dependencies.AndroidX.Compose.paging)
implementation(Dependencies.AndroidX.Compose.runtime)
implementation(Dependencies.AndroidX.Compose.ui)

View file

@ -3,3 +3,4 @@
-dontwarn androidx.compose.animation.tooling.ComposeAnimation
-dontobfuscate
-dontoptimize
-keep class dev.msfjarvis.lobsters.model.** { *; }

View file

@ -1,115 +0,0 @@
package dev.msfjarvis.lobsters
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.assertContentDescriptionEquals
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onChildAt
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import dev.msfjarvis.lobsters.ui.main.LobstersBottomNav
import dev.msfjarvis.lobsters.ui.navigation.Destination
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class BottomNavigationLayoutTest {
@get:Rule
val composeTestRule = createComposeRule()
@Before
fun setUp() {
composeTestRule.setContent {
LobstersTheme {
var mutableDestination by remember { mutableStateOf(Destination.startDestination) }
LobstersBottomNav(
currentDestination = mutableDestination,
navigateToDestination = { mutableDestination = it },
jumpToIndex = {}
)
}
}
}
@Test
fun bottomNavItemCountTest() {
// Test to make sure total items are equal to enum objects present in Destination
composeTestRule.onNodeWithTag("LobstersBottomNav")
.assertExists()
.assertIsDisplayed()
.onChildren()
.assertCountEquals(Destination.values().size)
}
@Test
fun bottomNavItemTest() {
// Check hottest BottomNavItem is rendered correctly
composeTestRule.onNodeWithTag("LobstersBottomNav")
.assertExists()
.assertIsDisplayed()
.onChildAt(0)
.assertTextEquals("Hottest")
.assertContentDescriptionEquals("Hottest")
.assertHasClickAction()
// Check saved BottomNavItem is rendered correctly
composeTestRule.onNodeWithTag("LobstersBottomNav")
.assertExists()
.assertIsDisplayed()
.onChildAt(1)
.assertTextEquals("Saved")
.assertContentDescriptionEquals("Saved")
.assertHasClickAction()
}
@Test
fun bottomNavItemSelectedTest() {
// Check hottest BottomNav item is selected
composeTestRule.onNodeWithTag("LobstersBottomNav")
.assertExists()
.assertIsDisplayed()
.onChildAt(0)
.assertIsSelected()
.assertTextEquals("Hottest")
// Check saved BottomNav item is not selected
composeTestRule.onNodeWithTag("LobstersBottomNav")
.assertExists()
.assertIsDisplayed()
.onChildAt(1)
.assertIsNotSelected()
// Select the saved BottomNav item
composeTestRule.onNodeWithTag("LobstersBottomNav")
.onChildAt(1)
.performClick()
// Check hottest BottomNav item is not selected
composeTestRule.onNodeWithTag("LobstersBottomNav")
.assertExists()
.assertIsDisplayed()
.onChildAt(0)
.assertIsNotSelected()
// Check saved BottomNav item is selected
composeTestRule.onNodeWithTag("LobstersBottomNav")
.assertExists()
.assertIsDisplayed()
.onChildAt(1)
.assertIsSelected()
.assertTextEquals("Saved")
}
}

View file

@ -0,0 +1,75 @@
package dev.msfjarvis.lobsters.ui.main
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.BackdropScaffold
import androidx.compose.material.BackdropScaffoldDefaults
import androidx.compose.material.BackdropValue
import androidx.compose.material.Text
import androidx.compose.material.rememberBackdropScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.compose.collectAsLazyPagingItems
import dev.msfjarvis.lobsters.R
import dev.msfjarvis.lobsters.ui.posts.HottestPosts
import dev.msfjarvis.lobsters.ui.posts.SavedPosts
import dev.msfjarvis.lobsters.ui.viewmodel.LobstersViewModel
@Composable
fun LobstersApp() {
val viewModel: LobstersViewModel = viewModel()
val scaffoldState = rememberBackdropScaffoldState(initialValue = BackdropValue.Concealed)
BackdropScaffold(
scaffoldState = scaffoldState,
gesturesEnabled = false,
appBar = {
Spacer(modifier = Modifier.height(BackdropScaffoldDefaults.PeekHeight.times(0.3f)))
Text(
text = stringResource(id = R.string.app_name),
textAlign = TextAlign.Center,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(BackdropScaffoldDefaults.PeekHeight.times(0.3f)))
},
backLayerContent = { BackgroundLayerContent(viewModel) },
) {
ForegroundLayerContent(viewModel)
}
}
@Composable
fun ForegroundLayerContent(
viewModel: LobstersViewModel,
) {
val hottestPosts = viewModel.posts.collectAsLazyPagingItems()
val hottestPostsListState = rememberLazyListState()
HottestPosts(
posts = hottestPosts,
listState = hottestPostsListState,
isPostSaved = viewModel::isPostSaved,
saveAction = viewModel::toggleSave,
)
}
@Composable
fun BackgroundLayerContent(
viewModel: LobstersViewModel,
) {
val savedPosts by viewModel.savedPosts.collectAsState()
SavedPosts(
posts = savedPosts,
saveAction = viewModel::toggleSave,
)
}

View file

@ -3,39 +3,12 @@ package dev.msfjarvis.lobsters.ui.main
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
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 androidx.paging.compose.collectAsLazyPagingItems
import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.lobsters.ui.navigation.Destination
import dev.msfjarvis.lobsters.ui.posts.HottestPosts
import dev.msfjarvis.lobsters.ui.posts.SavedPosts
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncher
import dev.msfjarvis.lobsters.ui.viewmodel.LobstersViewModel
import dev.msfjarvis.lobsters.util.IconResource
import javax.inject.Inject
import kotlinx.coroutines.launch
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@ -52,88 +25,3 @@ class MainActivity : AppCompatActivity() {
}
}
}
@Composable
fun LobstersApp() {
val viewModel: LobstersViewModel = viewModel()
val coroutineScope = rememberCoroutineScope()
val navController = rememberNavController()
val hottestPosts = viewModel.posts.collectAsLazyPagingItems()
val savedPosts by viewModel.savedPosts.collectAsState()
val hottestPostsListState = rememberLazyListState()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute =
navBackStackEntry?.arguments?.getString(KEY_ROUTE) ?: Destination.startDestination.route
val currentDestination = Destination.getDestinationFromRoute(currentRoute)
val navigateToDestination: (destination: Destination) -> Unit = { destination ->
navController.navigate(destination.route) {
launchSingleTop = true
popUpTo(navController.graph.startDestination) { inclusive = false }
}
}
val jumpToIndex: (Int) -> Unit = {
coroutineScope.launch {
hottestPostsListState.snapToItemIndex(it)
}
}
Scaffold(
bottomBar = {
LobstersBottomNav(
currentDestination,
navigateToDestination,
jumpToIndex,
)
},
) { innerPadding ->
NavHost(navController, startDestination = Destination.startDestination.route) {
composable(Destination.Hottest.route) {
HottestPosts(
posts = hottestPosts,
listState = hottestPostsListState,
isPostSaved = viewModel::isPostSaved,
saveAction = viewModel::toggleSave,
modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding()),
)
}
composable(Destination.Saved.route) {
SavedPosts(
posts = savedPosts,
saveAction = viewModel::toggleSave,
modifier = Modifier.padding(bottom = innerPadding.calculateBottomPadding()),
)
}
}
}
}
@Composable
fun LobstersBottomNav(
currentDestination: Destination,
navigateToDestination: (destination: Destination) -> Unit,
jumpToIndex: (index: Int) -> Unit,
) {
BottomNavigation(modifier = Modifier.testTag("LobstersBottomNav")) {
Destination.values().forEach { screen ->
BottomNavigationItem(
icon = {
IconResource(
resourceId = screen.badgeRes,
contentDescription = stringResource(screen.labelRes),
)
},
label = { Text(stringResource(id = screen.labelRes)) },
selected = currentDestination == screen,
alwaysShowLabels = false,
onClick = {
if (screen != currentDestination) {
navigateToDestination(screen)
} else if (screen.route == Destination.Hottest.route) {
jumpToIndex(0)
}
}
)
}
}
}

View file

@ -1,26 +0,0 @@
package dev.msfjarvis.lobsters.ui.navigation
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import dev.msfjarvis.lobsters.R
/**
* Destinations for navigation within the app.
*/
enum class Destination(
val route: String,
@StringRes val labelRes: Int,
@DrawableRes val badgeRes: Int,
) {
Hottest("hottest", R.string.hottest_posts, R.drawable.ic_whatshot_24px),
Saved("saved", R.string.saved_posts, R.drawable.ic_favorite_24px),
;
companion object {
val startDestination = Hottest
fun getDestinationFromRoute(route: String): Destination {
return values().firstOrNull { it.route == route } ?: error("Incorrect route passed")
}
}
}

View file

@ -11,6 +11,7 @@ import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.gradle.api.tasks.wrapper.Wrapper
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.repositories
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@ -21,7 +22,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
*/
internal fun Project.configureForRootProject() {
// register task for cleaning the build directory in the root project
tasks.register("clean", Delete::class.java) {
tasks.register<Delete>("clean") {
delete(rootProject.buildDir)
}
tasks.withType<Wrapper> {

View file

@ -43,7 +43,6 @@ object Dependencies {
const val foundationLayout = "androidx.compose.foundation:foundation-layout:$COMPOSE_VERSION"
const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha01"
const val material = "androidx.compose.material:material:$COMPOSE_VERSION"
const val navigation = "androidx.navigation:navigation-compose:1.0.0-alpha07"
const val paging = "androidx.paging:paging-compose:1.0.0-alpha07"
const val runtime = "androidx.compose.runtime:runtime:$COMPOSE_VERSION"
const val ui = "androidx.compose.ui:ui:$COMPOSE_VERSION"