mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-18 04:27:02 +05:30
Merge #111
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:
commit
a917917166
8 changed files with 78 additions and 256 deletions
|
@ -35,7 +35,6 @@ dependencies {
|
||||||
implementation(Dependencies.AndroidX.Compose.foundationLayout)
|
implementation(Dependencies.AndroidX.Compose.foundationLayout)
|
||||||
implementation(Dependencies.AndroidX.Compose.lifecycleViewModel)
|
implementation(Dependencies.AndroidX.Compose.lifecycleViewModel)
|
||||||
implementation(Dependencies.AndroidX.Compose.material)
|
implementation(Dependencies.AndroidX.Compose.material)
|
||||||
implementation(Dependencies.AndroidX.Compose.navigation)
|
|
||||||
implementation(Dependencies.AndroidX.Compose.paging)
|
implementation(Dependencies.AndroidX.Compose.paging)
|
||||||
implementation(Dependencies.AndroidX.Compose.runtime)
|
implementation(Dependencies.AndroidX.Compose.runtime)
|
||||||
implementation(Dependencies.AndroidX.Compose.ui)
|
implementation(Dependencies.AndroidX.Compose.ui)
|
||||||
|
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
|
@ -3,3 +3,4 @@
|
||||||
-dontwarn androidx.compose.animation.tooling.ComposeAnimation
|
-dontwarn androidx.compose.animation.tooling.ComposeAnimation
|
||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
-dontoptimize
|
-dontoptimize
|
||||||
|
-keep class dev.msfjarvis.lobsters.model.** { *; }
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,39 +3,12 @@ package dev.msfjarvis.lobsters.ui.main
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.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 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.theme.LobstersTheme
|
||||||
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
|
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
|
||||||
import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncher
|
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 javax.inject.Inject
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,6 +11,7 @@ import org.gradle.api.tasks.Delete
|
||||||
import org.gradle.api.tasks.testing.Test
|
import org.gradle.api.tasks.testing.Test
|
||||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||||
import org.gradle.api.tasks.wrapper.Wrapper
|
import org.gradle.api.tasks.wrapper.Wrapper
|
||||||
|
import org.gradle.kotlin.dsl.register
|
||||||
import org.gradle.kotlin.dsl.repositories
|
import org.gradle.kotlin.dsl.repositories
|
||||||
import org.gradle.kotlin.dsl.withType
|
import org.gradle.kotlin.dsl.withType
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
@ -21,7 +22,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
*/
|
*/
|
||||||
internal fun Project.configureForRootProject() {
|
internal fun Project.configureForRootProject() {
|
||||||
// register task for cleaning the build directory in the root project
|
// register task for cleaning the build directory in the root project
|
||||||
tasks.register("clean", Delete::class.java) {
|
tasks.register<Delete>("clean") {
|
||||||
delete(rootProject.buildDir)
|
delete(rootProject.buildDir)
|
||||||
}
|
}
|
||||||
tasks.withType<Wrapper> {
|
tasks.withType<Wrapper> {
|
||||||
|
|
|
@ -43,7 +43,6 @@ object Dependencies {
|
||||||
const val foundationLayout = "androidx.compose.foundation:foundation-layout:$COMPOSE_VERSION"
|
const val foundationLayout = "androidx.compose.foundation:foundation-layout:$COMPOSE_VERSION"
|
||||||
const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha01"
|
const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha01"
|
||||||
const val material = "androidx.compose.material:material:$COMPOSE_VERSION"
|
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 paging = "androidx.paging:paging-compose:1.0.0-alpha07"
|
||||||
const val runtime = "androidx.compose.runtime:runtime:$COMPOSE_VERSION"
|
const val runtime = "androidx.compose.runtime:runtime:$COMPOSE_VERSION"
|
||||||
const val ui = "androidx.compose.ui:ui:$COMPOSE_VERSION"
|
const val ui = "androidx.compose.ui:ui:$COMPOSE_VERSION"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue