mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-17 23:47:02 +05:30
Merge #106
106: Decouple navigation from UI composables r=msfjarvis a=Skrilltrax Navigation library recommends to decouple the navigation from the composables. They specially points to pass lambdas instead of passing the NavController directly. https://developer.android.com/jetpack/compose/navigation#testing Co-authored-by: Aditya Wasan <adityawasan55@gmail.com> Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
commit
9f88bf15ab
10 changed files with 185 additions and 41 deletions
15
.github/workflows/pull_request.yml
vendored
15
.github/workflows/pull_request.yml
vendored
|
@ -18,13 +18,24 @@ jobs:
|
|||
with:
|
||||
java-version: '11'
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c952173edf28a2bd22e1a4926590c1ac39630461
|
||||
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
|
||||
|
||||
- uses: burrunan/gradle-cache-action@v1
|
||||
- uses: burrunan/gradle-cache-action@03c71a8ba93d670980695505f48f49daf43704a6
|
||||
name: Run unit tests
|
||||
with:
|
||||
arguments: testDebug --stacktrace
|
||||
|
||||
- name: Run instrumentation tests
|
||||
uses: reactivecircus/android-emulator-runner@08b092e904025fada32a01b711af1e7ff7b7a4a3
|
||||
with:
|
||||
api-level: 23
|
||||
target: default
|
||||
script: |
|
||||
adb shell settings put global animator_duration_scale 0
|
||||
adb shell settings put global transition_animation_scale 0
|
||||
adb shell settings put global window_animation_scale 0
|
||||
./gradlew :app:connectedDebugAndroidTest
|
||||
|
||||
- name: (Fail-only) upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@27bce4eee761b5bc643f46a8dfb41b430c8d05f6
|
||||
|
|
|
@ -48,6 +48,7 @@ dependencies {
|
|||
implementation(Dependencies.ThirdParty.accompanist)
|
||||
implementation(Dependencies.ThirdParty.Moshi.lib)
|
||||
testImplementation(Dependencies.Testing.junit)
|
||||
androidTestImplementation(Dependencies.AndroidX.Compose.activity)
|
||||
androidTestImplementation(Dependencies.Testing.daggerHilt)
|
||||
androidTestImplementation(Dependencies.Testing.uiTest)
|
||||
androidTestImplementation(Dependencies.Testing.AndroidX.Compose.uiTestJunit4)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
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")
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ 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.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.BottomNavigation
|
||||
import androidx.compose.material.BottomNavigationItem
|
||||
|
@ -16,9 +15,9 @@ 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.NavHostController
|
||||
import androidx.navigation.compose.KEY_ROUTE
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
|
@ -31,13 +30,12 @@ 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.UrlLauncher
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
@ -64,12 +62,28 @@ fun LobstersApp() {
|
|||
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(
|
||||
navController,
|
||||
hottestPostsListState,
|
||||
coroutineScope,
|
||||
currentDestination,
|
||||
navigateToDestination,
|
||||
jumpToIndex,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
|
@ -96,14 +110,11 @@ fun LobstersApp() {
|
|||
|
||||
@Composable
|
||||
fun LobstersBottomNav(
|
||||
navController: NavHostController,
|
||||
hottestPostsListState: LazyListState,
|
||||
coroutineScope: CoroutineScope,
|
||||
currentDestination: Destination,
|
||||
navigateToDestination: (destination: Destination) -> Unit,
|
||||
jumpToIndex: (index: Int) -> Unit,
|
||||
) {
|
||||
BottomNavigation {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute =
|
||||
navBackStackEntry?.arguments?.getString(KEY_ROUTE) ?: Destination.startDestination.route
|
||||
BottomNavigation(modifier = Modifier.testTag("LobstersBottomNav")) {
|
||||
Destination.values().forEach { screen ->
|
||||
BottomNavigationItem(
|
||||
icon = {
|
||||
|
@ -113,18 +124,13 @@ fun LobstersBottomNav(
|
|||
)
|
||||
},
|
||||
label = { Text(stringResource(id = screen.labelRes)) },
|
||||
selected = currentRoute == screen.route,
|
||||
selected = currentDestination == screen,
|
||||
alwaysShowLabels = false,
|
||||
onClick = {
|
||||
if (screen.route != currentRoute) {
|
||||
navController.navigate(screen.route) {
|
||||
launchSingleTop = true
|
||||
popUpTo(navController.graph.startDestination) { inclusive = false }
|
||||
}
|
||||
if (screen != currentDestination) {
|
||||
navigateToDestination(screen)
|
||||
} else if (screen.route == Destination.Hottest.route) {
|
||||
coroutineScope.launch {
|
||||
hottestPostsListState.snapToItemIndex(0)
|
||||
}
|
||||
jumpToIndex(0)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -18,5 +18,9 @@ enum class Destination(
|
|||
|
||||
companion object {
|
||||
val startDestination = Hottest
|
||||
|
||||
fun getDestinationFromRoute(route: String): Destination {
|
||||
return values().firstOrNull { it.route == route } ?: error("Incorrect route passed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.remote.LobstersPagingSource
|
||||
import dev.msfjarvis.lobsters.data.repo.LobstersRepository
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LobstersViewModel @Inject constructor(
|
||||
|
|
|
@ -39,12 +39,12 @@ internal fun Project.configureForAllProjects() {
|
|||
google()
|
||||
mavenCentral()
|
||||
jcenter() {
|
||||
content {
|
||||
// Indirect dependencies
|
||||
// https://youtrack.jetbrains.com/issue/IDEA-261387
|
||||
includeModule("org.jetbrains.trove4j", "trove4j")
|
||||
includeGroup("org.jetbrains.kotlinx")
|
||||
}
|
||||
content {
|
||||
// Indirect dependencies
|
||||
// https://youtrack.jetbrains.com/issue/IDEA-261387
|
||||
includeModule("org.jetbrains.trove4j", "trove4j")
|
||||
includeGroup("org.jetbrains.kotlinx")
|
||||
}
|
||||
}
|
||||
}
|
||||
tasks.withType<KotlinCompile> {
|
||||
|
@ -107,6 +107,8 @@ internal fun TestedExtension.configureCommonAndroidOptions() {
|
|||
exclude("**/*.txt")
|
||||
exclude("**/*.kotlin_module")
|
||||
exclude("**/plugin.properties")
|
||||
exclude("META-INF/AL2.0")
|
||||
exclude("META-INF/LGPL2.1")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
|
|
@ -8,7 +8,8 @@ private const val DAGGER_HILT_VERSION = "2.32-alpha"
|
|||
object Plugins {
|
||||
const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-alpha05"
|
||||
const val androidGradlePlugin_lintModel = "com.android.tools.lint:lint-model:30.0.0-alpha05"
|
||||
const val daggerGradlePlugin = "com.google.dagger:hilt-android-gradle-plugin:${DAGGER_HILT_VERSION}"
|
||||
const val daggerGradlePlugin =
|
||||
"com.google.dagger:hilt-android-gradle-plugin:${DAGGER_HILT_VERSION}"
|
||||
const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30"
|
||||
const val jsemver = "com.github.zafarkhaja:java-semver:0.9.0"
|
||||
const val sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:1.4.4"
|
||||
|
@ -37,7 +38,8 @@ object Dependencies {
|
|||
|
||||
const val activity = "androidx.activity:activity-compose:1.3.0-alpha02"
|
||||
const val compiler = "androidx.compose.compiler:compiler:$COMPOSE_VERSION"
|
||||
const val constraintLayout = "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha02"
|
||||
const val constraintLayout =
|
||||
"androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha02"
|
||||
const val foundation = "androidx.compose.foundation:foundation:$COMPOSE_VERSION"
|
||||
const val foundationLayout = "androidx.compose.foundation:foundation-layout:$COMPOSE_VERSION"
|
||||
const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha01"
|
||||
|
@ -94,13 +96,16 @@ object Dependencies {
|
|||
const val daggerHilt = "com.google.dagger:hilt-android-testing:$DAGGER_HILT_VERSION"
|
||||
const val junit = "junit:junit:4.13.1"
|
||||
const val mockWebServer = "com.squareup.okhttp3:mockwebserver3-junit4:5.0.0-alpha.2"
|
||||
const val uiTest = "androidx.compose.ui:ui-test:$COMPOSE_VERSION"
|
||||
|
||||
object AndroidX {
|
||||
|
||||
private const val version = "1.3.1-alpha02"
|
||||
const val runner = "androidx.test:runner:$version"
|
||||
const val rules = "androidx.test:rules:$version"
|
||||
|
||||
object Compose {
|
||||
|
||||
const val uiTestJunit4 = "androidx.compose.ui:ui-test-junit4:$COMPOSE_VERSION"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
*/
|
||||
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import org.gradle.api.Project
|
||||
import java.util.*
|
||||
import org.gradle.api.Project
|
||||
|
||||
private const val KEYSTORE_CONFIG_PATH = "keystore.properties"
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
import com.android.build.gradle.internal.plugins.AppPlugin
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
private const val VERSIONING_PROP_FILE = "version.properties"
|
||||
private const val VERSIONING_PROP_VERSION_NAME = "versioning-plugin.versionName"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue