Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
eba2836
feat: adopt the Compose BOM (2024.12.01) for unified version management
dipenpradhan Jun 7, 2026
272e4ed
feat: upgrade AndroidX Lifecycle to 2.8.7
dipenpradhan Jun 7, 2026
d8323ed
feat: upgrade Navigation Compose to 2.8.9 with type-safe routes
dipenpradhan Jun 7, 2026
fb1fdce
refactor: migrate from ExoPlayer 2.x to AndroidX Media3 1.5.1
dipenpradhan Jun 7, 2026
6656a2e
feat: upgrade Coil to 3.1.0
dipenpradhan Jun 7, 2026
3f44e12
feat: implement Pull-to-Refresh demo with Material3 PullToRefreshBox
dipenpradhan Jun 7, 2026
4b34619
test: add UI tests for the demo introduced in this change
dipenpradhan Sep 8, 2026
42b19a0
feat: complete Swipe-to-Dismiss demo with delete/archive and undo
dipenpradhan Jun 7, 2026
cf3125f
test: cover the swipe-to-dismiss demo and fix undo not restoring the row
dipenpradhan Sep 8, 2026
93fbda7
feat: add adaptive layout / large screen demo
dipenpradhan Jun 7, 2026
7619ace
test: add UI tests for the demo introduced in this change
dipenpradhan Sep 8, 2026
4c6058b
feat: add infinite looping carousel with auto-advance
dipenpradhan Jun 7, 2026
e20aa00
test: cover the infinite carousel introduced in this change
dipenpradhan Sep 8, 2026
ffda83a
test: add Compose UI tests to the tags and login modules
dipenpradhan Jun 7, 2026
3efe53a
refactor: migrate dependency management to a Gradle version catalog
dipenpradhan Jun 7, 2026
d8242df
chore: refresh foundational dependencies to SDK-35-compatible baselines
dipenpradhan Jun 14, 2026
64528c3
feat: add Predictive Back gesture demo for Android 15+
dipenpradhan Jun 14, 2026
1fe2232
test: add UI tests for the demo introduced in this change
dipenpradhan Sep 8, 2026
c4e366a
Merge branch 'master' into issue-200-predictive-back
dipenpradhan Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
<a href="https://github.com/Gurupreet/ComposeCookBook/releases/download/latest-master/app-debug.apk" >
<img src = "https://img.shields.io/badge/Master-Master?color=7885FF&label=Sample%20App&logo=android&style=for-the-badge" />
</a>
<a href = "https://developer.android.com/jetpack/androidx/versions/all-channel#december_16_2020">
<img src = "https://img.shields.io/badge/Jetpack%20Compose-1.0.1-brightgreen" />
<a href = "https://developer.android.com/develop/ui/compose/bom/bom-mapping">
<img src = "https://img.shields.io/badge/Jetpack%20Compose-BOM%202024.12.01-brightgreen" />
</a>
<a href = "https://github.com/Gurupreet/ComposeCookBook/actions/workflows/android.yml">
<img src = "https://github.com/Gurupreet/ComposeCookBook/actions/workflows/android.yml/badge.svg" />
Expand Down Expand Up @@ -100,9 +100,12 @@ Please get **Android Studio Bumblebee latest Canary** [from here](https://develo
- __Others:__ After the above steps feel free to deep dive into Tablayouts, carousel, Dialogs and BottomSheets


### Adaptive layouts (large screens)
- `NavigationSuiteScaffold` switches between bottom bar, rail and drawer based on window size
- `ListDetailPaneScaffold` two-pane layout on tablets/foldables — find it under "Adaptive UI" on the home screen

## Coming Soon
- Some of the features that will be available in coming weeks
- Advance lists: Pull Refresh, Swipe lists etc
- Clean Architecture Sample with coroutines.
- Advance canvas drawing.
Much more in pipeline stay tuned!!
Expand Down
5 changes: 2 additions & 3 deletions animations/canvas/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies

plugins {
/** See [common-compose-module-configs-script-plugin.gradle.kts] file */
id("common-compose-module-configs-script-plugin")
Expand All @@ -9,5 +7,6 @@ android { namespace = "com.guru.composecookbook.canvas" }

dependencies {
implementation(project(":theme"))
addComposeOfficialDependencies()
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose.official)
}
11 changes: 4 additions & 7 deletions animations/lottie/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies
import com.guru.composecookbook.build.dependencies.addCoreAndroidDependencies
import com.guru.composecookbook.build.dependencies.addThirdPartyUiDependencies

plugins {
/** See [common-compose-module-configs-script-plugin.gradle.kts] file */
id("common-compose-module-configs-script-plugin")
Expand All @@ -10,7 +6,8 @@ plugins {
android { namespace = "com.guru.composecookbook.lottie" }

dependencies {
addComposeOfficialDependencies()
addCoreAndroidDependencies()
addThirdPartyUiDependencies()
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose.official)
implementation(libs.bundles.core.android)
implementation(libs.bundles.thirdparty.ui)
}
50 changes: 20 additions & 30 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,19 +1,4 @@
import com.guru.composecookbook.build.configurations.ProjectConfigs
import com.guru.composecookbook.build.dependencies.addAndroidInstrumentationTestsDependencies
import com.guru.composecookbook.build.dependencies.addBiometricDependency
import com.guru.composecookbook.build.dependencies.addComposeDebugDependencies
import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies
import com.guru.composecookbook.build.dependencies.addComposeThirdPartyDependencies
import com.guru.composecookbook.build.dependencies.addCoreAndroidDependencies
import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies
import com.guru.composecookbook.build.dependencies.addDataDependencies
import com.guru.composecookbook.build.dependencies.addGoogleAndroidDependencies
import com.guru.composecookbook.build.dependencies.addJunit5TestDependencies
import com.guru.composecookbook.build.dependencies.addKotlinDependencies
import com.guru.composecookbook.build.dependencies.addKotlinTestDependencies
import com.guru.composecookbook.build.dependencies.addNetworkingDependencies
import com.guru.composecookbook.build.dependencies.addThirdPartyUiDependencies
import com.guru.composecookbook.build.dependencies.addThirdPartyUnitTestsDependencies

plugins {
id("com.android.application")
Expand Down Expand Up @@ -86,25 +71,30 @@ dependencies {
implementation(project(":animations:canvas"))
implementation(project(":animations:lottie"))

addKotlinDependencies()
implementation(libs.bundles.kotlin)

addDataDependencies()
implementation(platform(libs.androidx.compose.bom))
ksp(libs.androidx.room.compiler)
implementation(libs.bundles.data)

addComposeOfficialDependencies()
addComposeDebugDependencies()
addComposeThirdPartyDependencies()
implementation(platform(libs.androidx.compose.bom))
implementation(libs.bundles.compose.official)
debugImplementation(platform(libs.androidx.compose.bom))
debugImplementation(libs.bundles.compose.debug)
implementation(libs.bundles.compose.thirdparty)

addThirdPartyUiDependencies()
implementation(libs.bundles.thirdparty.ui)

addCoreAndroidDependencies()
addCoreAndroidUiDependencies()
addGoogleAndroidDependencies()
addNetworkingDependencies()
implementation(libs.bundles.core.android)
implementation(libs.bundles.core.android.ui)
implementation(libs.bundles.google.android)
implementation(libs.bundles.networking)

addKotlinTestDependencies()
addJunit5TestDependencies()
addThirdPartyUnitTestsDependencies()
testImplementation(libs.bundles.kotlin.test)
testImplementation(libs.bundles.junit5.test)
testImplementation(libs.truth)

addAndroidInstrumentationTestsDependencies()
addBiometricDependency()
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.bundles.instrumentation.test)
implementation(libs.androidx.biometric)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.guru.composecookbook.ui.home.adaptive

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test

class AdaptiveUIActivityTest {

@get:Rule val composeTestRule = createComposeRule()

@Test
fun everyNavigationDestinationIsLabelled() {
composeTestRule.setContent { AdaptiveDemoScreen() }

composeTestRule.onNodeWithText("Albums").assertIsDisplayed()
composeTestRule.onNodeWithText("Favorites").assertIsDisplayed()
composeTestRule.onNodeWithText("Profile").assertIsDisplayed()
}

@Test
fun albumsDestinationIsShownOnLaunch() {
composeTestRule.setContent { AdaptiveDemoScreen() }

composeTestRule.onNodeWithText("Perfect").assertIsDisplayed()
// Two albums in the sample data are by Ed Sheeran, so the artist alone is not unique.
composeTestRule.onAllNodesWithText("Ed Sheeran").onFirst().assertIsDisplayed()
}

@Test
fun selectingFavoritesReportsTheCurrentWindowSizeClasses() {
composeTestRule.setContent { AdaptiveDemoScreen() }

composeTestRule.onNodeWithText("Favorites").performClick()
composeTestRule.waitForIdle()

composeTestRule.onNodeWithText("Width size class:", substring = true).assertIsDisplayed()
composeTestRule.onNodeWithText("Height size class:", substring = true).assertIsDisplayed()
}

@Test
fun albumListShowsSongArtistAndGenre() {
composeTestRule.setContent { AlbumListDetail() }

composeTestRule.onNodeWithText("Perfect").assertIsDisplayed()
// Artist and genre both repeat across the sample data; the song title is the unique field.
composeTestRule.onAllNodesWithText("Ed Sheeran").onFirst().assertIsDisplayed()
composeTestRule.onAllNodesWithText("Pop").onFirst().assertIsDisplayed()
}

@Test
fun tappingAnAlbumOpensItsDetailPane() {
composeTestRule.setContent { AlbumListDetail() }

composeTestRule.onNodeWithText("Perfect").performClick()
composeTestRule.waitForIdle()

composeTestRule.onNodeWithText("Album by Ed Sheeran-2016").assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.guru.composecookbook.ui.home.advancelists

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onParent
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
import org.junit.Rule
import org.junit.Test

class SwipeableListsTest {

@get:Rule val composeTestRule = createComposeRule()

@Test
fun albumsAreListedOnFirstComposition() {
composeTestRule.setContent { SwipeableLists() }

composeTestRule.onNodeWithText("Perfect").assertIsDisplayed()
composeTestRule.onNodeWithText("Havana").assertIsDisplayed()
// Two albums in the sample data are by Ed Sheeran, so the artist alone is not unique.
composeTestRule.onAllNodesWithText("Ed Sheeran").onFirst().assertIsDisplayed()
}

@Test
fun everyRowExposesItsOverflowButtonToAccessibilityServices() {
composeTestRule.setContent { SwipeableLists() }

composeTestRule.onAllNodesWithContentDescription("More options").onFirst().assertIsDisplayed()
}

@Test
fun swipingEndToStartDeletesTheRowAndOffersUndo() {
composeTestRule.setContent { SwipeableLists() }

// The swipe must be dispatched to the row, not to the song title: the title node is only a
// few pixels wide, well short of the box's 50% positional threshold.
composeTestRule.onNodeWithText("Perfect").onParent().performTouchInput { swipeLeft() }
composeTestRule.waitForIdle()

composeTestRule.onNodeWithText("Perfect").assertDoesNotExist()
composeTestRule.onNodeWithText("Perfect deleted").assertIsDisplayed()
composeTestRule.onNodeWithText("Undo").assertIsDisplayed()
}

@Test
fun swipingStartToEndArchivesTheRowAndOffersUndo() {
composeTestRule.setContent { SwipeableLists() }

composeTestRule.onNodeWithText("Perfect").onParent().performTouchInput { swipeRight() }
composeTestRule.waitForIdle()

composeTestRule.onNodeWithText("Perfect").assertDoesNotExist()
composeTestRule.onNodeWithText("Perfect archived").assertIsDisplayed()
}

@Test
fun undoRestoresASwipedAwayRow() {
composeTestRule.setContent { SwipeableLists() }

composeTestRule.onNodeWithText("Perfect").onParent().performTouchInput { swipeLeft() }
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText("Undo").performClick()
composeTestRule.waitForIdle()

composeTestRule.onNodeWithText("Perfect").assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.guru.composecookbook.ui.home.predictiveback

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test

class PredictiveBackActivityTest {

@get:Rule val composeTestRule = createComposeRule()

@Test
fun screenIsTitledAndTheDrawerStartsOpen() {
composeTestRule.setContent { PredictiveBackDemoScreen() }

composeTestRule.onNodeWithText("Predictive Back").assertIsDisplayed()
composeTestRule.onNodeWithText("Mail").assertIsDisplayed()
composeTestRule.onNodeWithText("Inbox").assertIsDisplayed()
composeTestRule.onNodeWithText("Trash").assertIsDisplayed()
}

@Test
fun openDrawerExplainsTheBackGestureAffordance() {
composeTestRule.setContent { PredictiveBackDemoScreen() }

composeTestRule.onNodeWithText("Content area").assertIsDisplayed()
composeTestRule
.onNodeWithText("the drawer follows your gesture", substring = true)
.assertIsDisplayed()
}

@Test
fun togglingTheDrawerClosedHidesItAndOffersToReopen() {
composeTestRule.setContent { PredictiveBackDemoScreen() }

composeTestRule.onNodeWithContentDescription("Toggle drawer").performClick()
composeTestRule.waitForIdle()

composeTestRule.onNodeWithText("Mail").assertDoesNotExist()
composeTestRule.onNodeWithText("Inbox").assertDoesNotExist()
composeTestRule.onNodeWithText("Reopen drawer").assertIsDisplayed()
composeTestRule.onNodeWithText("Drawer closed.", substring = true).assertIsDisplayed()
}

@Test
fun reopenButtonBringsTheDrawerBack() {
composeTestRule.setContent { PredictiveBackDemoScreen() }

composeTestRule.onNodeWithContentDescription("Toggle drawer").performClick()
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText("Reopen drawer").performClick()
composeTestRule.waitForIdle()

composeTestRule.onNodeWithText("Mail").assertIsDisplayed()
composeTestRule.onNodeWithText("Reopen drawer").assertDoesNotExist()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.guru.composecookbook.ui.home.pullrefreshdemos

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

class PullRefreshListTest {

@get:Rule val composeTestRule = createComposeRule()

@Test
fun firstAlbumIsDisplayedInitially() {
composeTestRule.setContent { PullRefreshList(onPullRefresh = {}) }

composeTestRule.onNodeWithText("Perfect").assertIsDisplayed()
// Two albums in the sample data are by Ed Sheeran, so this row text is not unique.
composeTestRule
.onAllNodesWithText("Ed Sheeran, Album by Ed Sheeran-2016")
.onFirst()
.assertIsDisplayed()
}

@Test
fun refreshedAlbumIsAbsentBeforeAnyRefresh() {
composeTestRule.setContent { PullRefreshList(onPullRefresh = {}) }

// AlbumsDataProvider.album is only prepended by a refresh, so it must not be there yet.
// Match on the artist/description line: it is the one string unique to that album.
composeTestRule.onNodeWithText("Adele, Album by Adele-2016").assertDoesNotExist()
}

@Test
fun pullingDownInvokesTheRefreshCallback() {
var invocations = 0
composeTestRule.setContent { PullRefreshList(onPullRefresh = { invocations++ }) }

// The gesture has to travel far enough to cross PullToRefreshBox's threshold, so it is
// dispatched to the whole screen rather than to a single row.
composeTestRule.onRoot().performTouchInput { swipeDown() }
composeTestRule.waitForIdle()

assertEquals(1, invocations)
}
}
Loading
Loading