diff --git a/README.md b/README.md index 0a4c1de3..34dc1924 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ - - + + @@ -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!! diff --git a/animations/canvas/build.gradle.kts b/animations/canvas/build.gradle.kts index 808a2451..922e4121 100644 --- a/animations/canvas/build.gradle.kts +++ b/animations/canvas/build.gradle.kts @@ -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") @@ -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) } diff --git a/animations/lottie/build.gradle.kts b/animations/lottie/build.gradle.kts index 7c74eed7..7e81a0e1 100644 --- a/animations/lottie/build.gradle.kts +++ b/animations/lottie/build.gradle.kts @@ -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") @@ -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) } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 151116eb..aa2fb412 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") @@ -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) } diff --git a/app/src/androidTest/java/com/guru/composecookbook/ui/home/adaptive/AdaptiveUIActivityTest.kt b/app/src/androidTest/java/com/guru/composecookbook/ui/home/adaptive/AdaptiveUIActivityTest.kt new file mode 100644 index 00000000..8dc083ff --- /dev/null +++ b/app/src/androidTest/java/com/guru/composecookbook/ui/home/adaptive/AdaptiveUIActivityTest.kt @@ -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() + } +} diff --git a/app/src/androidTest/java/com/guru/composecookbook/ui/home/advancelists/SwipeableListsTest.kt b/app/src/androidTest/java/com/guru/composecookbook/ui/home/advancelists/SwipeableListsTest.kt new file mode 100644 index 00000000..273e70d5 --- /dev/null +++ b/app/src/androidTest/java/com/guru/composecookbook/ui/home/advancelists/SwipeableListsTest.kt @@ -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() + } +} diff --git a/app/src/androidTest/java/com/guru/composecookbook/ui/home/predictiveback/PredictiveBackActivityTest.kt b/app/src/androidTest/java/com/guru/composecookbook/ui/home/predictiveback/PredictiveBackActivityTest.kt new file mode 100644 index 00000000..795b7012 --- /dev/null +++ b/app/src/androidTest/java/com/guru/composecookbook/ui/home/predictiveback/PredictiveBackActivityTest.kt @@ -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() + } +} diff --git a/app/src/androidTest/java/com/guru/composecookbook/ui/home/pullrefreshdemos/PullRefreshListTest.kt b/app/src/androidTest/java/com/guru/composecookbook/ui/home/pullrefreshdemos/PullRefreshListTest.kt new file mode 100644 index 00000000..3a221bd8 --- /dev/null +++ b/app/src/androidTest/java/com/guru/composecookbook/ui/home/pullrefreshdemos/PullRefreshListTest.kt @@ -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) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1f7adee5..1855d2fe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + + { DynamicUIActivity.newIntent(context, DynamicUiType.MOTIONLAYOUT.name, isDarkTheme) } + HomeScreenItems.AdaptiveUI -> { + AdaptiveUIActivity.newIntent(context, isDarkTheme) + } + HomeScreenItems.PredictiveBack -> { + PredictiveBackActivity.newIntent(context, isDarkTheme) + } } context.startActivity(intent) } diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/adaptive/AdaptiveUIActivity.kt b/app/src/main/java/com/guru/composecookbook/ui/home/adaptive/AdaptiveUIActivity.kt new file mode 100644 index 00000000..e6aa443d --- /dev/null +++ b/app/src/main/java/com/guru/composecookbook/ui/home/adaptive/AdaptiveUIActivity.kt @@ -0,0 +1,184 @@ +package com.guru.composecookbook.ui.home.adaptive + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.compose.foundation.clickable +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.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Album +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.guru.composecookbook.data.AlbumsDataProvider +import com.guru.composecookbook.data.model.Album +import com.guru.composecookbook.theme.ComposeCookBookMaterial3Theme +import com.guru.composecookbook.ui.home.dynamic.DynamicUIActivity + +/** + * Demonstrates adaptive layouts for large screens: + * - [NavigationSuiteScaffold] automatically switches between a bottom bar (compact), navigation + * rail (medium) and drawer (expanded) based on the window size class. + * - [ListDetailPaneScaffold] shows a single pane on phones and a two-pane list/detail layout on + * tablets, foldables and desktops. + */ +class AdaptiveUIActivity : ComponentActivity() { + + private val isDarkTheme: Boolean by lazy { + intent?.getBooleanExtra(DynamicUIActivity.DARK_THEME, false) ?: false + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { ComposeCookBookMaterial3Theme(isDarkTheme) { AdaptiveDemoScreen() } } + } + + companion object { + fun newIntent(context: Context, isDarkTheme: Boolean) = + Intent(context, AdaptiveUIActivity::class.java).apply { + putExtra(DynamicUIActivity.DARK_THEME, isDarkTheme) + } + } +} + +enum class AdaptiveDemoDestination(val label: String, val icon: ImageVector) { + Albums("Albums", Icons.Default.Album), + Favorites("Favorites", Icons.Default.Favorite), + Profile("Profile", Icons.Default.Person), +} + +@Composable +fun AdaptiveDemoScreen() { + var currentDestination by remember { mutableStateOf(AdaptiveDemoDestination.Albums) } + + NavigationSuiteScaffold( + navigationSuiteItems = { + AdaptiveDemoDestination.entries.forEach { destination -> + item( + icon = { Icon(destination.icon, contentDescription = destination.label) }, + label = { Text(destination.label) }, + selected = destination == currentDestination, + onClick = { currentDestination = destination }, + ) + } + } + ) { + when (currentDestination) { + AdaptiveDemoDestination.Albums -> AlbumListDetail() + AdaptiveDemoDestination.Favorites -> WindowInfoPane("Favorites") + AdaptiveDemoDestination.Profile -> WindowInfoPane("Profile") + } + } +} + +@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@Composable +fun AlbumListDetail() { + val navigator = rememberListDetailPaneScaffoldNavigator() + + BackHandler(navigator.canNavigateBack()) { navigator.navigateBack() } + + ListDetailPaneScaffold( + directive = navigator.scaffoldDirective, + value = navigator.scaffoldValue, + listPane = { + AnimatedPane { + LazyColumn { + items(AlbumsDataProvider.albums) { album -> + ListItem( + modifier = + Modifier.fillMaxWidth().clickable { + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, album) + }, + headlineContent = { Text(album.song) }, + supportingContent = { Text(album.artist) }, + leadingContent = { Icon(Icons.Default.Album, contentDescription = null) }, + trailingContent = { Text(album.genre, style = MaterialTheme.typography.labelSmall) }, + tonalElevation = 2.dp, + ) + androidx.compose.material3.HorizontalDivider() + } + } + } + }, + detailPane = { + AnimatedPane { + val album = navigator.currentDestination?.content + AlbumDetailPane(album) + } + }, + ) +} + +@Composable +private fun AlbumDetailPane(album: Album?) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + if (album == null) { + Text( + text = "Select an album from the list", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(16.dp), + ) + } else { + Card(modifier = Modifier.padding(16.dp)) { + Column(modifier = Modifier.padding(24.dp)) { + Text(text = album.song, style = MaterialTheme.typography.headlineMedium) + Text(text = album.artist, style = MaterialTheme.typography.titleMedium) + Text( + text = album.descriptions, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 16.dp), + ) + } + } + } + } +} + +@Composable +private fun WindowInfoPane(title: String) { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = title, style = MaterialTheme.typography.headlineMedium) + Text( + text = "Width size class: ${windowSizeClass.windowWidthSizeClass}", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 8.dp), + ) + Text( + text = "Height size class: ${windowSizeClass.windowHeightSizeClass}", + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/AdvanceListsActivity.kt b/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/AdvanceListsActivity.kt index b89d5438..ffd0c8b7 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/AdvanceListsActivity.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/AdvanceListsActivity.kt @@ -16,8 +16,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -49,7 +49,7 @@ class AdvanceListsActivity : ComponentActivity() { // A surface container using the 'background' color from the theme Scaffold( topBar = { - SmallTopAppBar( + TopAppBar( title = { Text(text = "Advance Lists(In Progress)") }, navigationIcon = { IconButton(onClick = { onBackPressed() }) { diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/AnimatedLists.kt b/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/AnimatedLists.kt index 6db2af23..fb10a504 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/AnimatedLists.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/AnimatedLists.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.rememberImagePainter +import coil3.compose.rememberAsyncImagePainter import com.guru.composecookbook.data.DemoDataProvider import com.guru.composecookbook.data.model.Tweet import com.guru.composecookbook.theme.typography @@ -145,8 +145,8 @@ fun AnimatedListItem(tweet: Tweet, itemIndex: Int, animationIndex: Int) { Row(modifier = animatedModifier, verticalAlignment = Alignment.CenterVertically) { Image( painter = - rememberImagePainter( - data = + rememberAsyncImagePainter( + model = "https://picsum.photos/id/${ itemIndex + 1 diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/SwipeableLists.kt b/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/SwipeableLists.kt index 3cf707f2..0d4cbb66 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/SwipeableLists.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/home/advancelists/SwipeableLists.kt @@ -1,71 +1,129 @@ package com.guru.composecookbook.ui.home.advancelists -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Archive import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.guru.composecookbook.data.AlbumsDataProvider import com.guru.composecookbook.data.model.Album +import kotlinx.coroutines.launch +/** + * Demonstrates Material3 [SwipeToDismissBox]: + * - swipe left (end to start) to delete an item, + * - swipe right (start to end) to archive it, + * - either action removes the item and offers Undo via a [Snackbar]. + */ @Composable fun SwipeableLists() { - val albums by remember { mutableStateOf(AlbumsDataProvider.albums) } - LazyColumn { - itemsIndexed( - items = albums, - itemContent = { index, album -> SwipeableListItem(index, album) { index -> } }, - ) + val albums = remember { AlbumsDataProvider.albums.toMutableStateList() } + val snackbarHostState = remember { SnackbarHostState() } + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn(state = listState) { + items(items = albums, key = { it.id }) { album -> + SwipeableListItem( + album = album, + onItemSwiped = { dismissValue -> + val index = albums.indexOf(album) + if (index >= 0) { + albums.removeAt(index) + val action = + if (dismissValue == SwipeToDismissBoxValue.StartToEnd) "archived" else "deleted" + scope.launch { + val result = + snackbarHostState.showSnackbar( + message = "${album.song} $action", + actionLabel = "Undo", + duration = SnackbarDuration.Short, + ) + if (result == SnackbarResult.ActionPerformed) { + val restoreAt = index.coerceAtMost(albums.size) + albums.add(restoreAt, album) + // LazyColumn keeps the first visible item anchored, so a row re-inserted above + // it would silently land off-screen. Scroll it back into view. + listState.animateScrollToItem(restoreAt) + } + } + } + }, + ) + } + } + SnackbarHost(hostState = snackbarHostState, modifier = Modifier.align(Alignment.BottomCenter)) } } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @Composable -fun SwipeableListItem(index: Int, album: Album, onItemSwiped: (Int) -> Unit) { +fun SwipeableListItem(album: Album, onItemSwiped: (SwipeToDismissBoxValue) -> Unit) { val dismissState = rememberSwipeToDismissBoxState( confirmValueChange = { dismissValue -> - if (dismissValue == SwipeToDismissBoxValue.EndToStart) { - onItemSwiped(index) + if (dismissValue != SwipeToDismissBoxValue.Settled) { + onItemSwiped(dismissValue) true } else { false } - } + }, + // require half of the item width to be dragged before dismissing + positionalThreshold = { totalDistance -> totalDistance * 0.5f }, ) SwipeToDismissBox( state = dismissState, - backgroundContent = { - Box( - modifier = - Modifier.fillMaxSize() - .background(MaterialTheme.colorScheme.errorContainer) - .padding(horizontal = 16.dp), - contentAlignment = Alignment.CenterEnd, - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete", - tint = MaterialTheme.colorScheme.onErrorContainer, - ) - } - }, + backgroundContent = { SwipeActionBackground(dismissState.dismissDirection) }, content = { ListItemContent(album) }, - enableDismissFromStartToEnd = false, + enableDismissFromStartToEnd = true, enableDismissFromEndToStart = true, ) } +@Composable +private fun SwipeActionBackground(direction: SwipeToDismissBoxValue) { + val isArchive = direction == SwipeToDismissBoxValue.StartToEnd + val background = + if (isArchive) MaterialTheme.colorScheme.secondaryContainer + else MaterialTheme.colorScheme.errorContainer + Box( + modifier = + Modifier.fillMaxSize() + .padding(horizontal = 16.dp, vertical = 8.dp) + .clip(MaterialTheme.shapes.medium) + .background(background) + .padding(horizontal = 16.dp), + contentAlignment = if (isArchive) Alignment.CenterStart else Alignment.CenterEnd, + ) { + if (isArchive) { + Icon( + imageVector = Icons.Default.Archive, + contentDescription = "Archive", + tint = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } else { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete", + tint = MaterialTheme.colorScheme.onErrorContainer, + ) + } + } +} + @Composable private fun ListItemContent(album: Album) { Card(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/customfling/FlingListActivity.kt b/app/src/main/java/com/guru/composecookbook/ui/home/customfling/FlingListActivity.kt index 88adf82e..cb7e5f7f 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/home/customfling/FlingListActivity.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/home/customfling/FlingListActivity.kt @@ -18,8 +18,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.livedata.observeAsState @@ -139,7 +139,7 @@ fun ListViewContent( */ Scaffold( topBar = { - SmallTopAppBar( + TopAppBar( title = { Column(modifier = Modifier.padding(4.dp)) { /* diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/dialogs/DialogsActivity.kt b/app/src/main/java/com/guru/composecookbook/ui/home/dialogs/DialogsActivity.kt index e53afdc9..10eb1bd6 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/home/dialogs/DialogsActivity.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/home/dialogs/DialogsActivity.kt @@ -24,9 +24,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -77,7 +77,7 @@ class DialogsActivity : AppCompatActivity() { fun DialogScreen(onBack: () -> Unit) { Scaffold( topBar = { - SmallTopAppBar( + TopAppBar( title = { Text(text = "Dialogs") }, navigationIcon = { IconButton(onClick = onBack) { diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/dynamic/DynamicUIActivity.kt b/app/src/main/java/com/guru/composecookbook/ui/home/dynamic/DynamicUIActivity.kt index fcc3f87b..44ef743f 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/home/dynamic/DynamicUIActivity.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/home/dynamic/DynamicUIActivity.kt @@ -13,8 +13,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -64,7 +64,7 @@ class DynamicUIActivity : ComponentActivity() { fun DynamicUiWrapper(uiType: String, onback: () -> Unit) { Scaffold( topBar = { - SmallTopAppBar( + TopAppBar( title = { Text(text = uiType) }, navigationIcon = { IconButton(onClick = onback) { diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/lists/ListViewActivity.kt b/app/src/main/java/com/guru/composecookbook/ui/home/lists/ListViewActivity.kt index 45c1f9f2..59022f45 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/home/lists/ListViewActivity.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/home/lists/ListViewActivity.kt @@ -21,8 +21,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -73,7 +73,7 @@ fun BaseView(isDarkTheme: Boolean, content: @Composable () -> Unit) { fun ListViewContent(listType: String, onBack: () -> Unit) { Scaffold( topBar = { - SmallTopAppBar( + TopAppBar( title = { Column(modifier = Modifier.padding(4.dp)) { Text(text = "ListView") diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/predictiveback/PredictiveBackActivity.kt b/app/src/main/java/com/guru/composecookbook/ui/home/predictiveback/PredictiveBackActivity.kt new file mode 100644 index 00000000..79bdbf25 --- /dev/null +++ b/app/src/main/java/com/guru/composecookbook/ui/home/predictiveback/PredictiveBackActivity.kt @@ -0,0 +1,179 @@ +package com.guru.composecookbook.ui.home.predictiveback + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.PredictiveBackHandler +import androidx.activity.compose.setContent +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.MenuOpen +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import com.guru.composecookbook.theme.ComposeCookBookMaterial3Theme +import com.guru.composecookbook.ui.home.dynamic.DynamicUIActivity +import kotlin.coroutines.cancellation.CancellationException + +/** + * Demonstrates the Android 15+ predictive back gesture with Compose's [PredictiveBackHandler]. An + * in-app navigation drawer follows the user's back-swipe progress in real time — it slides out and + * scales as the gesture proceeds, commits closed when the gesture completes, and springs back if + * the gesture is cancelled. + */ +class PredictiveBackActivity : ComponentActivity() { + + private val isDarkTheme: Boolean by lazy { + intent?.getBooleanExtra(DynamicUIActivity.DARK_THEME, false) ?: false + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { ComposeCookBookMaterial3Theme(isDarkTheme) { PredictiveBackDemoScreen() } } + } + + companion object { + fun newIntent(context: Context, isDarkTheme: Boolean) = + Intent(context, PredictiveBackActivity::class.java).apply { + putExtra(DynamicUIActivity.DARK_THEME, isDarkTheme) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PredictiveBackDemoScreen() { + var drawerOpen by remember { mutableStateOf(true) } + // Live gesture progress (0f..1f) reported by the predictive back handler. + var backProgress by remember { mutableFloatStateOf(0f) } + + // Only intercept back while the drawer is open; when closed the system handles back normally. + PredictiveBackHandler(enabled = drawerOpen) { progress -> + try { + progress.collect { backEvent -> backProgress = backEvent.progress } + // Gesture committed -> close the drawer instead of finishing the activity. + drawerOpen = false + backProgress = 0f + } catch (e: CancellationException) { + // Gesture cancelled -> snap the drawer back open. + backProgress = 0f + throw e + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Predictive Back") }, + actions = { + IconButton(onClick = { drawerOpen = !drawerOpen }) { + Icon(Icons.AutoMirrored.Filled.MenuOpen, contentDescription = "Toggle drawer") + } + }, + ) + } + ) { padding -> + Box(modifier = Modifier.fillMaxSize().padding(padding)) { + DetailContent(drawerOpen = drawerOpen, onOpenDrawer = { drawerOpen = true }) + + if (drawerOpen) { + // Translate + scale + fade the drawer in lock-step with the gesture so the user sees + // exactly where releasing will take them (the predictive back affordance). + val animatedProgress by animateFloatAsState(backProgress, label = "backProgress") + DemoDrawer( + modifier = + Modifier.graphicsLayer { + translationX = -size.width * 0.25f * animatedProgress + scaleX = 1f - 0.1f * animatedProgress + scaleY = 1f - 0.1f * animatedProgress + alpha = 1f - 0.5f * animatedProgress + } + ) + } + } + } +} + +@Composable +private fun DetailContent(drawerOpen: Boolean, onOpenDrawer: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(12.dp), + ) { + Text("Content area", style = MaterialTheme.typography.headlineSmall) + Text( + if (drawerOpen) + "Swipe from the screen edge (or press back) — the drawer follows your gesture and " + + "closes when you commit, instead of leaving the screen." + else "Drawer closed. Press back again to leave, or tap below to reopen it.", + style = MaterialTheme.typography.bodyMedium, + ) + if (!drawerOpen) { + androidx.compose.material3.Button(onClick = onOpenDrawer) { Text("Reopen drawer") } + } + } +} + +@Composable +private fun DemoDrawer(modifier: Modifier = Modifier) { + val items = listOf("Inbox", "Starred", "Sent", "Drafts", "Spam", "Trash") + Box( + modifier = + modifier + .fillMaxHeight() + .width(260.dp) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + ) { + Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp)) { + Text( + "Mail", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp), + ) + LazyColumn { + items(items) { label -> + ListItem( + headlineContent = { Text(label) }, + colors = + androidx.compose.material3.ListItemDefaults.colors( + containerColor = androidx.compose.ui.graphics.Color.Transparent + ), + ) + } + } + } + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomEnd) { + Text( + "← swipe back to close", + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(16.dp), + ) + } + } +} diff --git a/app/src/main/java/com/guru/composecookbook/ui/home/pullrefreshdemos/PullRefreshList.kt b/app/src/main/java/com/guru/composecookbook/ui/home/pullrefreshdemos/PullRefreshList.kt index c68f1b01..1438fdc5 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/home/pullrefreshdemos/PullRefreshList.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/home/pullrefreshdemos/PullRefreshList.kt @@ -1,69 +1,53 @@ package com.guru.composecookbook.ui.home.pullrefreshdemos -import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier import com.guru.composecookbook.data.AlbumsDataProvider +import com.guru.composecookbook.spotify.ui.details.components.SpotifySongListItem +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +/** + * Demonstrates Material3 [PullToRefreshBox]: pull the list down to trigger a simulated network + * refresh (1.5s delay) that prepends a "new" album to the list. + */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PullRefreshList(onPullRefresh: () -> Unit) { - val albums = AlbumsDataProvider.albums - // TODO revisit pull refresh - val initialYTranslate = -100f - val maximumYTranslate = 200f - val animatedProgress = remember { Animatable(0f) } + val albums = remember { AlbumsDataProvider.albums.toMutableStateList() } + var isRefreshing by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() val lazyListState = rememberLazyListState() - // val draggableModifier = Modifier.draggable( - // orientation = Orientation.Vertical, - // reverseDirection = false, - // enabled = lazyListState.firstVisibleItemIndex < 2, - // onDragStarted = { - // onPullRefresh.invoke() - // if (animatedProgress.value < maximumYTranslate) { - // animatedProgress.animateTo( - // targetValue = maximumYTranslate, - // animationSpec = tween(durationMillis = 1200, easing = LinearEasing), - // ) - // } - // }, - // onDragStopped = { - // animatedProgress.animateTo( - // targetValue = 3000f, - // animationSpec = repeatable( - // iterations = 1, - // animation = tween(durationMillis = 3000, easing = LinearEasing), - // ), - // ) - // } - // ) - - // Box(modifier = draggableModifier) { - // LazyColumn(state = lazyListState) { - // items( - // count = albums.size, - // itemContent = { index -> - // SpotifySongListItem(album = albums[index]) - // }) - // } - // //Animated Icon - // Icon( - // imageVector = Icons.Default.RotateRight, - // tint = Color.Black, - // contentDescription = null, - // modifier = Modifier.align(Alignment.TopCenter) - // .graphicsLayer( - // translationY = animateFloatAsState( - // animatedProgress.value.coerceIn( - // initialYTranslate, - // maximumYTranslate - // ) - // ).value, - // rotationZ = animateFloatAsState( - // animatedProgress.value - // ).value - // ).background(Color.LightGray, shape = CircleShape).padding(2.dp) - // ) - // } + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + onPullRefresh.invoke() + scope.launch { + isRefreshing = true + // Simulate a network reload + delay(1500) + albums.add(0, AlbumsDataProvider.album.copy(id = albums.size + 1)) + isRefreshing = false + lazyListState.scrollToItem(0) + } + }, + modifier = Modifier.fillMaxSize(), + ) { + LazyColumn(state = lazyListState, modifier = Modifier.fillMaxSize()) { + items(albums) { album -> SpotifySongListItem(album = album) } + } + } } diff --git a/app/src/main/java/com/guru/composecookbook/ui/learnwidgets/AppBars.kt b/app/src/main/java/com/guru/composecookbook/ui/learnwidgets/AppBars.kt index ef1e7752..cb496194 100644 --- a/app/src/main/java/com/guru/composecookbook/ui/learnwidgets/AppBars.kt +++ b/app/src/main/java/com/guru/composecookbook/ui/learnwidgets/AppBars.kt @@ -25,8 +25,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar as Material3TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -57,7 +57,7 @@ fun AppBars() { fun TopAppBarsDemo() { SubtitleText(subtitle = "Top App bar") - SmallTopAppBar( + Material3TopAppBar( title = { Text(text = "Home") }, navigationIcon = { IconButton(onClick = {}) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 935e3c3a..44ca9b0e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,8 @@ ca-app-pub-3940256099942544/6300978111 ca-app-pub-5207778306358129~3940384518 My Compose App + Adaptive UI + Predictive Back Custom Fling Custom Fling Settings Templates diff --git a/build.gradle.kts b/build.gradle.kts index 0b97f8c3..84e272b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,9 @@ -buildscript { - repositories { - mavenCentral() - google() - } - dependencies { - classpath("com.android.tools.build:gradle:8.7.3") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") - classpath("org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.1.20") - } -} - +// Plugin versions come from gradle/libs.versions.toml (the version catalog). +// AGP, Kotlin, KSP, serialization and the Compose compiler plugin reach the +// build classpath through buildSrc's dependencies (also catalog-driven), so +// only ktfmt needs to be declared here. plugins { - id("com.ncorti.ktfmt.gradle") version "0.22.0" apply false + alias(libs.plugins.ktfmt) apply false } allprojects { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 1f16e886..994df797 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -11,41 +11,28 @@ sourceSets { } } -buildscript { - - repositories { - mavenLocal() - mavenCentral() - google() - } - - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") - } -} - - repositories { - mavenLocal() mavenCentral() google() - google() } dependencies { // in order to be able to use "kotlin-android" in the common script - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") + implementation(libs.kotlin.gradle.plugin) // in order to be able to apply "org.jetbrains.kotlin.plugin.compose" in the common scripts - implementation("org.jetbrains.kotlin:compose-compiler-gradle-plugin:2.1.20") + implementation(libs.compose.compiler.gradle.plugin) + + // in order to be able to apply "org.jetbrains.kotlin.plugin.serialization" in module scripts + implementation(libs.kotlin.serialization.plugin) // in order to be able to apply "com.google.devtools.ksp" in the common scripts - // (KSP version must match the Kotlin version above) - implementation("com.google.devtools.ksp:symbol-processing-gradle-plugin:2.1.20-1.0.31") + // (the KSP version in the catalog must match the Kotlin version) + implementation(libs.ksp.gradle.plugin) // in order to recognize the "plugins" block in the common script - implementation("com.android.tools.build:gradle:8.7.3") + implementation(libs.android.gradle.plugin) // in order to recognize the "android" block in the common script - implementation("com.android.tools.build:gradle-api:8.7.3") + implementation(libs.android.gradle.api) } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..ee11cc12 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,5 @@ +// Expose the root version catalog to buildSrc so plugin/dependency +// versions live in a single place: gradle/libs.versions.toml +dependencyResolutionManagement { + versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } +} diff --git a/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/Dependencies.kt deleted file mode 100644 index c2c63dd6..00000000 --- a/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/Dependencies.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.guru.composecookbook.build.dependencies - -object Dependencies { - - const val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" - const val fontAwesomeCompose = - "com.github.Gurupreet:FontAwesomeCompose:${Versions.fontAwesomeCompose}" - const val kotlinStandardLibraryJdk8 = - "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}" - const val androidCoreKtx = "androidx.core:core-ktx:${Versions.coreKtx}" - const val androidAppCompat = "androidx.appcompat:appcompat:${Versions.appcompat}" - const val androidPaletteKtx = "androidx.palette:palette-ktx:${Versions.paletteKtx}" - const val googleMaterial = "com.google.android.material:material:${Versions.material}" - const val androidMultiDex = "androidx.multidex:multidex:${Versions.multidex}" - const val playServicesAds = - "com.google.android.gms:play-services-ads:${Versions.playServicesAds}" - // Removed googleMaps = "com.google.android.libraries.maps:maps" as it's not publicly available - const val playServicesMaps = - "com.google.android.gms:play-services-maps:${Versions.playServicesMaps}" - const val composeUi = "androidx.compose.ui:ui:${Versions.compose}" - const val composeUiTooling = "androidx.compose.ui:ui-tooling:${Versions.compose}" - // Material Design 1 and 3 dependencies - const val composeMaterial = "androidx.compose.material:material:${Versions.compose}" - const val composeMaterial3 = "androidx.compose.material3:material3:${Versions.material3}" - const val composeMaterial3WindowSizeClass = "androidx.compose.material3:material3-window-size-class:${Versions.material3}" - const val composeMaterialIconsExtended = - "androidx.compose.material:material-icons-extended:${Versions.compose}" - - // Runtime and State Management - const val composeRuntime = "androidx.compose.runtime:runtime:${Versions.compose}" - const val composeRuntimeLivedata = - "androidx.compose.runtime:runtime-livedata:${Versions.compose}" - - // Layout and Navigation - const val composeConstraintLayout = - "androidx.constraintlayout:constraintlayout-compose:${Versions.constraintLayoutCompose}" - const val composePaging = "androidx.paging:paging-compose:${Versions.pagingCompose}" - const val composeViewModel = - "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.androidLifecycleGrouped}" - const val composeActivity = "androidx.activity:activity-compose:${Versions.activityCompose}" - const val composeLottie = "com.airbnb.android:lottie-compose:${Versions.lottie}" - const val composeNavigation = "androidx.navigation:navigation-compose:${Versions.navCompose}" - - const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:${Versions.compose}" - const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:${Versions.compose}" - - /** - * Custom fling behaviour - * refer to https://github.com/iamjosephmj/flinger for more insights on the library - */ - const val flinger = "com.github.iamjosephmj:flinger:${Versions.flinger}" - - const val androidPagingRuntime = "androidx.paging:paging-runtime-ktx:${Versions.paging}" - const val lottie = "com.airbnb.android:lottie:${Versions.lottie}" - const val androidExoPlayer = "com.google.android.exoplayer:exoplayer:${Versions.exoplayer}" - const val roomRuntime = "androidx.room:room-runtime:${Versions.room}" - const val roomKtx = "androidx.room:room-ktx:${Versions.room}" - const val roomCompiler = "androidx.room:room-compiler:${Versions.room}" - const val coilCompose = "io.coil-kt:coil-compose:${Versions.coilCompose}" - const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}" - const val okHttpLoggingInterceptor = - "com.squareup.okhttp3:logging-interceptor:${Versions.loggingInterceptor}" - const val gson = "com.google.code.gson:gson:${Versions.gson}" - const val okHttpGsonConverter = - "com.squareup.retrofit2:converter-gson:${Versions.retrofitConverterGson}" - const val coroutinesCore = - "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" - const val coroutinesAndroid = - "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}" - const val viewModelKtx = - "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidLifecycleGrouped}" - const val liveDataKtx = - "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.androidLifecycleGrouped}" - const val lifecycleRuntimeKtx = - "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidLifecycleGrouped}" - const val lifecycleSavedState = - "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Versions.androidLifecycleGrouped}" - const val junitJupiterApi = "org.junit.jupiter:junit-jupiter-api:${Versions.junitJupiterApi}" - const val junitJupiterEngine = - "org.junit.jupiter:junit-jupiter-engine:${Versions.junitJupiterEngine}" - const val truth = "com.google.truth:truth:${Versions.truth}" - const val kotlinJunit5 = "org.jetbrains.kotlin:kotlin-test-junit5:${Versions.kotlin}" - const val kotlinTest = "org.jetbrains.kotlin:kotlin-test:${Versions.kotlin}" - const val androidXJunit = "androidx.test.ext:junit:${Versions.androidXJunit}" - - const val biometric = "androidx.biometric:biometric:${Versions.biometric}" - -} diff --git a/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/DependencyHandlerExtensions.kt b/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/DependencyHandlerExtensions.kt deleted file mode 100644 index 54a15de4..00000000 --- a/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/DependencyHandlerExtensions.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.guru.composecookbook.build.dependencies - -import org.gradle.api.artifacts.dsl.DependencyHandler - -fun DependencyHandler.addComposeOfficialDependencies() { - composeOfficialDependencies.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addComposeThirdPartyDependencies() { - composeThirdPartyDependencies.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addComposeDebugDependencies() { - composeDebugDependencies.forEach { - add("debugImplementation", it) - } -} - -fun DependencyHandler.addKotlinDependencies() { - kotlinDependencies.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addKotlinTestDependencies() { - kotlinTestDependencies.forEach { - add("testImplementation", it) - } -} - -fun DependencyHandler.addDataDependencies() { - add("ksp", Dependencies.roomCompiler) - dataDependencies.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addCoreAndroidDependencies() { - coreAndroidDependencies.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addCoreAndroidUiDependencies() { - coreAndroidUiDependencies.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addGoogleAndroidDependencies() { - googleAndroidLibraries.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addNetworkingDependencies() { - networkingDependencies.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addJunit5TestDependencies() { - junit5TestDependencies.forEach { - add("testImplementation", it) - } -} - -fun DependencyHandler.addThirdPartyUnitTestsDependencies() { - thirdPartyUnitTestsDependencies.forEach { - add("testImplementation", it) - } -} - -fun DependencyHandler.addAndroidInstrumentationTestsDependencies() { - androidInstrumentationTestsDependencies.forEach { - add("androidTestImplementation", it) - } -} - -fun DependencyHandler.addThirdPartyUiDependencies() { - thirdPartyUiDependencies.forEach { - add("implementation", it) - } -} - -fun DependencyHandler.addBiometricDependency() { - add("implementation", Dependencies.biometric) -} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/GroupedDependencies.kt b/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/GroupedDependencies.kt deleted file mode 100644 index 8cd6ff23..00000000 --- a/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/GroupedDependencies.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.guru.composecookbook.build.dependencies - -internal val composeOfficialDependencies = listOf( - Dependencies.composeUi, - Dependencies.composeUiTooling, - Dependencies.composeMaterial, - Dependencies.composeMaterial3, - Dependencies.composeMaterialIconsExtended, - Dependencies.composeRuntimeLivedata, - Dependencies.composeConstraintLayout, - Dependencies.composePaging, - Dependencies.composeViewModel, - Dependencies.composeActivity, - Dependencies.composeNavigation, - Dependencies.composeRuntime, -) - -internal val composeThirdPartyDependencies = listOf( - Dependencies.fontAwesomeCompose, - Dependencies.composeLottie, - Dependencies.flinger -) - -internal val composeDebugDependencies = listOf( - Dependencies.kotlinReflect, - Dependencies.composeUiTooling, - Dependencies.composeUiTestManifest -) - -internal val kotlinDependencies = listOf( - Dependencies.kotlinStandardLibraryJdk8, - Dependencies.coroutinesCore, - Dependencies.coroutinesAndroid -) - -internal val kotlinTestDependencies = listOf( - Dependencies.kotlinTest, - Dependencies.kotlinJunit5 -) - -internal val dataDependencies = listOf( - Dependencies.roomRuntime, - Dependencies.roomKtx, - Dependencies.androidPagingRuntime, - Dependencies.composeRuntime -) - -internal val coreAndroidDependencies = listOf( - Dependencies.androidMultiDex, - Dependencies.androidCoreKtx, - Dependencies.androidAppCompat, -) - -internal val coreAndroidUiDependencies = listOf( - Dependencies.googleMaterial, - Dependencies.androidPaletteKtx, - Dependencies.androidPagingRuntime, - Dependencies.lifecycleRuntimeKtx, - Dependencies.lifecycleSavedState, - Dependencies.liveDataKtx, - Dependencies.viewModelKtx -) - -internal val googleAndroidLibraries = listOf( - Dependencies.androidExoPlayer, - Dependencies.playServicesAds, - Dependencies.playServicesMaps - // Removed Dependencies.googleMaps as it's not available -) - -internal val networkingDependencies = listOf( - Dependencies.retrofit, - Dependencies.okHttpLoggingInterceptor, - Dependencies.gson, - Dependencies.okHttpGsonConverter, -) - -internal val junit5TestDependencies = listOf( - Dependencies.junitJupiterApi, - Dependencies.junitJupiterEngine -) - -internal val thirdPartyUnitTestsDependencies = listOf( - Dependencies.truth, -) - -internal val androidInstrumentationTestsDependencies = listOf( - Dependencies.composeUiTestJunit4, - Dependencies.composeActivity, - Dependencies.androidXJunit, - Dependencies.composeUiTestManifest -) - -internal val thirdPartyUiDependencies = listOf( - Dependencies.coilCompose, - Dependencies.lottie, - Dependencies.composeLottie -) - diff --git a/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/Versions.kt b/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/Versions.kt deleted file mode 100644 index 270f8411..00000000 --- a/buildSrc/src/main/kotlin/com/guru/composecookbook/build/dependencies/Versions.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.guru.composecookbook.build.dependencies - -internal object Versions { - const val compose = "1.6.1" // Latest stable Compose UI version - const val activityCompose = "1.8.2" // Latest stable - const val pagingCompose = "3.2.1" // Latest stable - const val navCompose = "2.7.7" // Latest stable navigation - const val constraintLayoutCompose = "1.1.0" // Latest stable - const val fontAwesomeCompose = "1.1.0" // Keeping as is since it's a third-party lib - const val coilCompose = "2.5.0" // Latest stable - const val kotlin = "2.1.20" // Kotlin 2.x with the K2 compiler - const val coroutines = "1.7.3" // Latest stable - const val androidLifecycleGrouped = "2.7.0" // Latest stable - const val flinger = "1.0.5" // Keeping as is since it's a third-party lib - const val paging = "3.2.1" // Latest stable - const val lottie = "6.3.0" // Latest stable - const val room = "2.6.1" // Latest stable - const val coreKtx = "1.12.0" // Latest stable - const val appcompat = "1.6.1" // Latest stable - const val paletteKtx = "1.0.0" // Latest stable - const val material = "1.11.0" // Latest stable Material Design 1 - const val material3 = "1.2.1" // Latest stable Material 3 - const val playServicesAds = "22.5.0" // Latest stable - const val googleMaps = "2.0.1" // Latest Google Maps version - const val playServicesMaps = "18.2.0" // Latest stable - const val exoplayer = "2.19.1" // Latest stable - const val loggingInterceptor = "4.12.0" // Latest stable - const val retrofit = "2.9.0" // Latest stable - const val gson = "2.10.1" // Latest stable - const val retrofitConverterGson = "2.9.0" // Latest stable - const val multidex = "2.0.1" // No new versions expected - const val junitJupiterApi = "5.10.1" // Latest stable - const val junitJupiterEngine = "5.10.1" // Latest stable - const val truth = "1.1.3" // Latest stable - const val androidXJunit = "1.1.5" // Latest stable - const val biometric = "1.2.0-alpha05" // Latest stable -} diff --git a/buildSrc/src/main/kotlin/common-compose-module-configs-script-plugin.gradle.kts b/buildSrc/src/main/kotlin/common-compose-module-configs-script-plugin.gradle.kts index e537fbae..f2389f7c 100644 --- a/buildSrc/src/main/kotlin/common-compose-module-configs-script-plugin.gradle.kts +++ b/buildSrc/src/main/kotlin/common-compose-module-configs-script-plugin.gradle.kts @@ -12,6 +12,7 @@ android { defaultConfig { minSdk = ProjectConfigs.minSdkVersion targetSdk = ProjectConfigs.targetSdkVersion + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { diff --git a/components/carousel/build.gradle.kts b/components/carousel/build.gradle.kts index e2bc39ef..2810b6d4 100644 --- a/components/carousel/build.gradle.kts +++ b/components/carousel/build.gradle.kts @@ -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") @@ -11,5 +9,9 @@ dependencies { implementation(project(":theme")) implementation(project(":data")) - addComposeOfficialDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.bundles.instrumentation.test) } diff --git a/components/carousel/src/androidTest/java/com/guru/composecookbook/carousel/InfiniteCarouselTest.kt b/components/carousel/src/androidTest/java/com/guru/composecookbook/carousel/InfiniteCarouselTest.kt new file mode 100644 index 00000000..f5124fdf --- /dev/null +++ b/components/carousel/src/androidTest/java/com/guru/composecookbook/carousel/InfiniteCarouselTest.kt @@ -0,0 +1,75 @@ +package com.guru.composecookbook.carousel + +import androidx.compose.material3.Text +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeRight +import com.guru.composecookbook.data.DemoDataProvider +import org.junit.Rule +import org.junit.Test + +class InfiniteCarouselTest { + + @get:Rule val composeTestRule = createComposeRule() + + private val items = DemoDataProvider.itemList.take(3) + + // Auto advance is effectively disabled so the test controls paging deterministically. + private fun setCarouselContent() { + composeTestRule.setContent { + InfiniteCarousel(items = items, autoAdvanceMillis = Long.MAX_VALUE) { item -> + Text(text = item.title) + } + } + } + + @Test + fun firstItemIsDisplayedInitially() { + setCarouselContent() + + composeTestRule.onNodeWithText(items[0].title).assertIsDisplayed() + } + + @Test + fun swipingForwardLoopsThroughItems() { + setCarouselContent() + + // The gesture must span the pager, not the narrow Text node inside the page. + composeTestRule.onRoot().performTouchInput { swipeLeft() } + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText(items[1].title).assertIsDisplayed() + } + + @Test + fun swipingBackwardFromFirstItemShowsLastItem() { + setCarouselContent() + + // Looping backwards past the first item must show the last item. + composeTestRule.onRoot().performTouchInput { swipeRight() } + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText(items[2].title).assertIsDisplayed() + } + + @Test + fun emptyListRendersNothingInsteadOfCrashing() { + // Composing at all is the real assertion: without the guard the page-index modulo + // divides by zero and this call throws before any assertion is reached. + composeTestRule.setContent { + InfiniteCarousel(items = emptyList(), autoAdvanceMillis = Long.MAX_VALUE) { + Text(text = PAGE_CONTENT) + } + } + + composeTestRule.onNodeWithText(PAGE_CONTENT).assertDoesNotExist() + } + + private companion object { + const val PAGE_CONTENT = "page content" + } +} diff --git a/components/carousel/src/main/java/com/guru/composecookbook/carousel/CarouselLayout.kt b/components/carousel/src/main/java/com/guru/composecookbook/carousel/CarouselLayout.kt index 807ab9ae..cbb25524 100644 --- a/components/carousel/src/main/java/com/guru/composecookbook/carousel/CarouselLayout.kt +++ b/components/carousel/src/main/java/com/guru/composecookbook/carousel/CarouselLayout.kt @@ -38,6 +38,15 @@ fun CarouselLayout() { val pagerState: PagerState = run { remember { PagerState(1, 0, items.size - 1) } } val selectedPage = remember { mutableStateOf(2) } + Text( + text = "Infinite looping carousel (auto advance)", + style = typography.body2, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + ) + InfiniteCarousel(items = items, modifier = Modifier.height(230.dp)) { item -> + CarouselItem(item) + } + PrepareFirstPager(pagerState, items, selectedPage) PrepareSecondPager(pagerState, items, selectedPage) PrepareThirdPager(pagerState, items, selectedPage) diff --git a/components/carousel/src/main/java/com/guru/composecookbook/carousel/InfiniteCarousel.kt b/components/carousel/src/main/java/com/guru/composecookbook/carousel/InfiniteCarousel.kt new file mode 100644 index 00000000..db025eeb --- /dev/null +++ b/components/carousel/src/main/java/com/guru/composecookbook/carousel/InfiniteCarousel.kt @@ -0,0 +1,67 @@ +package com.guru.composecookbook.carousel + +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lens +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.guru.composecookbook.data.model.Item +import kotlinx.coroutines.delay + +/** + * An infinitely looping carousel built on top of foundation's [HorizontalPager]. + * + * The pager is given a very large virtual page count and starts in the middle, so the user can + * swipe endlessly in both directions; the virtual page index is mapped back onto the real items + * with a modulo. While idle it auto-advances every [autoAdvanceMillis]; auto-advance pauses while + * the user is dragging. + */ +@Composable +fun InfiniteCarousel( + items: List, + modifier: Modifier = Modifier, + autoAdvanceMillis: Long = 3000L, + content: @Composable (Item) -> Unit, +) { + // Nothing to page through: render nothing rather than dividing by zero below. + if (items.isEmpty()) return + + // Start in the middle of the virtual range, aligned to the first real item. + val virtualCount = Int.MAX_VALUE + val startPage = remember(items) { virtualCount / 2 - ((virtualCount / 2) % items.size) } + val pagerState = rememberPagerState(initialPage = startPage) { virtualCount } + val isDragged by pagerState.interactionSource.collectIsDraggedAsState() + + // Auto advance while the user is not interacting with the pager. + LaunchedEffect(isDragged, items) { + if (!isDragged) { + while (true) { + delay(autoAdvanceMillis) + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + } + + Column(modifier = modifier) { + HorizontalPager(state = pagerState) { page -> content(items[page % items.size]) } + Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { + val selectedIndex = pagerState.currentPage % items.size + items.forEachIndexed { index, _ -> + CarouselDot( + selected = index == selectedIndex, + color = MaterialTheme.colorScheme.primary, + icon = Icons.Filled.Lens, + ) + } + } + } +} diff --git a/components/charts/build.gradle.kts b/components/charts/build.gradle.kts index 9f39a0ff..e00b187e 100644 --- a/components/charts/build.gradle.kts +++ b/components/charts/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -11,6 +8,7 @@ android { namespace = "com.guru.composecookbook.charts" } dependencies { implementation(project(":theme")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) } diff --git a/components/colorpicker/build.gradle.kts b/components/colorpicker/build.gradle.kts index 7ad277f2..69a99802 100644 --- a/components/colorpicker/build.gradle.kts +++ b/components/colorpicker/build.gradle.kts @@ -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") @@ -10,5 +8,6 @@ android { namespace = "com.guru.composecookbook.colorpicker" } dependencies { implementation(project(":theme")) - addComposeOfficialDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) } diff --git a/components/comingsoon/build.gradle.kts b/components/comingsoon/build.gradle.kts index 7da56f3c..d73ead82 100644 --- a/components/comingsoon/build.gradle.kts +++ b/components/comingsoon/build.gradle.kts @@ -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") @@ -11,5 +9,6 @@ dependencies { implementation(project(":animations:lottie")) implementation(project(":theme")) - addComposeOfficialDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) } diff --git a/components/fab/build.gradle.kts b/components/fab/build.gradle.kts index 66b4bb01..611b8635 100644 --- a/components/fab/build.gradle.kts +++ b/components/fab/build.gradle.kts @@ -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") @@ -7,4 +5,7 @@ plugins { android { namespace = "com.guru.composecookbook.fab" } -dependencies { addComposeOfficialDependencies() } +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) +} diff --git a/components/tags/build.gradle.kts b/components/tags/build.gradle.kts index e5ba1509..a7a95d54 100644 --- a/components/tags/build.gradle.kts +++ b/components/tags/build.gradle.kts @@ -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") @@ -10,5 +8,9 @@ android { namespace = "com.guru.composecookbook.tags" } dependencies { implementation(project(":theme")) - addComposeOfficialDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.bundles.instrumentation.test) } diff --git a/components/tags/src/androidTest/java/com/guru/composecookbook/tags/InterestTagTest.kt b/components/tags/src/androidTest/java/com/guru/composecookbook/tags/InterestTagTest.kt new file mode 100644 index 00000000..3d315b55 --- /dev/null +++ b/components/tags/src/androidTest/java/com/guru/composecookbook/tags/InterestTagTest.kt @@ -0,0 +1,39 @@ +package com.guru.composecookbook.tags + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class InterestTagTest { + + @get:Rule val composeTestRule = createComposeRule() + + @Test + fun interestTagDisplaysItsText() { + composeTestRule.setContent { InterestTag(text = "Jetpack Compose") } + + composeTestRule.onNodeWithText("Jetpack Compose").assertIsDisplayed() + } + + @Test + fun interestTagIsClickable() { + composeTestRule.setContent { InterestTag(text = "Clickable tag") } + + composeTestRule.onNodeWithText("Clickable tag").assertHasClickAction() + } + + @Test + fun interestTagInvokesOnClick() { + var clicked = false + composeTestRule.setContent { InterestTag(text = "Tap me", onClick = { clicked = true }) } + + composeTestRule.onNodeWithText("Tap me").performClick() + + assertTrue(clicked) + } +} diff --git a/components/verticalgrid/build.gradle.kts b/components/verticalgrid/build.gradle.kts index 306f95c0..a336b134 100644 --- a/components/verticalgrid/build.gradle.kts +++ b/components/verticalgrid/build.gradle.kts @@ -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") @@ -7,4 +5,7 @@ plugins { android { namespace = "com.guru.composecookbook.verticalgrid" } -dependencies { addComposeOfficialDependencies() } +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) +} diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 4e206914..b0e891cb 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -6,5 +6,6 @@ plugins { android { namespace = "com.guru.composecookbook.data" } dependencies { - implementation(com.guru.composecookbook.build.dependencies.Dependencies.composeRuntime) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.compose.runtime) } diff --git a/data/src/main/java/com/guru/composecookbook/data/DemoDataProvider.kt b/data/src/main/java/com/guru/composecookbook/data/DemoDataProvider.kt index f35390e2..a0590b96 100644 --- a/data/src/main/java/com/guru/composecookbook/data/DemoDataProvider.kt +++ b/data/src/main/java/com/guru/composecookbook/data/DemoDataProvider.kt @@ -346,6 +346,8 @@ object DemoDataProvider { HomeScreenItems.TabLayout, HomeScreenItems.BottomSheets, HomeScreenItems.PullRefresh, + HomeScreenItems.AdaptiveUI, + HomeScreenItems.PredictiveBack, ) const val longText = diff --git a/data/src/main/java/com/guru/composecookbook/data/model/HomeScreenItems.kt b/data/src/main/java/com/guru/composecookbook/data/model/HomeScreenItems.kt index e8303151..515eebd3 100644 --- a/data/src/main/java/com/guru/composecookbook/data/model/HomeScreenItems.kt +++ b/data/src/main/java/com/guru/composecookbook/data/model/HomeScreenItems.kt @@ -31,6 +31,10 @@ sealed class HomeScreenItems { object MotionLayout : HomeScreenItems() + object AdaptiveUI : HomeScreenItems() + + object PredictiveBack : HomeScreenItems() + val name: String get() = when (this) { @@ -49,5 +53,7 @@ sealed class HomeScreenItems { AdvanceLists -> "Advance Lists" PullRefresh -> "Pull refresh demos" CustomFling -> "Custom Fling" + AdaptiveUI -> "Adaptive UI (Large screens)" + PredictiveBack -> "Predictive Back gesture" } } diff --git a/demos/cryptoapp/app/build.gradle.kts b/demos/cryptoapp/app/build.gradle.kts index b3e6ede1..7d2f1282 100644 --- a/demos/cryptoapp/app/build.gradle.kts +++ b/demos/cryptoapp/app/build.gradle.kts @@ -1,9 +1,3 @@ -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.addThirdPartyUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -18,25 +12,17 @@ dependencies { implementation(project(":demos:cryptoapp:data")) implementation(project(":theme")) implementation(project(":data")) - implementation("androidx.wear:wear:1.2.0") - - addComposeOfficialDependencies() - addComposeThirdPartyDependencies() - - addCoreAndroidUiDependencies() - addThirdPartyUiDependencies() - - addCoreAndroidDependencies() + // Wear OS (compose-material here is the Wear variant, not the mobile one) + implementation(libs.bundles.wear) - val wear_compose_version = "1.0.0-alpha07" - implementation("androidx.wear.compose:compose-material:$wear_compose_version") + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.compose.thirdparty) - // Foundation is additive, so you can use the mobile version in your Wear OS app. - implementation("androidx.wear.compose:compose-foundation:$wear_compose_version") + implementation(libs.bundles.core.android.ui) + implementation(libs.bundles.thirdparty.ui) - // If you are using Compose Navigation, use the Wear OS version (NOT THE MOBILE ONE), that is, - // uncomment the line below and update the version number. - implementation("androidx.wear.compose:compose-navigation:$wear_compose_version") + implementation(libs.bundles.core.android) } android { buildFeatures { viewBinding = true } } diff --git a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/composewear/AndroidWearActivity.kt b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/composewear/AndroidWearActivity.kt index c87a1a1f..96c4f286 100644 --- a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/composewear/AndroidWearActivity.kt +++ b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/composewear/AndroidWearActivity.kt @@ -43,7 +43,7 @@ import androidx.wear.compose.material.rememberScalingLazyListState import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController -import coil.compose.rememberImagePainter +import coil3.compose.rememberAsyncImagePainter import com.guru.composecookbook.charts.LineChart import com.guru.composecookbook.cryptoapp.data.db.models.Crypto import com.guru.composecookbook.cryptoapp.ui.home.CryptoHomeViewModel @@ -129,7 +129,7 @@ fun WearStockListItem(crypto: Crypto, onItemSelected: (String) -> Unit) { Card(onClick = { onItemSelected.invoke(crypto.symbol) }, shape = MaterialTheme.shapes.small) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Image( - painter = rememberImagePainter(data = crypto.image), + painter = rememberAsyncImagePainter(model = crypto.image), modifier = Modifier.size(32.dp).padding(4.dp), contentDescription = null, contentScale = ContentScale.Crop, @@ -159,7 +159,7 @@ fun StockDetailScreen(crypto: Crypto) { ) { Row(verticalAlignment = Alignment.CenterVertically) { Image( - painter = rememberImagePainter(data = crypto.image), + painter = rememberAsyncImagePainter(model = crypto.image), modifier = Modifier.size(24.dp).padding(end = 8.dp), contentDescription = null, contentScale = ContentScale.Crop, diff --git a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/detail/components/CryptoDetailScreen.kt b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/detail/components/CryptoDetailScreen.kt index 25fe5324..417fab17 100644 --- a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/detail/components/CryptoDetailScreen.kt +++ b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/detail/components/CryptoDetailScreen.kt @@ -48,7 +48,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.rememberImagePainter +import coil3.compose.rememberAsyncImagePainter import com.guru.composecookbook.charts.BarCharts import com.guru.composecookbook.charts.LineChart import com.guru.composecookbook.cryptoapp.R @@ -102,7 +102,7 @@ fun CryptoTopSection(crypto: Crypto, scrollState: ScrollState) { Row(modifier = Modifier.padding(top = 20.dp)) { Text(text = crypto.name, style = typography.h6, modifier = Modifier.padding(end = 8.dp)) Image( - painter = rememberImagePainter(data = crypto.image), + painter = rememberAsyncImagePainter(model = crypto.image), modifier = Modifier.size(28.dp), contentDescription = null, ) diff --git a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/detail/components/FavoriteCryptoCard.kt b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/detail/components/FavoriteCryptoCard.kt index 78f19924..7696f18c 100644 --- a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/detail/components/FavoriteCryptoCard.kt +++ b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/detail/components/FavoriteCryptoCard.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.rememberImagePainter +import coil3.compose.rememberAsyncImagePainter import com.guru.composecookbook.cryptoapp.data.CryptoDemoDataProvider import com.guru.composecookbook.cryptoapp.data.db.models.Crypto import com.guru.composecookbook.cryptoapp.ui.internal.extensions.roundToTwoDecimals @@ -27,7 +27,7 @@ fun FavoriteCryptoCard(crypto: Crypto) { Column(modifier = Modifier.padding(16.dp).width(120.dp).height(180.dp)) { Row(modifier = Modifier.weight(1f)) { Image( - painter = rememberImagePainter(data = crypto.image), + painter = rememberAsyncImagePainter(model = crypto.image), modifier = Modifier.size(24.dp), contentDescription = null, ) diff --git a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/home/components/CryptoHomeScreen.kt b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/home/components/CryptoHomeScreen.kt index 6abeeac6..15b2b8e7 100644 --- a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/home/components/CryptoHomeScreen.kt +++ b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/home/components/CryptoHomeScreen.kt @@ -47,7 +47,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import coil.compose.rememberImagePainter +import coil3.compose.rememberAsyncImagePainter import com.guru.composecookbook.cryptoapp.data.CryptoDemoDataProvider import com.guru.composecookbook.cryptoapp.data.db.models.Crypto import com.guru.composecookbook.cryptoapp.ui.home.CryptoHomeInteractionEvents @@ -147,7 +147,7 @@ fun FavoriteItem(crypto: Crypto, openCryptoDetail: () -> Unit) { verticalAlignment = Alignment.CenterVertically, ) { Image( - painter = rememberImagePainter(data = crypto.image), + painter = rememberAsyncImagePainter(model = crypto.image), modifier = Modifier.size(24.dp), contentDescription = null, ) diff --git a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/home/components/CryptoListItem.kt b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/home/components/CryptoListItem.kt index 8b4b1740..198dd235 100644 --- a/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/home/components/CryptoListItem.kt +++ b/demos/cryptoapp/app/src/main/java/com/guru/composecookbook/cryptoapp/ui/home/components/CryptoListItem.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import com.guru.composecookbook.charts.LineChart import com.guru.composecookbook.cryptoapp.data.CryptoDemoDataProvider import com.guru.composecookbook.cryptoapp.data.db.models.Crypto diff --git a/demos/cryptoapp/data/build.gradle.kts b/demos/cryptoapp/data/build.gradle.kts index dc2390d4..d0af3dad 100644 --- a/demos/cryptoapp/data/build.gradle.kts +++ b/demos/cryptoapp/data/build.gradle.kts @@ -1,7 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addCoreAndroidDependencies -import com.guru.composecookbook.build.dependencies.addDataDependencies -import com.guru.composecookbook.build.dependencies.addNetworkingDependencies - plugins { /** See [common-kotlin-module-configs-script-plugin.gradle.kts] file */ id("common-kotlin-module-configs-script-plugin") @@ -10,7 +6,9 @@ plugins { android { namespace = "com.guru.composecookbook.cryptoapp.data" } dependencies { - addCoreAndroidDependencies() - addNetworkingDependencies() - addDataDependencies() + implementation(libs.bundles.core.android) + implementation(libs.bundles.networking) + implementation(platform(libs.androidx.compose.bom)) + ksp(libs.androidx.room.compiler) + implementation(libs.bundles.data) } diff --git a/demos/datingapp/build.gradle.kts b/demos/datingapp/build.gradle.kts index 68a5f29d..7cb8642f 100644 --- a/demos/datingapp/build.gradle.kts +++ b/demos/datingapp/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -14,6 +11,7 @@ dependencies { implementation(project(":data")) implementation(project(":theme")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) } diff --git a/demos/datingapp/src/main/java/com/guru/composecookbook/datingapp/DatingHomeActivity.kt b/demos/datingapp/src/main/java/com/guru/composecookbook/datingapp/DatingHomeActivity.kt index e37eb3d8..659b7501 100644 --- a/demos/datingapp/src/main/java/com/guru/composecookbook/datingapp/DatingHomeActivity.kt +++ b/demos/datingapp/src/main/java/com/guru/composecookbook/datingapp/DatingHomeActivity.kt @@ -21,8 +21,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -115,7 +115,7 @@ fun DatingHomeAppbar(navType: MutableState) { DatingNavType.CHATS -> "Chats" DatingNavType.PROFILE -> "My Profile" } - SmallTopAppBar( + TopAppBar( title = { Text(title, style = typography.h6) }, actions = { IconButton(onClick = {}) { Icons.Default.MyLocation } }, ) diff --git a/demos/gmail/build.gradle.kts b/demos/gmail/build.gradle.kts index 8007c68a..fd38254c 100644 --- a/demos/gmail/build.gradle.kts +++ b/demos/gmail/build.gradle.kts @@ -1,9 +1,7 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") + id("org.jetbrains.kotlin.plugin.serialization") } android { namespace = "com.guru.composecookbook.gmail" } @@ -13,6 +11,8 @@ dependencies { implementation(project(":data")) implementation(project(":theme")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() + implementation(libs.bundles.kotlin) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) } diff --git a/demos/gmail/src/main/java/com/guru/composecookbook/gmail/ui/home/GmailHome.kt b/demos/gmail/src/main/java/com/guru/composecookbook/gmail/ui/home/GmailHome.kt index c3010872..494fffb6 100644 --- a/demos/gmail/src/main/java/com/guru/composecookbook/gmail/ui/home/GmailHome.kt +++ b/demos/gmail/src/main/java/com/guru/composecookbook/gmail/ui/home/GmailHome.kt @@ -68,16 +68,24 @@ import com.guru.composecookbook.gmail.ui.details.MessageDetailScreen import com.guru.composecookbook.theme.graySurface import com.guru.composecookbook.theme.green500 import kotlin.math.absoluteValue +import kotlinx.serialization.Serializable + +/** Type-safe navigation routes for the Gmail demo. */ +@Serializable object GmailHomeRoute + +@Serializable object GmailDetailRoute + +@Serializable object GmailCreateRoute @Composable fun GmailScreen() { val navController = rememberNavController() - NavHost(navController = navController, startDestination = "home") { - composable("home") { GmailHome(navController = navController) } + NavHost(navController = navController, startDestination = GmailHomeRoute) { + composable { GmailHome(navController = navController) } - composable("detail") { MessageDetailScreen(navController = navController) } + composable { MessageDetailScreen(navController = navController) } - composable("create") { CreateMessageScreen(navController = navController) } + composable { CreateMessageScreen(navController = navController) } } } @@ -157,7 +165,7 @@ fun IconWithBadge(badge: Int, icon: ImageVector, modifier: Modifier = Modifier) fun GmailFloatingActionButton(navController: NavHostController, expandState: Boolean) { FloatingActionButton( - onClick = { navController.navigate("create") }, + onClick = { navController.navigate(GmailCreateRoute) }, modifier = Modifier.padding(16.dp).height(48.dp).widthIn(min = 48.dp), backgroundColor = MaterialTheme.colors.surface, contentColor = MaterialTheme.colors.primary, @@ -241,14 +249,14 @@ fun GmailContent( AnimatedVisibility(visible = visible.value) { Box(modifier = Modifier.background(green500)) { GmailListActionItems(modifier = Modifier.align(Alignment.CenterEnd)) - GmailListItem(it) { navController.navigate("detail") } + GmailListItem(it) { navController.navigate(GmailDetailRoute) } } } } } SearchLayout(searchOffsetY.value, scaffoldState.drawerState, showUserDialog) { - navController.navigate("create") + navController.navigate(GmailCreateRoute) } } } diff --git a/demos/instagram/build.gradle.kts b/demos/instagram/build.gradle.kts index 8903bc93..0524875a 100644 --- a/demos/instagram/build.gradle.kts +++ b/demos/instagram/build.gradle.kts @@ -1,7 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addComposeThirdPartyDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -13,7 +9,8 @@ dependencies { implementation(project(":data")) implementation(project(":theme")) - addComposeOfficialDependencies() - addComposeThirdPartyDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.compose.thirdparty) + implementation(libs.bundles.core.android.ui) } diff --git a/demos/instagram/src/main/java/com/guru/composecookbook/instagram/components/stories/StoryItem.kt b/demos/instagram/src/main/java/com/guru/composecookbook/instagram/components/stories/StoryItem.kt index 3ee365fa..01e0e3d3 100644 --- a/demos/instagram/src/main/java/com/guru/composecookbook/instagram/components/stories/StoryItem.kt +++ b/demos/instagram/src/main/java/com/guru/composecookbook/instagram/components/stories/StoryItem.kt @@ -8,8 +8,8 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.MaterialTheme -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -59,7 +59,7 @@ fun StoryItem( ) .clickable( interactionSource = interactionSource, - indication = rememberRipple(bounded = false, radius = ProfileSizes.large / 2), + indication = ripple(bounded = false, radius = ProfileSizes.large / 2), enabled = true, onClickLabel = null, onClick = onClick, diff --git a/demos/meditation/build.gradle.kts b/demos/meditation/build.gradle.kts index 16e51963..5c5f380e 100644 --- a/demos/meditation/build.gradle.kts +++ b/demos/meditation/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -12,10 +9,11 @@ dependencies { implementation(project(":theme")) implementation(project(":components:verticalgrid")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) // Test dependencies - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.junit) } diff --git a/demos/moviesapp/app/build.gradle.kts b/demos/moviesapp/app/build.gradle.kts index 8dda942d..9b0c301c 100644 --- a/demos/moviesapp/app/build.gradle.kts +++ b/demos/moviesapp/app/build.gradle.kts @@ -1,9 +1,3 @@ -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.addThirdPartyUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -18,11 +12,12 @@ dependencies { implementation(project(":theme")) implementation(project(":data")) - addComposeOfficialDependencies() - addComposeThirdPartyDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.compose.thirdparty) - addCoreAndroidUiDependencies() - addThirdPartyUiDependencies() + implementation(libs.bundles.core.android.ui) + implementation(libs.bundles.thirdparty.ui) - addCoreAndroidDependencies() + implementation(libs.bundles.core.android) } diff --git a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/MovieDetailActivity.kt b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/MovieDetailActivity.kt index 8d71bb93..4f29f0cb 100644 --- a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/MovieDetailActivity.kt +++ b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/MovieDetailActivity.kt @@ -6,7 +6,7 @@ import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import coil.annotation.ExperimentalCoilApi +import coil3.annotation.ExperimentalCoilApi import com.guru.composecookbook.moviesapp.data.db.models.Movie import com.guru.composecookbook.moviesapp.ui.details.components.MovieDetailContent import com.guru.composecookbook.theme.ComposeCookBookTheme diff --git a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/components/MovieDetailContent.kt b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/components/MovieDetailContent.kt index 947c50e8..c7982651 100644 --- a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/components/MovieDetailContent.kt +++ b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/components/MovieDetailContent.kt @@ -1,118 +1,118 @@ -package com.guru.composecookbook.moviesapp.ui.details.components - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Button -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.LibraryAdd -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asAndroidBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.imageResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.AsyncImagePainter -import coil.compose.rememberAsyncImagePainter -import com.guru.composecookbook.moviesapp.data.db.models.Movie -import com.guru.composecookbook.moviesapp.ui.details.MovieDetailViewModel -import com.guru.composecookbook.moviesapp.ui.details.MovieDetailViewModelFactory -import com.guru.composecookbook.theme.extensions.generateDominantColorState -import com.guru.composecookbook.theme.graySurface -import com.guru.composecookbook.theme.modifiers.verticalGradientBackground -import com.guru.composecookbook.theme.typography - -@Composable -fun MovieDetailContent(movie: Movie, imageId: Int) { - val expand = remember { mutableStateOf(false) } - val viewModel: MovieDetailViewModel = - viewModel(factory = MovieDetailViewModelFactory(LocalContext.current)) - var dominantColors = listOf(graySurface, Color.Black) - - if (imageId != 0) { - val context = LocalContext.current - val currentBitmap = ImageBitmap.imageResource(context.resources, imageId) - - val swatch = currentBitmap.asAndroidBitmap().generateDominantColorState() - dominantColors = listOf(Color(swatch.rgb), Color.Black) - } - - LazyColumn( - modifier = - Modifier.verticalGradientBackground(dominantColors) - .padding(animateDpAsState(if (expand.value) 1.dp else 120.dp, tween(350)).value) - ) { - item { - val painter = - rememberAsyncImagePainter(model = "https://image.tmdb.org/t/p/w500/${movie.poster_path}") - Image( - painter = painter, - contentScale = ContentScale.Crop, - contentDescription = null, - modifier = Modifier.height(600.dp).fillMaxWidth(), - ) - when (painter.state) { - is AsyncImagePainter.State.Success -> expand.value = true - else -> expand.value = false - } - } - item { - Column(modifier = Modifier.background(MaterialTheme.colors.onSurface)) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = movie.title, modifier = Modifier.padding(8.dp), style = typography.h6) - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Default.LibraryAdd, - contentDescription = null, - tint = MaterialTheme.colors.primary, - ) - } - } - GenreSection(viewModel, movie.genre_ids) - Text( - text = "Release: ${movie.release_date}", - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - style = typography.h6.copy(fontSize = 12.sp), - ) - Text( - text = "PG13 • ${movie.vote_average}/10", - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - style = typography.h6.copy(fontSize = 12.sp, fontWeight = FontWeight.Medium), - ) - Text(text = movie.overview, modifier = Modifier.padding(8.dp), style = typography.subtitle2) - Spacer(modifier = Modifier.height(20.dp)) - SimilarMoviesSection(movie, viewModel) - Spacer(modifier = Modifier.height(50.dp)) - Button(onClick = {}, modifier = Modifier.fillMaxWidth()) { - Text(text = "Get Tickets", modifier = Modifier.padding(8.dp)) - } - } - } - } -} +package com.guru.composecookbook.moviesapp.ui.details.components + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LibraryAdd +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil3.compose.AsyncImagePainter +import coil3.compose.rememberAsyncImagePainter +import com.guru.composecookbook.moviesapp.data.db.models.Movie +import com.guru.composecookbook.moviesapp.ui.details.MovieDetailViewModel +import com.guru.composecookbook.moviesapp.ui.details.MovieDetailViewModelFactory +import com.guru.composecookbook.theme.extensions.generateDominantColorState +import com.guru.composecookbook.theme.graySurface +import com.guru.composecookbook.theme.modifiers.verticalGradientBackground +import com.guru.composecookbook.theme.typography + +@Composable +fun MovieDetailContent(movie: Movie, imageId: Int) { + val expand = remember { mutableStateOf(false) } + val viewModel: MovieDetailViewModel = + viewModel(factory = MovieDetailViewModelFactory(LocalContext.current)) + var dominantColors = listOf(graySurface, Color.Black) + + if (imageId != 0) { + val context = LocalContext.current + val currentBitmap = ImageBitmap.imageResource(context.resources, imageId) + + val swatch = currentBitmap.asAndroidBitmap().generateDominantColorState() + dominantColors = listOf(Color(swatch.rgb), Color.Black) + } + + LazyColumn( + modifier = + Modifier.verticalGradientBackground(dominantColors) + .padding(animateDpAsState(if (expand.value) 1.dp else 120.dp, tween(350)).value) + ) { + item { + val painter = + rememberAsyncImagePainter(model = "https://image.tmdb.org/t/p/w500/${movie.poster_path}") + Image( + painter = painter, + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier.height(600.dp).fillMaxWidth(), + ) + when (painter.state) { + is AsyncImagePainter.State.Success -> expand.value = true + else -> expand.value = false + } + } + item { + Column(modifier = Modifier.background(MaterialTheme.colors.onSurface)) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = movie.title, modifier = Modifier.padding(8.dp), style = typography.h6) + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Default.LibraryAdd, + contentDescription = null, + tint = MaterialTheme.colors.primary, + ) + } + } + GenreSection(viewModel, movie.genre_ids) + Text( + text = "Release: ${movie.release_date}", + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = typography.h6.copy(fontSize = 12.sp), + ) + Text( + text = "PG13 • ${movie.vote_average}/10", + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = typography.h6.copy(fontSize = 12.sp, fontWeight = FontWeight.Medium), + ) + Text(text = movie.overview, modifier = Modifier.padding(8.dp), style = typography.subtitle2) + Spacer(modifier = Modifier.height(20.dp)) + SimilarMoviesSection(movie, viewModel) + Spacer(modifier = Modifier.height(50.dp)) + Button(onClick = {}, modifier = Modifier.fillMaxWidth()) { + Text(text = "Get Tickets", modifier = Modifier.padding(8.dp)) + } + } + } + } +} diff --git a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/components/SimilarMoviesSection.kt b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/components/SimilarMoviesSection.kt index 2da4e797..7da79a33 100644 --- a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/components/SimilarMoviesSection.kt +++ b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/details/components/SimilarMoviesSection.kt @@ -1,45 +1,47 @@ -package com.guru.composecookbook.moviesapp.ui.details.components - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp -import coil.compose.rememberImagePainter -import com.guru.composecookbook.moviesapp.data.db.models.Movie -import com.guru.composecookbook.moviesapp.ui.details.MovieDetailViewModel -import com.guru.composecookbook.theme.typography - -@Composable -fun SimilarMoviesSection(currentMovie: Movie?, viewModel: MovieDetailViewModel) { - viewModel.getSimilarMovies(currentMovie?.id.toString()) - val similarMovies by viewModel.similarMoviesLiveData.observeAsState() - similarMovies?.let { movies -> - Text(text = "Similar Movies", style = typography.h5, modifier = Modifier.padding(8.dp)) - LazyRow { - items( - items = movies, - itemContent = { movie: Movie -> - Image( - painter = - rememberImagePainter(data = "https://image.tmdb.org/t/p/w500/${movie.poster_path}"), - contentDescription = null, - modifier = - Modifier.width(200.dp).height(300.dp).padding(12.dp).clip(RoundedCornerShape(12.dp)), - contentScale = ContentScale.Crop, - ) - }, - ) - } - } -} +package com.guru.composecookbook.moviesapp.ui.details.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil3.compose.rememberAsyncImagePainter +import com.guru.composecookbook.moviesapp.data.db.models.Movie +import com.guru.composecookbook.moviesapp.ui.details.MovieDetailViewModel +import com.guru.composecookbook.theme.typography + +@Composable +fun SimilarMoviesSection(currentMovie: Movie?, viewModel: MovieDetailViewModel) { + viewModel.getSimilarMovies(currentMovie?.id.toString()) + val similarMovies by viewModel.similarMoviesLiveData.observeAsState() + similarMovies?.let { movies -> + Text(text = "Similar Movies", style = typography.h5, modifier = Modifier.padding(8.dp)) + LazyRow { + items( + items = movies, + itemContent = { movie: Movie -> + Image( + painter = + rememberAsyncImagePainter( + model = "https://image.tmdb.org/t/p/w500/${movie.poster_path}" + ), + contentDescription = null, + modifier = + Modifier.width(200.dp).height(300.dp).padding(12.dp).clip(RoundedCornerShape(12.dp)), + contentScale = ContentScale.Crop, + ) + }, + ) + } + } +} diff --git a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/home/MoviesHomeActivity.kt b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/home/MoviesHomeActivity.kt index 535da41b..93efedf0 100644 --- a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/home/MoviesHomeActivity.kt +++ b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/home/MoviesHomeActivity.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel -import coil.annotation.ExperimentalCoilApi +import coil3.annotation.ExperimentalCoilApi import com.guru.composecookbook.moviesapp.data.db.models.Movie import com.guru.composecookbook.moviesapp.ui.details.MovieDetailActivity import com.guru.composecookbook.moviesapp.ui.home.components.MovieHomeScreen diff --git a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/home/components/MoviePagerItem.kt b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/home/components/MoviePagerItem.kt index 334e5281..cbe3f0ec 100644 --- a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/home/components/MoviePagerItem.kt +++ b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/home/components/MoviePagerItem.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.rememberImagePainter +import coil3.compose.rememberAsyncImagePainter import com.guru.composecookbook.moviesapp.data.DemoMovieDataProvider import com.guru.composecookbook.moviesapp.data.db.models.Genre import com.guru.composecookbook.moviesapp.data.db.models.Movie @@ -94,7 +94,7 @@ fun MoviePagerItem( ) { Column { Image( - painter = rememberImagePainter(data = posterFullPath), + painter = rememberAsyncImagePainter(model = posterFullPath), contentScale = ContentScale.Crop, contentDescription = null, modifier = Modifier.fillMaxWidth().height(360.dp), diff --git a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/trending/components/MoviesLaneItem.kt b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/trending/components/MoviesLaneItem.kt index e23ea3dd..864b77a9 100644 --- a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/trending/components/MoviesLaneItem.kt +++ b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/trending/components/MoviesLaneItem.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp -import coil.compose.rememberImagePainter +import coil3.compose.rememberAsyncImagePainter import com.guru.composecookbook.moviesapp.data.db.models.Movie import com.guru.composecookbook.theme.typography @@ -33,7 +33,9 @@ fun MoviesLaneItem(movies: List, title: String = "", onMovieSelected: (Mo itemContent = { movie: Movie -> Image( painter = - rememberImagePainter(data = "https://image.tmdb.org/t/p/w500/${movie.poster_path}"), + rememberAsyncImagePainter( + model = "https://image.tmdb.org/t/p/w500/${movie.poster_path}" + ), contentDescription = null, modifier = Modifier.width(190.dp) diff --git a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/watch/components/MovieWatchlistItem.kt b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/watch/components/MovieWatchlistItem.kt index f7ee868b..a9086710 100644 --- a/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/watch/components/MovieWatchlistItem.kt +++ b/demos/moviesapp/app/src/main/java/com/guru/composecookbook/moviesapp/ui/watch/components/MovieWatchlistItem.kt @@ -1,56 +1,58 @@ -package com.guru.composecookbook.moviesapp.ui.watch.components - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.RemoveCircleOutline -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import coil.compose.rememberImagePainter -import com.guru.composecookbook.moviesapp.data.db.models.Movie -import com.guru.composecookbook.theme.typography - -@Composable -fun MovieWatchlistItem( - movie: Movie, - onMovieSelected: () -> Unit, - onRemoveFromWatchlist: () -> Unit, -) { - Box(modifier = Modifier.clickable(onClick = onMovieSelected)) { - Image( - painter = - rememberImagePainter(data = "https://image.tmdb.org/t/p/original/${movie.backdrop_path}"), - contentDescription = null, - modifier = Modifier.fillMaxWidth().height(280.dp), - contentScale = ContentScale.Crop, - ) - Spacer( // overlay - modifier = Modifier.fillMaxWidth().height(280.dp).background(Color.Black.copy(alpha = 0.1f)) - ) - Text( - text = movie.title, - style = typography.h6.copy(fontWeight = FontWeight.ExtraBold), - modifier = Modifier.align(Alignment.BottomStart).padding(8.dp), - ) - IconButton( - onClick = { onRemoveFromWatchlist.invoke() }, - modifier = Modifier.align(Alignment.BottomEnd), - ) { - Icon(imageVector = Icons.Default.RemoveCircleOutline, contentDescription = null) - } - } -} +package com.guru.composecookbook.moviesapp.ui.watch.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.RemoveCircleOutline +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil3.compose.rememberAsyncImagePainter +import com.guru.composecookbook.moviesapp.data.db.models.Movie +import com.guru.composecookbook.theme.typography + +@Composable +fun MovieWatchlistItem( + movie: Movie, + onMovieSelected: () -> Unit, + onRemoveFromWatchlist: () -> Unit, +) { + Box(modifier = Modifier.clickable(onClick = onMovieSelected)) { + Image( + painter = + rememberAsyncImagePainter( + model = "https://image.tmdb.org/t/p/original/${movie.backdrop_path}" + ), + contentDescription = null, + modifier = Modifier.fillMaxWidth().height(280.dp), + contentScale = ContentScale.Crop, + ) + Spacer( // overlay + modifier = Modifier.fillMaxWidth().height(280.dp).background(Color.Black.copy(alpha = 0.1f)) + ) + Text( + text = movie.title, + style = typography.h6.copy(fontWeight = FontWeight.ExtraBold), + modifier = Modifier.align(Alignment.BottomStart).padding(8.dp), + ) + IconButton( + onClick = { onRemoveFromWatchlist.invoke() }, + modifier = Modifier.align(Alignment.BottomEnd), + ) { + Icon(imageVector = Icons.Default.RemoveCircleOutline, contentDescription = null) + } + } +} diff --git a/demos/moviesapp/data/build.gradle.kts b/demos/moviesapp/data/build.gradle.kts index f6325bd1..2a084a48 100644 --- a/demos/moviesapp/data/build.gradle.kts +++ b/demos/moviesapp/data/build.gradle.kts @@ -1,7 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addCoreAndroidDependencies -import com.guru.composecookbook.build.dependencies.addDataDependencies -import com.guru.composecookbook.build.dependencies.addNetworkingDependencies - plugins { /** See [common-kotlin-module-configs-script-plugin.gradle.kts] file */ id("common-kotlin-module-configs-script-plugin") @@ -10,7 +6,9 @@ plugins { android { namespace = "com.guru.composecookbook.moviesapp.data" } dependencies { - addCoreAndroidDependencies() - addNetworkingDependencies() - addDataDependencies() + implementation(libs.bundles.core.android) + implementation(libs.bundles.networking) + implementation(platform(libs.androidx.compose.bom)) + ksp(libs.androidx.room.compiler) + implementation(libs.bundles.data) } diff --git a/demos/paint/build.gradle.kts b/demos/paint/build.gradle.kts index adaec39c..2a94d6c5 100644 --- a/demos/paint/build.gradle.kts +++ b/demos/paint/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -12,6 +9,7 @@ dependencies { implementation(project(":theme")) implementation(project(":components:colorpicker")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) } diff --git a/demos/spotify/build.gradle.kts b/demos/spotify/build.gradle.kts index fccc36c8..288ce693 100644 --- a/demos/spotify/build.gradle.kts +++ b/demos/spotify/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -13,6 +10,7 @@ dependencies { implementation(project(":data")) implementation(project(":theme")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) } diff --git a/demos/tiktok/build.gradle.kts b/demos/tiktok/build.gradle.kts index 05f45d1c..a8c19b39 100644 --- a/demos/tiktok/build.gradle.kts +++ b/demos/tiktok/build.gradle.kts @@ -1,11 +1,7 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies -import com.guru.composecookbook.build.dependencies.addGoogleAndroidDependencies -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") + id("org.jetbrains.kotlin.plugin.serialization") } android { namespace = "com.guru.composecookbook.tiktok" } @@ -16,8 +12,10 @@ dependencies { implementation(project(":data")) implementation(project(":theme")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() - addThirdPartyUiDependencies() - addGoogleAndroidDependencies() + implementation(libs.bundles.kotlin) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) + implementation(libs.bundles.thirdparty.ui) + implementation(libs.bundles.google.android) } diff --git a/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/TikTokScreen.kt b/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/TikTokScreen.kt index ae9df718..86af936d 100644 --- a/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/TikTokScreen.kt +++ b/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/TikTokScreen.kt @@ -1,15 +1,41 @@ package com.guru.composecookbook.tiktok -sealed class TikTokScreen(val route: String) { - object Home : TikTokScreen("Home") +import kotlinx.serialization.Serializable - object Discover : TikTokScreen("Discover") +/** + * Type-safe navigation routes for the TikTok demo. Each screen is a serializable route; [route] is + * kept as the human readable label shown in the bottom bar. + */ +sealed interface TikTokScreen { + val route: String - object Create : TikTokScreen("Create") + @Serializable + data object Home : TikTokScreen { + override val route = "Home" + } - object Inbox : TikTokScreen("Inbox") + @Serializable + data object Discover : TikTokScreen { + override val route = "Discover" + } - object Me : TikTokScreen("Me") + @Serializable + data object Create : TikTokScreen { + override val route = "Create" + } - object Profile : TikTokScreen("Profile") + @Serializable + data object Inbox : TikTokScreen { + override val route = "Inbox" + } + + @Serializable + data object Me : TikTokScreen { + override val route = "Me" + } + + @Serializable + data class Profile(val userId: String) : TikTokScreen { + override val route = "Profile" + } } diff --git a/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/TiktokActivity.kt b/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/TiktokActivity.kt index 57438e1d..58319dae 100644 --- a/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/TiktokActivity.kt +++ b/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/TiktokActivity.kt @@ -20,11 +20,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.core.content.ContextCompat +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute import com.guru.composecookbook.theme.ComposeCookBookTheme import com.guru.composecookbook.theme.tiktokBlack import com.guru.composecookbook.tiktok.components.discovers.DiscoverScreen @@ -60,17 +62,18 @@ fun TiktokAppContent() { @Composable fun TikTokBottomNavigation(navController: NavHostController) { val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.arguments?.getString("route") + val currentDestination = navBackStackEntry?.destination BottomNavigation(backgroundColor = tiktokBlack) { TiktokDemoDataProvider.bottomBarList.forEach { tiktokScreen -> + val isSelected = currentDestination?.hasRoute(tiktokScreen::class) == true BottomNavigationItem( icon = { BottomBarIcon(tiktokScreen) }, - selected = currentRoute == tiktokScreen.route, + selected = isSelected, onClick = { navController.popBackStack(navController.graph.startDestinationId, false) - if (currentRoute != tiktokScreen.route) { - navController.navigate(tiktokScreen.route) + if (!isSelected) { + navController.navigate(tiktokScreen) } }, label = { @@ -91,7 +94,7 @@ fun BottomBarIcon(screen: TikTokScreen) { TikTokScreen.Create -> TiktokCreateIcon() TikTokScreen.Inbox -> Icon(imageVector = Icons.Filled.Inbox, contentDescription = null) TikTokScreen.Me -> Icon(imageVector = Icons.Filled.Person, contentDescription = null) - TikTokScreen.Profile -> { + is TikTokScreen.Profile -> { /* this branch added to satisfy exhaustive when */ } } @@ -101,19 +104,19 @@ fun BottomBarIcon(screen: TikTokScreen) { fun TikTokBodyContent(navController: NavHostController, modifier: Modifier = Modifier) { NavHost( navController = navController, - startDestination = TikTokScreen.Home.route, + startDestination = TikTokScreen.Home, modifier = modifier, ) { - composable(TikTokScreen.Home.route) { + composable { HomeScreen(tiktokInteractionEvents = { handleInteractionEvent(it, navController) }) } - composable(TikTokScreen.Discover.route) { DiscoverScreen() } - composable(TikTokScreen.Create.route) { Text(text = "Create:TODO") } - composable(TikTokScreen.Inbox.route) { Text(text = "Inbox:TODO") } - composable(TikTokScreen.Me.route) { ProfileScreen("10", navController) } + composable { DiscoverScreen() } + composable { Text(text = "Create:TODO") } + composable { Text(text = "Inbox:TODO") } + composable { ProfileScreen("10", navController) } // This navigation is for going to user profile but it should be moved to separate place - composable("${TikTokScreen.Profile.route}/{userId}") { backStackEntry -> - ProfileScreen(backStackEntry.arguments?.getString("userId")!!, navController) + composable { backStackEntry -> + ProfileScreen(backStackEntry.toRoute().userId, navController) } } } @@ -125,7 +128,7 @@ fun handleInteractionEvent( when (tiktokHomeInteractionEvents) { is TiktokHomeInteractionEvents.OpenProfile -> { navController.navigate( - "${TikTokScreen.Profile.route}/${tiktokHomeInteractionEvents.album.id}" + TikTokScreen.Profile(userId = tiktokHomeInteractionEvents.album.id.toString()) ) } else -> { diff --git a/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/components/discovers/MediaItem.kt b/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/components/discovers/MediaItem.kt index a9fa1275..ac401e9e 100644 --- a/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/components/discovers/MediaItem.kt +++ b/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/components/discovers/MediaItem.kt @@ -1,21 +1,21 @@ -package com.guru.composecookbook.tiktok.components.discovers - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp -import coil.compose.rememberImagePainter - -@Composable -fun MediaItem(id: Int, modifier: Modifier = Modifier) { - Image( - painter = rememberImagePainter(data = "https://picsum.photos/id/${id}/200/200"), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = modifier.height(150.dp).width(120.dp).padding(2.dp), - ) -} +package com.guru.composecookbook.tiktok.components.discovers + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil3.compose.rememberAsyncImagePainter + +@Composable +fun MediaItem(id: Int, modifier: Modifier = Modifier) { + Image( + painter = rememberAsyncImagePainter(model = "https://picsum.photos/id/${id}/200/200"), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = modifier.height(150.dp).width(120.dp).padding(2.dp), + ) +} diff --git a/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/components/home/TikTokPlayer.kt b/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/components/home/TikTokPlayer.kt index 0a1fd66f..82ae0a8c 100644 --- a/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/components/home/TikTokPlayer.kt +++ b/demos/tiktok/src/main/java/com/guru/composecookbook/tiktok/components/home/TikTokPlayer.kt @@ -1,30 +1,26 @@ package com.guru.composecookbook.tiktok.components.home import android.content.Context -import android.net.Uri +import androidx.annotation.OptIn import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.viewinterop.AndroidView -import com.google.android.exoplayer2.C -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.SimpleExoPlayer -import com.google.android.exoplayer2.source.ProgressiveMediaSource -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout -import com.google.android.exoplayer2.ui.PlayerView -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView +@OptIn(UnstableApi::class) @Composable fun TikTokPlayer(context: Context, url: String, selected: Boolean) { val tiktokPlayer = remember { - SimpleExoPlayer.Builder(context).build().apply { - val mediaSource = - ProgressiveMediaSource.Factory(DefaultDataSourceFactory(context, "composeCookBook")) - .createMediaSource(MediaItem.fromUri(Uri.parse("asset:///${url}"))) - - this.setMediaSource(mediaSource, true) - this.prepare() + ExoPlayer.Builder(context).build().apply { + setMediaItem(MediaItem.fromUri("asset:///$url")) + prepare() } } tiktokPlayer.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING diff --git a/demos/twitter/build.gradle.kts b/demos/twitter/build.gradle.kts index b79080a0..f9667a0a 100644 --- a/demos/twitter/build.gradle.kts +++ b/demos/twitter/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -12,6 +9,7 @@ dependencies { implementation(project(":data")) implementation(project(":theme")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) } diff --git a/demos/youtube/build.gradle.kts b/demos/youtube/build.gradle.kts index f4c51474..60586b5a 100644 --- a/demos/youtube/build.gradle.kts +++ b/demos/youtube/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -12,6 +9,7 @@ dependencies { implementation(project(":data")) implementation(project(":theme")) - addComposeOfficialDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.core.android.ui) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..09baf9b6 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,178 @@ +[versions] +agp = "8.7.3" +kotlin = "2.1.20" +ksp = "2.1.20-1.0.31" +ktfmt = "0.22.0" + +activity-compose = "1.10.1" +appcompat = "1.7.1" +biometric = "1.2.0-alpha05" +coil = "3.1.0" +compose-bom = "2024.12.01" +constraintlayout-compose = "1.1.0" +core-ktx = "1.16.0" +coroutines = "1.10.2" +flinger = "1.0.5" +fontawesome-compose = "1.1.0" +gson = "2.10.1" +junit-jupiter = "5.10.1" +junit4 = "4.13.2" +junit-androidx = "1.1.5" +kotlinx-serialization-json = "1.8.0" +lifecycle = "2.8.7" +logging-interceptor = "4.12.0" +lottie = "6.3.0" +material = "1.12.0" +media3 = "1.5.1" +multidex = "2.0.1" +navigation-compose = "2.8.9" +paging = "3.3.6" +palette-ktx = "1.0.0" +play-services-ads = "22.5.0" +play-services-maps = "18.2.0" +retrofit = "2.11.0" +room = "2.7.1" +truth = "1.1.3" +wear = "1.2.0" +wear-compose = "1.0.0-alpha07" + +[libraries] +# Kotlin +kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } + +# Compose (versions managed by the BOM) +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } +compose-ui = { module = "androidx.compose.ui:ui" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-material = { module = "androidx.compose.material:material" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" } +compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive" } +compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout" } +compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation" } +compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" } +compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +compose-runtime = { module = "androidx.compose.runtime:runtime" } +compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } + +# Compose adjacent +compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +compose-constraintlayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayout-compose" } +compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } +compose-paging = { module = "androidx.paging:paging-compose", version.ref = "paging" } +compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } + +# AndroidX +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } +androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } +androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } +androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "paging" } +androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "palette-ktx" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" } +androidx-wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wear-compose" } +androidx-wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wear-compose" } +androidx-wear-compose-navigation = { module = "androidx.wear.compose:compose-navigation", version.ref = "wear-compose" } + +# Media3 +media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } +media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } + +# Google +google-material = { module = "com.google.android.material:material", version.ref = "material" } +play-services-ads = { module = "com.google.android.gms:play-services-ads", version.ref = "play-services-ads" } +play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "play-services-maps" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } + +# Networking +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "logging-interceptor" } + +# Third party UI +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } +lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" } +lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie" } +fontawesome-compose = { module = "com.github.Gurupreet:FontAwesomeCompose", version.ref = "fontawesome-compose" } +flinger = { module = "com.github.iamjosephmj:flinger", version.ref = "flinger" } + +# Testing +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" } +truth = { module = "com.google.truth:truth", version.ref = "truth" } +junit = { module = "junit:junit", version.ref = "junit4" } +androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "junit-androidx" } + +# Build plugins used on classpaths +android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } +android-gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "agp" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } +kotlin-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } +ksp-gradle-plugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" } + +[bundles] +kotlin = ["kotlin-stdlib-jdk8", "kotlinx-coroutines-core", "kotlinx-coroutines-android", "kotlinx-serialization-json"] +kotlin-test = ["kotlin-test", "kotlin-test-junit5"] +compose-official = [ + "compose-ui", + "compose-ui-tooling", + "compose-material", + "compose-material3", + "compose-material-icons-extended", + "compose-runtime-livedata", + "compose-constraintlayout", + "compose-paging", + "compose-viewmodel", + "compose-activity", + "compose-navigation", + "compose-runtime", + "compose-material3-adaptive", + "compose-material3-adaptive-layout", + "compose-material3-adaptive-navigation", + "compose-material3-adaptive-navigation-suite", +] +compose-thirdparty = ["fontawesome-compose", "lottie-compose", "flinger"] +compose-debug = ["kotlin-reflect", "compose-ui-tooling", "compose-ui-test-manifest"] +data = ["androidx-room-runtime", "androidx-room-ktx", "androidx-paging-runtime-ktx", "compose-runtime"] +core-android = ["androidx-multidex", "androidx-core-ktx", "androidx-appcompat"] +core-android-ui = [ + "google-material", + "androidx-palette-ktx", + "androidx-paging-runtime-ktx", + "androidx-lifecycle-runtime-ktx", + "androidx-lifecycle-viewmodel-savedstate", + "androidx-lifecycle-livedata-ktx", + "androidx-lifecycle-viewmodel-ktx", +] +google-android = ["media3-exoplayer", "media3-ui", "play-services-ads", "play-services-maps"] +networking = ["retrofit", "okhttp-logging-interceptor", "gson", "retrofit-converter-gson"] +junit5-test = ["junit-jupiter-api", "junit-jupiter-engine"] +instrumentation-test = ["compose-ui-test-junit4", "compose-activity", "androidx-test-junit", "compose-ui-test-manifest"] +thirdparty-ui = ["coil-compose", "coil-network-okhttp", "lottie", "lottie-compose"] +wear = ["androidx-wear", "androidx-wear-compose-material", "androidx-wear-compose-foundation", "androidx-wear-compose-navigation"] + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } diff --git a/templates/cascademenu/build.gradle.kts b/templates/cascademenu/build.gradle.kts index 0b9c7b3f..49f4694e 100644 --- a/templates/cascademenu/build.gradle.kts +++ b/templates/cascademenu/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addComposeThirdPartyDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -13,6 +10,7 @@ dependencies { implementation(project(":data")) implementation(project(":components:tags")) - addComposeOfficialDependencies() - addComposeThirdPartyDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.compose.thirdparty) } diff --git a/templates/login/build.gradle.kts b/templates/login/build.gradle.kts index f27aa2d8..2b6a8940 100644 --- a/templates/login/build.gradle.kts +++ b/templates/login/build.gradle.kts @@ -1,6 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addComposeThirdPartyDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -15,6 +12,10 @@ dependencies { implementation(project(":components:tags")) implementation(project(":templates:onboarding")) - addComposeOfficialDependencies() - addComposeThirdPartyDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.compose.thirdparty) + + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.bundles.instrumentation.test) } diff --git a/templates/login/src/androidTest/java/com/guru/composecookbook/login/LoginScreenTest.kt b/templates/login/src/androidTest/java/com/guru/composecookbook/login/LoginScreenTest.kt new file mode 100644 index 00000000..db15c9de --- /dev/null +++ b/templates/login/src/androidTest/java/com/guru/composecookbook/login/LoginScreenTest.kt @@ -0,0 +1,42 @@ +package com.guru.composecookbook.login + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import org.junit.Rule +import org.junit.Test + +class LoginScreenTest { + + @get:Rule val composeTestRule = createComposeRule() + + private fun setLoginContent(onLoginSuccess: () -> Unit = {}) { + composeTestRule.setContent { LoginScreen(onLoginSuccess = onLoginSuccess) } + } + + @Test + fun loginFieldsAreDisplayed() { + setLoginContent() + + composeTestRule.onNodeWithText("Email address").assertIsDisplayed() + composeTestRule.onNodeWithText("Password").assertIsDisplayed() + } + + @Test + fun loginButtonIsDisplayed() { + setLoginContent() + + composeTestRule.onNodeWithText("Log In").performScrollTo().assertIsDisplayed() + } + + @Test + fun typedEmailIsReflectedInTheField() { + setLoginContent() + + composeTestRule.onNodeWithText("Email address").performTextInput("user@example.com") + + composeTestRule.onNodeWithText("user@example.com").assertIsDisplayed() + } +} diff --git a/templates/onboarding/build.gradle.kts b/templates/onboarding/build.gradle.kts index 4e7d65de..141f1ea0 100644 --- a/templates/onboarding/build.gradle.kts +++ b/templates/onboarding/build.gradle.kts @@ -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") @@ -11,5 +9,6 @@ dependencies { implementation(project(":components:carousel")) implementation(project(":animations:lottie")) - addComposeOfficialDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) } diff --git a/templates/paymentcard/build.gradle.kts b/templates/paymentcard/build.gradle.kts index 842070a3..967511be 100644 --- a/templates/paymentcard/build.gradle.kts +++ b/templates/paymentcard/build.gradle.kts @@ -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") @@ -10,5 +8,6 @@ android { namespace = "com.guru.composecookbook.paymentcard" } dependencies { implementation(project(":theme")) - addComposeOfficialDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) } diff --git a/templates/pinlock/build.gradle.kts b/templates/pinlock/build.gradle.kts index 2e336576..4d5d2919 100644 --- a/templates/pinlock/build.gradle.kts +++ b/templates/pinlock/build.gradle.kts @@ -1,7 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addBiometricDependency -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addComposeThirdPartyDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -16,7 +12,8 @@ dependencies { implementation(project(":components:tags")) implementation(project(":templates:onboarding")) - addComposeOfficialDependencies() - addComposeThirdPartyDependencies() - addBiometricDependency() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.compose.thirdparty) + implementation(libs.androidx.biometric) } diff --git a/templates/profile/build.gradle.kts b/templates/profile/build.gradle.kts index f40ac089..38571a2f 100644 --- a/templates/profile/build.gradle.kts +++ b/templates/profile/build.gradle.kts @@ -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") @@ -12,5 +10,6 @@ dependencies { implementation(project(":data")) implementation(project(":components:tags")) - addComposeOfficialDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) } diff --git a/theme/build.gradle.kts b/theme/build.gradle.kts index c25cff6a..12f96c61 100644 --- a/theme/build.gradle.kts +++ b/theme/build.gradle.kts @@ -1,7 +1,3 @@ -import com.guru.composecookbook.build.dependencies.addComposeOfficialDependencies -import com.guru.composecookbook.build.dependencies.addComposeThirdPartyDependencies -import com.guru.composecookbook.build.dependencies.addCoreAndroidUiDependencies - plugins { /** See [common-compose-module-configs-script-plugin.gradle.kts] file */ id("common-compose-module-configs-script-plugin") @@ -10,7 +6,8 @@ plugins { android { namespace = "com.guru.composecookbook.theme" } dependencies { - addComposeOfficialDependencies() - addComposeThirdPartyDependencies() - addCoreAndroidUiDependencies() + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.compose.official) + implementation(libs.bundles.compose.thirdparty) + implementation(libs.bundles.core.android.ui) }