├── .gitignore ├── .idea ├── .gitignore ├── androidTestResultsUserPreferences.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── compose │ │ └── practical │ │ ├── ExampleInstrumentedTest.kt │ │ ├── authentication │ │ ├── AuthenticationTest.kt │ │ ├── PasswordInputTest.kt │ │ └── PasswordRequirementsTest.kt │ │ ├── homeScreen │ │ ├── BottomNavigationTest.kt │ │ ├── ContentAreaTest.kt │ │ └── NavigationTest.kt │ │ └── onboardingScreen │ │ ├── OnBoardImageViewTest.kt │ │ ├── OnboardingScreenKtTest.kt │ │ └── TabSelectorTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── compose │ │ │ └── practical │ │ │ ├── MainActivity.kt │ │ │ └── ui │ │ │ ├── authentication │ │ │ ├── AuthenticationEvent.kt │ │ │ ├── AuthenticationScreen.kt │ │ │ ├── AuthenticationStateKit.kt │ │ │ ├── AuthenticationViewModel.kt │ │ │ └── Tags.kt │ │ │ ├── emailInbox │ │ │ ├── InboxScreen.kt │ │ │ ├── InboxViewModel.kt │ │ │ ├── model │ │ │ │ ├── Email.kt │ │ │ │ └── EmailFactory.kt │ │ │ └── state │ │ │ │ └── InboxState.kt │ │ │ ├── homeScreen │ │ │ ├── HomeScreen.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── Tags.kt │ │ │ ├── components │ │ │ │ ├── BottomNavigation.kt │ │ │ │ ├── Content.kt │ │ │ │ ├── Drawer.kt │ │ │ │ ├── Home.kt │ │ │ │ ├── Navigation.kt │ │ │ │ └── TopBar.kt │ │ │ └── model │ │ │ │ └── Destinations.kt │ │ │ ├── mutliSelectGrid │ │ │ └── GridComposables.kt │ │ │ ├── onboardingScreen │ │ │ └── OnboardingScreen.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── image1.jpg │ │ ├── image2.jpg │ │ └── image3.jpg │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── compose │ └── practical │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── runtimepermissions ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── compose │ │ └── runtimepermissions │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── compose │ │ │ └── runtimepermissions │ │ │ ├── MainActivity.kt │ │ │ ├── RuntimePermissionStates.kt │ │ │ └── ui │ │ │ ├── PermissionViewModel.kt │ │ │ ├── UiComponents.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── compose │ └── runtimepermissions │ └── ExampleUnitTest.kt ├── settings.gradle └── settings ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── compose │ └── settings │ ├── ExampleInstrumentedTest.kt │ ├── SettingsTest.kt │ └── settingFeatures │ ├── AppVersionSettingItemTest.kt │ ├── HintsSettingItemTest.kt │ ├── ManageSubscriptionSettingItemTest.kt │ ├── MarketingSettingItemTest.kt │ ├── NotificationSettingItemTest.kt │ └── ThemeSettingItemTest.kt ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── compose │ │ └── settings │ │ ├── MainSettingActivity.kt │ │ └── ui │ │ ├── SettingState.kt │ │ ├── Settings.kt │ │ ├── SettingsViewModel.kt │ │ ├── Tags.kt │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml └── test └── java └── com └── compose └── settings └── ExampleUnitTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jetpack Compose Practical for Real Time Scenarios 2 | 3 | ## Description 4 | 5 | The Jetpack Compose Practical is a sample application that demonstrates various practical scenarios using Jetpack Compose. It includes features such as an Authentication Screen, Settings Screen, Home Screen, Multigrid Selection using Android Mad Skill, runtime permission handling using Kotlin, Jetpack Compose, Material Design 3 components, and UI test cases using Mockito and Espresso. 6 | Many Screens are created by learnings from ## Special Thanks to @Joe Birch for Practical Jetpack Compose Book 7 | 8 | ## Screenshots 9 | 10 | | Authentication Screen | Settings Screen | Home Screen | Multigrid Selection | 11 | | --------------------- | --------------- | ----------- | ------------------- | 12 | | ![Authentication Screen](screenshots/authentication.png) | ![Settings Screen](screenshots/settings.png) | ![Home Screen](screenshots/home.png) | ![Multigrid Selection](screenshots/multigrid.png) | 13 | 14 | | Onboarding Screen | 15 | |--------------------------| --------------- | ----------- | ------------------- | 16 | | ![Onboard Screen](screenshots/onboarding.png) | 17 | ## Features 18 | 19 | The Jetpack Compose Playground includes the following features: 20 | 21 | - **Authentication Screen**: Allows users to authenticate using their credentials. 22 | - **Settings Screen**: Provides options for users to customize the application settings. 23 | - **Home Screen**: Displays relevant information and actions for the user. 24 | - **Multigrid Selection**: Utilizes the Android Mad Skill to implement a multi-grid selection feature. 25 | - **Runtime Permission Handling**: Demonstrates how to handle runtime permissions using Kotlin and Jetpack Compose. 26 | - **On-Boarding Screen**: Displays On-Boarding process for the user. Made using tabs layout compose. 27 | - **Material Design 3 Components**: Utilizes the latest Material Design 3 components to create a modern and visually appealing user interface. 28 | - **UI Test Cases**: Includes UI test cases using Mockito and Espresso to ensure the correctness and reliability of the app's components. 29 | 30 | ## Prerequisites 31 | 32 | To run the Jetpack Compose Playground, make sure you have the following installed: 33 | 34 | - Android Studio (Arctic Fox or higher) 35 | - Kotlin version 1.5.0 or higher 36 | 37 | ## Setup and Configuration 38 | 39 | To run the Jetpack Compose Playground on your local machine, follow these steps: 40 | 41 | 1. Clone the repository: `git clone https://github.com/manishkaushik900/PracticalCompose.git` 42 | 2. Open the project in Android Studio. 43 | 3. Build and run the app on an emulator or physical device. 44 | 45 | ## Testing 46 | 47 | The Jetpack Compose Playground includes UI test cases using Mockito and Espresso. To run the tests, follow these steps: 48 | 49 | 1. Open the project in Android Studio. 50 | 2. Right-click on the `androidTest` folder. 51 | 3. Select "Run Tests" to execute the UI test cases. 52 | 53 | ## Contributions 54 | 55 | Contributions to the Jetpack Compose Playground are welcome. If you have any ideas, bug fixes, or improvements, feel free to submit a pull request. 56 | 57 | ## Special Thanks to @Joe Birch for Practical Jetpack Compose Book 58 | 59 | 60 | `` 61 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.compose.practical' 8 | compileSdk 34 9 | 10 | defaultConfig { 11 | applicationId "com.compose.practical" 12 | minSdk 24 13 | targetSdk 34 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion '1.5.3' 41 | } 42 | packagingOptions { 43 | resources { 44 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'androidx.core:core-ktx:1.12.0' 52 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') 53 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' 54 | implementation 'androidx.activity:activity-compose:1.8.0' 55 | implementation platform('androidx.compose:compose-bom:2023.10.01') 56 | implementation 'androidx.compose.ui:ui' 57 | implementation 'androidx.compose.ui:ui-graphics' 58 | implementation 'androidx.compose.ui:ui-tooling-preview' 59 | implementation 'androidx.compose.material3:material3' 60 | 61 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2' 62 | implementation 'androidx.compose.material:material-icons-extended:1.5.4' 63 | implementation "androidx.compose.foundation:foundation" 64 | 65 | implementation 'androidx.navigation:navigation-compose:2.7.4' 66 | implementation "io.coil-kt:coil-compose:2.4.0" 67 | 68 | 69 | testImplementation 'junit:junit:4.13.2' 70 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 71 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 72 | androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01') 73 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4' 74 | debugImplementation 'androidx.compose.ui:ui-tooling' 75 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 76 | 77 | 78 | 79 | androidTestImplementation 'org.mockito.kotlin:mockito-kotlin:5.1.0' 80 | androidTestImplementation 'org.mockito:mockito-android:5.6.0' 81 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.compose.practical", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/authentication/AuthenticationTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.authentication 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.test.assertIsDisplayed 5 | import androidx.compose.ui.test.assertIsEnabled 6 | import androidx.compose.ui.test.assertIsNotEnabled 7 | import androidx.compose.ui.test.assertTextEquals 8 | import androidx.compose.ui.test.junit4.createComposeRule 9 | import androidx.compose.ui.test.onNodeWithTag 10 | import androidx.compose.ui.test.onNodeWithText 11 | import androidx.compose.ui.test.performClick 12 | import androidx.compose.ui.test.performTextClearance 13 | import androidx.compose.ui.test.performTextInput 14 | import androidx.test.platform.app.InstrumentationRegistry 15 | import com.compose.practical.R 16 | import com.compose.practical.ui.authentication.Authentication 17 | import com.compose.practical.ui.authentication.AuthenticationContent 18 | import com.compose.practical.ui.authentication.AuthenticationState 19 | import com.compose.practical.ui.authentication.Tags 20 | import com.compose.practical.ui.authentication.Tags.TAG_AUTHENTICATE_BUTTON 21 | import com.compose.practical.ui.authentication.Tags.TAG_CONTENT 22 | import com.compose.practical.ui.authentication.Tags.TAG_ERROR_ALERT 23 | import com.compose.practical.ui.authentication.Tags.TAG_INPUT_EMAIL 24 | import com.compose.practical.ui.authentication.Tags.TAG_INPUT_PASSWORD 25 | import com.compose.practical.ui.authentication.Tags.TAG_PROGRESS 26 | import org.junit.Rule 27 | import org.junit.Test 28 | 29 | class AuthenticationTest { 30 | 31 | @get:Rule 32 | val composeTestRule = createComposeRule() 33 | 34 | 35 | @Test 36 | fun Sign_In_Title_Is_Displayed_By_Default() { 37 | 38 | composeTestRule.setContent { 39 | Authentication() 40 | } 41 | 42 | composeTestRule.onNodeWithText( 43 | InstrumentationRegistry.getInstrumentation().targetContext.getString( 44 | R.string.label_sign_in_to_account 45 | ) 46 | ).assertIsDisplayed() 47 | 48 | } 49 | 50 | @Test 51 | fun Need_Account_Displayed_By_Default() { 52 | composeTestRule.setContent { 53 | Authentication() 54 | } 55 | composeTestRule 56 | .onNodeWithText( 57 | InstrumentationRegistry.getInstrumentation() 58 | .targetContext.getString( 59 | R.string.action_need_account 60 | ) 61 | ) 62 | .assertIsDisplayed() 63 | } 64 | 65 | @Test 66 | fun Sign_Up_Title_Is_Displayed_After_Toggled() { 67 | 68 | composeTestRule.setContent { 69 | Authentication() 70 | } 71 | 72 | composeTestRule 73 | .onNodeWithText( 74 | InstrumentationRegistry.getInstrumentation() 75 | .targetContext.getString( 76 | R.string.action_need_account 77 | ) 78 | ).performClick() 79 | 80 | composeTestRule.onNodeWithText( 81 | InstrumentationRegistry.getInstrumentation().targetContext.getString( 82 | R.string.label_sign_up_for_account 83 | ) 84 | ).assertIsDisplayed() 85 | 86 | } 87 | 88 | @Test 89 | fun Already_Account_Is_Displayed_After_Toggled() { 90 | 91 | composeTestRule.setContent { 92 | Authentication() 93 | } 94 | 95 | composeTestRule 96 | .onNodeWithTag( 97 | Tags.TAG_AUTHENTICATION_TOGGLE 98 | ).performClick() 99 | 100 | composeTestRule.onNodeWithText( 101 | InstrumentationRegistry.getInstrumentation().targetContext.getString( 102 | R.string.action_already_have_account 103 | ) 104 | ).assertIsDisplayed() 105 | 106 | } 107 | 108 | @Test 109 | fun Sign_Up_Button_Displayed_After_Toggle() { 110 | composeTestRule.setContent { 111 | Authentication() 112 | } 113 | composeTestRule 114 | .onNodeWithTag( 115 | Tags.TAG_AUTHENTICATION_TOGGLE 116 | ).performClick() 117 | 118 | composeTestRule.onNodeWithTag(Tags.TAG_AUTHENTICATE_BUTTON).assertTextEquals( 119 | InstrumentationRegistry.getInstrumentation().targetContext.getString( 120 | R.string.action_sign_up 121 | ) 122 | ) 123 | } 124 | 125 | @Test 126 | fun Authentication_Button_Disabled_By_Default() { 127 | composeTestRule.setContent { 128 | Authentication() 129 | } 130 | composeTestRule 131 | .onNodeWithTag(TAG_AUTHENTICATE_BUTTON) 132 | .assertIsNotEnabled() 133 | } 134 | 135 | @Test 136 | fun Authentication_Button_Enabled_With_Valid_Content() { 137 | composeTestRule.setContent { 138 | Authentication() 139 | } 140 | 141 | composeTestRule.onNodeWithTag(TAG_INPUT_EMAIL).performTextInput("contacr@manish.com") 142 | 143 | composeTestRule.onNodeWithTag(TAG_INPUT_PASSWORD).performTextInput("password") 144 | 145 | composeTestRule.onNodeWithTag( 146 | TAG_AUTHENTICATE_BUTTON 147 | ).assertIsEnabled() 148 | 149 | } 150 | 151 | 152 | @Test 153 | fun Authentication_Button_Enabled_Then_Disabled_With_Valid_Content() { 154 | composeTestRule.setContent { 155 | Authentication() 156 | } 157 | 158 | composeTestRule.onNodeWithTag(TAG_INPUT_EMAIL).performTextInput("contacr@manish.com") 159 | 160 | composeTestRule.onNodeWithTag(TAG_INPUT_PASSWORD).performTextInput("password") 161 | 162 | composeTestRule.onNodeWithTag( 163 | TAG_AUTHENTICATE_BUTTON 164 | ).assertIsEnabled() 165 | 166 | composeTestRule.onNodeWithTag(TAG_INPUT_EMAIL).performTextClearance() 167 | 168 | composeTestRule.onNodeWithTag(TAG_INPUT_PASSWORD).performTextClearance() 169 | 170 | composeTestRule 171 | .onNodeWithTag(TAG_AUTHENTICATE_BUTTON) 172 | .assertIsNotEnabled() 173 | 174 | } 175 | 176 | @Test 177 | fun Error_Alert_Not_Dispalyed_By_Default(){ 178 | composeTestRule.setContent { 179 | Authentication() 180 | } 181 | 182 | composeTestRule 183 | .onNodeWithTag(TAG_ERROR_ALERT).assertDoesNotExist() 184 | 185 | } 186 | 187 | @Test 188 | fun Error_Alert_Dispalyed_After_Error(){ 189 | composeTestRule.setContent { 190 | AuthenticationContent( 191 | modifier= Modifier, 192 | AuthenticationState( 193 | error = "Some error" 194 | ), 195 | handleEvent = {} 196 | ) 197 | } 198 | composeTestRule.onNodeWithTag( 199 | TAG_ERROR_ALERT 200 | ).assertIsDisplayed() 201 | 202 | } 203 | 204 | @Test 205 | fun Progress_Not_Displayed_By_Default() { 206 | composeTestRule.setContent { 207 | Authentication() 208 | } 209 | composeTestRule.onNodeWithTag( 210 | TAG_PROGRESS 211 | ).assertDoesNotExist() 212 | } 213 | 214 | @Test 215 | fun Progress_Displayed_While_Loading() { 216 | 217 | composeTestRule.setContent { 218 | AuthenticationContent( 219 | modifier= Modifier, 220 | AuthenticationState(isLoading = true), 221 | handleEvent = {} 222 | ) 223 | } 224 | 225 | composeTestRule.onNodeWithTag( 226 | TAG_PROGRESS 227 | ).assertIsDisplayed() 228 | } 229 | 230 | @Test 231 | fun Progress_Not_Displayed_After_Loading() { 232 | composeTestRule.setContent { 233 | AuthenticationContent( 234 | authenticationState = AuthenticationState( 235 | email = "contact@compose.academy", 236 | password = "password" 237 | ) 238 | ) { } 239 | } 240 | composeTestRule.onNodeWithTag( 241 | TAG_AUTHENTICATE_BUTTON 242 | ).performClick() 243 | 244 | composeTestRule.onNodeWithTag( 245 | TAG_PROGRESS 246 | ).assertDoesNotExist() 247 | } 248 | 249 | @Test 250 | fun Content_Displayed_After_Loading() { 251 | composeTestRule.setContent { 252 | Authentication() 253 | } 254 | composeTestRule.onNodeWithTag( 255 | TAG_INPUT_EMAIL 256 | ).performTextInput("contact@compose.academy") 257 | composeTestRule.onNodeWithTag( 258 | TAG_INPUT_PASSWORD 259 | ).performTextInput("password") 260 | composeTestRule.onNodeWithTag( 261 | TAG_AUTHENTICATE_BUTTON 262 | ).performClick() 263 | composeTestRule.onNodeWithTag( 264 | TAG_CONTENT, useUnmergedTree = true 265 | ).assertExists() 266 | } 267 | 268 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/authentication/PasswordInputTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.authentication 2 | 3 | import androidx.compose.ui.test.assertIsDisplayed 4 | import androidx.compose.ui.test.assertTextEquals 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onNodeWithTag 7 | import androidx.compose.ui.test.performClick 8 | import androidx.compose.ui.test.performTextInput 9 | import com.compose.practical.ui.authentication.PasswordInput 10 | import com.compose.practical.ui.authentication.Tags.TAG_INPUT_PASSWORD 11 | import com.compose.practical.ui.authentication.Tags.TAG_PASSWORD_HIDDEN 12 | import org.junit.Rule 13 | import org.junit.Test 14 | import org.mockito.kotlin.mock 15 | import org.mockito.kotlin.verify 16 | 17 | class PasswordInputTest { 18 | 19 | @get:Rule 20 | val composeTestRule = createComposeRule() 21 | 22 | 23 | @Test 24 | fun Password_Displayed() { 25 | val password = "password123" 26 | composeTestRule.setContent { 27 | PasswordInput( 28 | password = password, 29 | onPasswordChange = { }, 30 | onDoneClicked = { } 31 | ) 32 | } 33 | 34 | composeTestRule 35 | .onNodeWithTag(TAG_INPUT_PASSWORD) 36 | .assertTextEquals(password) 37 | } 38 | 39 | @Test 40 | fun Password_Changes_Triggered() { 41 | 42 | val onPasswordChanged: (password: String) -> Unit = mock() 43 | val password = "password123" 44 | composeTestRule.setContent { 45 | PasswordInput( 46 | password = password, 47 | onPasswordChange = onPasswordChanged, 48 | onDoneClicked = { } 49 | ) 50 | } 51 | 52 | val passwordText = "456" 53 | composeTestRule 54 | .onNodeWithTag(TAG_INPUT_PASSWORD) 55 | .performTextInput(passwordText) 56 | 57 | verify(onPasswordChanged).invoke(password + passwordText) 58 | } 59 | 60 | @Test 61 | fun Password_Toggled_Reflects_state() { 62 | composeTestRule.setContent { 63 | PasswordInput( 64 | password = "password123", 65 | onPasswordChange = { }, 66 | onDoneClicked = { } 67 | ) 68 | } 69 | 70 | composeTestRule 71 | .onNodeWithTag(TAG_PASSWORD_HIDDEN + "true") 72 | .performClick() 73 | 74 | composeTestRule 75 | .onNodeWithTag(TAG_PASSWORD_HIDDEN + "false") 76 | .assertIsDisplayed() 77 | 78 | } 79 | 80 | 81 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/authentication/PasswordRequirementsTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.authentication 2 | 3 | import androidx.compose.ui.test.assertIsDisplayed 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.compose.ui.test.onNodeWithText 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | import com.compose.practical.R 8 | import com.compose.practical.ui.authentication.PasswordRequirements 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | class PasswordRequirementsTest { 13 | 14 | @get:Rule 15 | val composeTestRule = createComposeRule() 16 | 17 | 18 | 19 | @Test 20 | fun Password_Requirement_Displayed_As_Not_Satisfied() { 21 | 22 | val requirements = PasswordRequirements.values().toList() 23 | val satisfiedRequirements = requirements[(0 until requirements.count()).random()] 24 | 25 | composeTestRule.setContent { 26 | PasswordRequirements(satisfiedRequirements = listOf(satisfiedRequirements)) 27 | } 28 | 29 | PasswordRequirements.values().forEach { requirement -> 30 | val requirement = 31 | InstrumentationRegistry.getInstrumentation() 32 | .targetContext.getString(requirement.label) 33 | val satisfyRequirement = 34 | InstrumentationRegistry.getInstrumentation() 35 | .targetContext.getString(satisfiedRequirements.label) 36 | 37 | val result = if (requirement == satisfyRequirement) { 38 | InstrumentationRegistry.getInstrumentation().targetContext.getString( 39 | R.string.password_requirement_satisfied,requirement 40 | ) 41 | } else { 42 | InstrumentationRegistry.getInstrumentation() 43 | .targetContext.getString( 44 | R.string.password_requirement_needed,requirement 45 | ) 46 | 47 | } 48 | 49 | composeTestRule 50 | .onNodeWithText(result) 51 | .assertIsDisplayed() 52 | } 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/homeScreen/BottomNavigationTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.homeScreen 2 | 3 | import androidx.compose.ui.test.assertIsDisplayed 4 | import androidx.compose.ui.test.assertIsNotSelected 5 | import androidx.compose.ui.test.assertIsSelected 6 | import androidx.compose.ui.test.junit4.createComposeRule 7 | import androidx.compose.ui.test.onNodeWithContentDescription 8 | import androidx.compose.ui.test.onNodeWithTag 9 | import androidx.compose.ui.test.onNodeWithText 10 | import androidx.compose.ui.test.performClick 11 | import androidx.test.platform.app.InstrumentationRegistry 12 | import com.compose.practical.R 13 | import com.compose.practical.ui.homeScreen.Tags.TAG_BOTTOM_NAVIGATION 14 | import com.compose.practical.ui.homeScreen.components.BottomNavigationBar 15 | import com.compose.practical.ui.homeScreen.model.Destinations 16 | import org.junit.Rule 17 | import org.junit.Test 18 | import org.mockito.Mockito.verify 19 | import org.mockito.kotlin.mock 20 | 21 | class BottomNavigationTest { 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule() 25 | 26 | @Test 27 | fun Bottom_Navigation_Displayed(){ 28 | 29 | composeTestRule.setContent { 30 | BottomNavigationBar(currentDestination = Destinations.Feed, onNavigate ={} , onFloatingBtnClick = {}) 31 | } 32 | 33 | composeTestRule.onNodeWithTag(TAG_BOTTOM_NAVIGATION).assertIsDisplayed() 34 | } 35 | 36 | @Test 37 | fun Bottom_Navigation_Items_Displayed(){ 38 | composeTestRule.setContent { 39 | BottomNavigationBar(currentDestination = Destinations.Feed, onNavigate ={} , onFloatingBtnClick = {}) 40 | } 41 | 42 | listOf( Destinations.Feed, 43 | Destinations.Contacts, 44 | Destinations.Calendar).forEach{ 45 | composeTestRule.onNodeWithText(it.title).assertIsDisplayed() 46 | } 47 | } 48 | 49 | @Test 50 | fun Bottom_Navigatiopn_onNavigate_Callback_Triggered(){ 51 | 52 | val destination = Destinations.Contacts 53 | val onNavigate: (destination:Destinations)-> Unit = mock() 54 | 55 | composeTestRule.setContent { 56 | BottomNavigationBar(currentDestination = Destinations.Feed, onNavigate =onNavigate , onFloatingBtnClick = {}) 57 | } 58 | 59 | composeTestRule.onNodeWithText(destination.title).performClick() 60 | 61 | verify(onNavigate).invoke(destination) 62 | 63 | } 64 | 65 | @Test 66 | fun Bottom_Navigatiopn_Create_Button_Displayed(){ 67 | composeTestRule.setContent { 68 | BottomNavigationBar(currentDestination = Destinations.Feed, onNavigate ={} , onFloatingBtnClick = {}) 69 | } 70 | 71 | composeTestRule.onNodeWithContentDescription( InstrumentationRegistry.getInstrumentation() 72 | .targetContext.getString( 73 | R.string.cd_create_item 74 | )).assertIsDisplayed() 75 | 76 | } 77 | 78 | @Test 79 | fun Bottom_Navigatiopn_onFloatingBtn_Callback_Triggered(){ 80 | 81 | val onFloatingBtn: ()-> Unit = mock() 82 | 83 | composeTestRule.setContent { 84 | BottomNavigationBar(currentDestination = Destinations.Feed, onNavigate ={} , onFloatingBtnClick = onFloatingBtn) 85 | } 86 | 87 | composeTestRule.onNodeWithContentDescription( InstrumentationRegistry.getInstrumentation() 88 | .targetContext.getString( 89 | R.string.cd_create_item 90 | )).performClick() 91 | 92 | verify(onFloatingBtn).invoke() 93 | 94 | } 95 | 96 | @Test 97 | fun Current_Destination_Selected(){ 98 | val destination = Destinations.Contacts 99 | composeTestRule.setContent { 100 | BottomNavigationBar(currentDestination = destination, onNavigate ={} , onFloatingBtnClick = {}) 101 | } 102 | 103 | composeTestRule.onNodeWithText(destination.title).assertIsSelected() 104 | composeTestRule.onNodeWithText(Destinations.Feed.title).assertIsNotSelected() 105 | composeTestRule.onNodeWithText(Destinations.Calendar.title).assertIsNotSelected() 106 | 107 | 108 | 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/homeScreen/ContentAreaTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.homeScreen 2 | 3 | import androidx.compose.ui.test.assert 4 | import androidx.compose.ui.test.assertContentDescriptionEquals 5 | import androidx.compose.ui.test.assertIsDisplayed 6 | import androidx.compose.ui.test.hasText 7 | import androidx.compose.ui.test.junit4.createComposeRule 8 | import androidx.compose.ui.test.onNodeWithTag 9 | import com.compose.practical.ui.homeScreen.Tags.TAG_CONTENT_ICON 10 | import com.compose.practical.ui.homeScreen.Tags.TAG_CONTENT_TITLE 11 | import com.compose.practical.ui.homeScreen.components.ContentArea 12 | import com.compose.practical.ui.homeScreen.model.Destinations 13 | import org.junit.Rule 14 | import org.junit.Test 15 | 16 | class ContentAreaTest { 17 | 18 | @get:Rule 19 | val composeTestRule = createComposeRule() 20 | 21 | @Test 22 | fun Destination_Displaye(){ 23 | 24 | val destination = Destinations.Feed 25 | 26 | composeTestRule.setContent { 27 | ContentArea(destinations = destination) 28 | } 29 | 30 | composeTestRule.onNodeWithTag(destination.path).assertIsDisplayed() 31 | 32 | } 33 | 34 | @Test 35 | fun Content_Title_Dispalyed(){ 36 | val destination = Destinations.Contacts 37 | 38 | composeTestRule.setContent { 39 | ContentArea(destinations = destination) 40 | } 41 | 42 | composeTestRule.onNodeWithTag(TAG_CONTENT_TITLE).assert(hasText(destination.title)) 43 | 44 | } 45 | 46 | @Test 47 | fun Content_Icon_Dispalyed(){ 48 | val destination = Destinations.Contacts 49 | 50 | composeTestRule.setContent { 51 | ContentArea(destinations = destination) 52 | } 53 | 54 | composeTestRule.onNodeWithTag(TAG_CONTENT_ICON).assertContentDescriptionEquals(destination.title) 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/homeScreen/NavigationTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.homeScreen 2 | 3 | import androidx.compose.ui.test.assertIsDisplayed 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.compose.ui.test.onNodeWithTag 6 | import androidx.navigation.compose.rememberNavController 7 | import com.compose.practical.ui.homeScreen.components.Navigation 8 | import com.compose.practical.ui.homeScreen.model.Destinations 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | class NavigationTest { 13 | 14 | @get:Rule 15 | val composeTestRule = createComposeRule() 16 | 17 | @Test 18 | fun Feed_Displayed_ByDefault(){ 19 | composeTestRule.setContent { 20 | Navigation(navController = rememberNavController()) 21 | } 22 | 23 | composeTestRule.onNodeWithTag(Destinations.Feed.path) 24 | .assertIsDisplayed() 25 | } 26 | 27 | @Test 28 | fun Contacts_Displayed(){ 29 | 30 | assertNavigation(Destinations.Contacts) 31 | 32 | } 33 | @Test 34 | fun Calendar_Displayed(){ 35 | assertNavigation(Destinations.Calendar) 36 | } 37 | 38 | @Test 39 | fun Create_Displayed(){ 40 | assertNavigation(Destinations.Add) 41 | 42 | } 43 | 44 | @Test 45 | fun Upgrade_Displayed(){ 46 | assertNavigation(Destinations.Upgrade) 47 | } 48 | 49 | @Test 50 | fun Settings_Displayed(){ 51 | assertNavigation(Destinations.Settings) 52 | } 53 | 54 | private fun assertNavigation(destinations: Destinations){ 55 | 56 | composeTestRule.setContent { 57 | val navController = rememberNavController() 58 | Navigation(navController = navController) 59 | navController.navigate(destinations.path) 60 | 61 | } 62 | composeTestRule.onNodeWithTag(destinations.path).assertIsDisplayed() 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/onboardingScreen/OnBoardImageViewTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.onboardingScreen 2 | 3 | import androidx.compose.ui.test.assertIsDisplayed 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.compose.ui.test.onNodeWithTag 6 | import com.compose.practical.ui.onboardingScreen.OnBoardImageView 7 | import com.compose.practical.ui.onboardingScreen.Tags 8 | import com.compose.practical.ui.onboardingScreen.onboardPagesList 9 | import org.junit.Rule 10 | import org.junit.Test 11 | 12 | class OnBoardImageViewTest { 13 | 14 | @get:Rule 15 | val composeTestRule = createComposeRule() 16 | 17 | @Test 18 | fun correctImageDisplayed() { 19 | val currentPage = onboardPagesList[0] 20 | 21 | composeTestRule.setContent { 22 | OnBoardImageView( 23 | currentPage = currentPage 24 | ) 25 | } 26 | 27 | // Verify that the correct image is displayed 28 | composeTestRule.onNodeWithTag(Tags.TAG_ONBOARD_SCREEN_IMAGE_VIEW+currentPage.title).assertIsDisplayed() 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/onboardingScreen/OnboardingScreenKtTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.onboardingScreen 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Surface 5 | import androidx.compose.ui.test.assertIsDisplayed 6 | import androidx.compose.ui.test.junit4.createComposeRule 7 | import androidx.compose.ui.test.onNodeWithTag 8 | import androidx.compose.ui.test.onNodeWithText 9 | import androidx.compose.ui.test.performClick 10 | import com.compose.practical.ui.onboardingScreen.OnBoardNavButton 11 | import com.compose.practical.ui.onboardingScreen.OnboardScreen 12 | import com.compose.practical.ui.onboardingScreen.Tags 13 | import com.compose.practical.ui.onboardingScreen.Tags.TAG_ONBOARD_SCREEN_NAV_BUTTON 14 | import com.compose.practical.ui.onboardingScreen.onboardPagesList 15 | import org.junit.Rule 16 | import org.junit.Test 17 | import org.mockito.kotlin.mock 18 | import org.mockito.kotlin.never 19 | import org.mockito.kotlin.verify 20 | 21 | class OnboardingScreenKtTest { 22 | 23 | @get:Rule 24 | val composeTestRule = createComposeRule() 25 | 26 | 27 | @Test 28 | fun assert_onboardScreen_DisplayedCorrectly(){ 29 | composeTestRule.setContent { 30 | MaterialTheme{ 31 | Surface { 32 | OnboardScreen() 33 | } 34 | } 35 | } 36 | 37 | composeTestRule.onNodeWithTag( 38 | Tags.TAG_ONBOARD_SCREEN 39 | ).assertIsDisplayed() 40 | 41 | 42 | } 43 | 44 | @Test 45 | fun asser_ONBOARD_SCREEN_IMAGEVIEW_Displayed_Correctly(){ 46 | composeTestRule.setContent { 47 | MaterialTheme{ 48 | Surface { 49 | OnboardScreen() 50 | } 51 | } 52 | } 53 | 54 | composeTestRule.onNodeWithTag( 55 | Tags.TAG_ONBOARD_SCREEN_IMAGE_VIEW 56 | ).assertIsDisplayed() 57 | } 58 | 59 | @Test 60 | fun assert_ONBOARD_SCREEN_Details_Displayed_Correctly(){ 61 | composeTestRule.setContent { 62 | MaterialTheme{ 63 | Surface { 64 | OnboardScreen() 65 | } 66 | } 67 | } 68 | 69 | composeTestRule.onNodeWithText( 70 | onboardPagesList[0].title 71 | ).assertIsDisplayed() 72 | 73 | composeTestRule.onNodeWithText( 74 | onboardPagesList[0].description 75 | ).assertIsDisplayed() 76 | } 77 | 78 | @Test 79 | fun onNextButtonClicked_CallbackTriggered(){ 80 | val onNextButtonClick: ()-> Unit = mock() 81 | 82 | composeTestRule.setContent { 83 | OnBoardNavButton( 84 | currentPage = 0, 85 | noOfPages = 3, 86 | onNextClicked = onNextButtonClick 87 | ) 88 | } 89 | 90 | composeTestRule.onNodeWithTag(TAG_ONBOARD_SCREEN_NAV_BUTTON).performClick() 91 | 92 | verify(onNextButtonClick).invoke() 93 | 94 | } 95 | 96 | @Test 97 | fun onNextButtonClicked_LastPageHandledProperly(){ 98 | val onNextButtonClick: ()-> Unit = mock() 99 | 100 | composeTestRule.setContent { 101 | OnBoardNavButton( 102 | currentPage = 2, 103 | noOfPages = 3, 104 | onNextClicked = onNextButtonClick 105 | ) 106 | } 107 | 108 | composeTestRule.onNodeWithText("Get Started").assertIsDisplayed() 109 | 110 | composeTestRule.onNodeWithTag(TAG_ONBOARD_SCREEN_NAV_BUTTON).performClick() 111 | 112 | verify(onNextButtonClick, never()).invoke() 113 | 114 | } 115 | 116 | 117 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/compose/practical/onboardingScreen/TabSelectorTest.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.onboardingScreen 2 | 3 | import androidx.compose.ui.test.assertCountEquals 4 | import androidx.compose.ui.test.assertIsSelected 5 | import androidx.compose.ui.test.junit4.createComposeRule 6 | import androidx.compose.ui.test.onChildren 7 | import androidx.compose.ui.test.onNodeWithTag 8 | import androidx.compose.ui.test.performClick 9 | import androidx.test.ext.junit.runners.AndroidJUnit4 10 | import com.compose.practical.ui.onboardingScreen.TabSelector 11 | import com.compose.practical.ui.onboardingScreen.Tags.TAG_ONBOARD_TAG_ROW 12 | import com.compose.practical.ui.onboardingScreen.onboardPagesList 13 | import org.junit.Assert.assertEquals 14 | import org.junit.Rule 15 | import org.junit.Test 16 | import org.junit.runner.RunWith 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | class TabSelectorTest { 20 | 21 | @get:Rule 22 | val composeTestRule = createComposeRule() 23 | 24 | @Test 25 | fun correctNumberOfTabsDisplayed() { 26 | val currentPage = 0 27 | composeTestRule.setContent { 28 | TabSelector( 29 | onboardPages = onboardPagesList, 30 | currentPage = currentPage, 31 | onTabSelected = {} 32 | ) 33 | } 34 | 35 | // Verify that the correct number of tabs are displayed 36 | composeTestRule.onNodeWithTag(TAG_ONBOARD_TAG_ROW) 37 | .onChildren() 38 | .assertCountEquals(onboardPagesList.size) 39 | } 40 | 41 | @Test 42 | fun validateSelectedTabBasedOnCurrentPage() { 43 | val currentPage = 2 44 | val onTabSelected: (Int) -> Unit = {} 45 | 46 | composeTestRule.setContent { 47 | TabSelector( 48 | onboardPages = onboardPagesList, 49 | currentPage = currentPage, 50 | onTabSelected = onTabSelected 51 | ) 52 | } 53 | 54 | composeTestRule.onNodeWithTag(TAG_ONBOARD_TAG_ROW).onChildren()[currentPage].assertIsSelected() 55 | } 56 | 57 | @Test 58 | fun tabSelection_onTabSelectedCallbackTriggered() { 59 | var selectedTabIndex = -1 60 | composeTestRule.setContent { 61 | 62 | TabSelector( 63 | onboardPages = onboardPagesList, 64 | currentPage = 0, 65 | onTabSelected = { selectedTabIndex = it } 66 | ) 67 | 68 | } 69 | 70 | // Click on the second tab 71 | composeTestRule.onNodeWithTag(TAG_ONBOARD_TAG_ROW).onChildren()[0].assertIsSelected() 72 | composeTestRule.onNodeWithTag(TAG_ONBOARD_TAG_ROW).onChildren()[1].performClick() 73 | 74 | // Verify that the onTabSelected callback is triggered with the correct index 75 | assertEquals(1, selectedTabIndex) 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import com.compose.practical.ui.emailInbox.Inbox 9 | 10 | class MainActivity : ComponentActivity() { 11 | @OptIn(ExperimentalMaterial3Api::class) 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContent { 15 | MaterialTheme { 16 | // Home() 17 | // Authentication() 18 | // PhotosGrid() 19 | 20 | Inbox() 21 | } 22 | 23 | } 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/authentication/AuthenticationEvent.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.authentication 2 | 3 | sealed class AuthenticationEvent { 4 | object ToggleAuthenticationMode : AuthenticationEvent() 5 | 6 | class EmailChanged(val emailAddress: String) : 7 | AuthenticationEvent() 8 | 9 | class PasswordChanged(val password: String) : 10 | AuthenticationEvent() 11 | 12 | object Authenticate:AuthenticationEvent() 13 | 14 | object ErrorDismissed: AuthenticationEvent() 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/authentication/AuthenticationStateKit.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.authentication 2 | 3 | import androidx.annotation.StringRes 4 | import com.compose.practical.R 5 | 6 | data class AuthenticationState( 7 | val authenticationMode: AuthenticationMode = AuthenticationMode.SIGN_IN, 8 | val email: String? = null, 9 | val password: String? = null, 10 | val passwordRequirements: List = emptyList(), 11 | val isLoading: Boolean = false, 12 | val error: String? = null 13 | 14 | ) { 15 | fun isFormValid(): Boolean { 16 | return password?.isNotEmpty() == true 17 | && email?.isNotEmpty() == true 18 | && (authenticationMode == AuthenticationMode.SIGN_IN 19 | || passwordRequirements.containsAll( 20 | PasswordRequirements.values().toList() 21 | ) 22 | ) 23 | } 24 | } 25 | 26 | enum class PasswordRequirements(@StringRes val label: Int) { 27 | 28 | CAPITAL_LETTER(R.string.password_requirement_capital), NUMBER(R.string.password_requirement_digit), EIGHT_CHARACTERS( 29 | R.string.password_requirement_characters 30 | ) 31 | } 32 | 33 | enum class AuthenticationMode { 34 | SIGN_UP, SIGN_IN 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/authentication/AuthenticationViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.authentication 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.withContext 10 | 11 | class AuthenticationViewModel : ViewModel() { 12 | 13 | val uiState = MutableStateFlow(AuthenticationState()) 14 | 15 | private fun toggleAuthenticationMode() { 16 | val authenticationMode = uiState.value.authenticationMode 17 | val newAuthenticationMode = if ( 18 | authenticationMode == AuthenticationMode.SIGN_IN 19 | ) { 20 | AuthenticationMode.SIGN_UP 21 | } else { 22 | AuthenticationMode.SIGN_IN 23 | } 24 | uiState.value = uiState.value.copy( 25 | authenticationMode = newAuthenticationMode 26 | ) 27 | } 28 | 29 | private fun updateEmail(email: String) { 30 | uiState.value = uiState.value.copy( 31 | email = email 32 | ) 33 | } 34 | 35 | private fun updatePassword(password: String) { 36 | val requirements = mutableListOf() 37 | if (password.length > 7) { 38 | requirements.add(PasswordRequirements.EIGHT_CHARACTERS) 39 | } 40 | if (password.any { it.isUpperCase() }) { 41 | requirements.add(PasswordRequirements.CAPITAL_LETTER) 42 | } 43 | if (password.any { it.isDigit() }) { 44 | requirements.add(PasswordRequirements.NUMBER) 45 | } 46 | uiState.value = uiState.value.copy( 47 | password = password, 48 | passwordRequirements = requirements.toList() 49 | ) 50 | } 51 | 52 | private fun authenticate() { 53 | uiState.value = uiState.value.copy( 54 | isLoading = true 55 | ) 56 | // trigger network request 57 | 58 | viewModelScope.launch(Dispatchers.IO) { 59 | delay(2000L) 60 | 61 | withContext(Dispatchers.Main){ 62 | uiState.value = uiState.value.copy( 63 | isLoading = false, 64 | error = "Something Went Wrong" 65 | ) 66 | } 67 | } 68 | 69 | } 70 | 71 | private fun dismissError() { 72 | uiState.value = uiState.value.copy( 73 | error = null 74 | ) 75 | } 76 | 77 | fun handleEvent(authenticationEvent: AuthenticationEvent) { 78 | when (authenticationEvent) { 79 | is AuthenticationEvent.ToggleAuthenticationMode -> { 80 | toggleAuthenticationMode() 81 | } 82 | 83 | is AuthenticationEvent.EmailChanged -> { 84 | updateEmail(authenticationEvent.emailAddress) 85 | } 86 | is AuthenticationEvent.PasswordChanged -> { 87 | updatePassword(authenticationEvent.password) 88 | } 89 | 90 | is AuthenticationEvent.Authenticate -> { 91 | authenticate() 92 | } 93 | 94 | is AuthenticationEvent.ErrorDismissed -> { 95 | dismissError() 96 | } 97 | } 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/authentication/Tags.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.authentication 2 | 3 | object Tags { 4 | const val TAG_AUTHENTICATE_BUTTON = "authenticate_button" 5 | const val TAG_AUTHENTICATION_TOGGLE = "authentication_mode_toggle" 6 | 7 | const val TAG_INPUT_EMAIL = "input_email" 8 | const val TAG_INPUT_PASSWORD = "input_password" 9 | const val TAG_ERROR_ALERT = "error_alert" 10 | const val TAG_PROGRESS = "progress" 11 | const val TAG_CONTENT = "content" 12 | const val TAG_PASSWORD_HIDDEN = "password_hidden_" 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/emailInbox/InboxScreen.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.emailInbox 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.animateColorAsState 5 | import androidx.compose.animation.core.spring 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.lazy.LazyColumn 16 | import androidx.compose.foundation.lazy.items 17 | import androidx.compose.material.icons.Icons 18 | import androidx.compose.material.icons.filled.Delete 19 | import androidx.compose.material3.Button 20 | import androidx.compose.material3.Card 21 | import androidx.compose.material3.CircularProgressIndicator 22 | import androidx.compose.material3.DismissDirection 23 | import androidx.compose.material3.DismissValue 24 | import androidx.compose.material3.ExperimentalMaterial3Api 25 | import androidx.compose.material3.Icon 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.material3.Scaffold 28 | import androidx.compose.material3.SwipeToDismiss 29 | import androidx.compose.material3.Text 30 | import androidx.compose.material3.TopAppBar 31 | import androidx.compose.material3.rememberDismissState 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.runtime.LaunchedEffect 34 | import androidx.compose.runtime.collectAsState 35 | import androidx.compose.runtime.getValue 36 | import androidx.compose.runtime.mutableStateOf 37 | import androidx.compose.runtime.remember 38 | import androidx.compose.runtime.setValue 39 | import androidx.compose.ui.Alignment 40 | import androidx.compose.ui.Modifier 41 | import androidx.compose.ui.graphics.Color 42 | import androidx.compose.ui.res.stringResource 43 | import androidx.compose.ui.text.font.FontWeight 44 | import androidx.compose.ui.text.style.TextAlign 45 | import androidx.compose.ui.text.style.TextOverflow 46 | import androidx.compose.ui.tooling.preview.Preview 47 | import androidx.compose.ui.unit.dp 48 | import androidx.compose.ui.unit.sp 49 | import androidx.lifecycle.viewmodel.compose.viewModel 50 | import com.compose.practical.R 51 | import com.compose.practical.ui.emailInbox.model.Email 52 | import com.compose.practical.ui.emailInbox.state.InboxState 53 | import com.compose.practical.ui.emailInbox.state.InboxStatus 54 | 55 | 56 | @Composable 57 | fun Inbox() { 58 | 59 | val viewModel: InboxViewModel = viewModel() 60 | 61 | 62 | MaterialTheme { 63 | EmailInbox( 64 | modifier = Modifier.fillMaxWidth(), 65 | inboxState = viewModel.uiState.collectAsState().value, 66 | inboxEventListener = viewModel::handleEvent 67 | ) 68 | 69 | } 70 | 71 | LaunchedEffect(Unit) { 72 | viewModel.loadContent() 73 | } 74 | 75 | } 76 | 77 | @OptIn(ExperimentalMaterial3Api::class) 78 | @Composable 79 | fun EmailInbox( 80 | modifier: Modifier = Modifier, 81 | inboxState: InboxState, 82 | inboxEventListener: (event: InboxEvent) -> Unit 83 | 84 | ) { 85 | //this is manish 86 | Scaffold(modifier = modifier, 87 | topBar = { 88 | TopAppBar( 89 | title = { 90 | Text( 91 | modifier = Modifier 92 | .fillMaxWidth() 93 | .padding(top = 8.dp), 94 | fontWeight = FontWeight.Bold, 95 | textAlign = TextAlign.Center, 96 | text = stringResource(id = R.string.title_inbox, inboxState.content?.size?:"0") 97 | ) 98 | } 99 | ) 100 | } 101 | ) { 102 | 103 | Box( 104 | modifier = Modifier 105 | .padding(it) 106 | .fillMaxSize(), 107 | contentAlignment = Alignment.Center 108 | ) { 109 | 110 | if (inboxState.status == InboxStatus.LOADING) { 111 | Loading() 112 | } else if (inboxState.status == InboxStatus.ERROR) { 113 | ErrorState { 114 | inboxEventListener(InboxEvent.RefreshContent) 115 | } 116 | } else if (!inboxState.content.isNullOrEmpty()) { 117 | EmailList(emails = inboxState.content) 118 | } else { 119 | EmptyState { 120 | inboxEventListener(InboxEvent.RefreshContent) 121 | } 122 | } 123 | } 124 | 125 | } 126 | 127 | } 128 | 129 | @OptIn(ExperimentalMaterial3Api::class) 130 | @Composable 131 | fun EmailList( 132 | modifier: Modifier = Modifier, 133 | emails: List 134 | ) { 135 | var isEmailItemDismissed by remember { mutableStateOf(false) } 136 | var show by remember { mutableStateOf(true) } 137 | val dismissState = rememberDismissState( 138 | confirmValueChange = { 139 | if (it == DismissValue.DismissedToEnd) { 140 | show = false 141 | isEmailItemDismissed = true 142 | true 143 | } else false 144 | }, positionalThreshold = { 150.dp.toPx() } 145 | ) 146 | 147 | LazyColumn( 148 | modifier = modifier 149 | ) { 150 | items(emails, key = { item -> item.id }) { email: Email -> 151 | 152 | AnimatedVisibility( 153 | show, exit = fadeOut(spring()) 154 | ) { 155 | SwipeToDismiss( 156 | state = dismissState, background = { 157 | val color by animateColorAsState( 158 | when (dismissState.targetValue) { 159 | DismissValue.Default -> Color.LightGray 160 | DismissValue.DismissedToEnd -> Color.Green 161 | DismissValue.DismissedToStart -> Color.Red 162 | }, label = "Delete" 163 | ) 164 | Box( 165 | Modifier 166 | .fillMaxSize() 167 | .background(color) 168 | ) { 169 | Icon( 170 | modifier = Modifier.align(Alignment.CenterStart), 171 | imageVector = Icons.Default.Delete, 172 | contentDescription = stringResource( 173 | id = 174 | R.string.cd_delete_email 175 | ) 176 | ) 177 | 178 | } 179 | }, 180 | directions = setOf( 181 | DismissDirection.StartToEnd 182 | ), 183 | dismissContent = { 184 | EmailItem( 185 | email = email 186 | ) 187 | } 188 | ) 189 | } 190 | 191 | } 192 | } 193 | } 194 | 195 | 196 | @Composable 197 | fun EmailItem( 198 | modifier: Modifier = Modifier, 199 | email: Email 200 | ) { 201 | Card( 202 | modifier = modifier.padding(16.dp) 203 | ) { 204 | Column( 205 | modifier = Modifier 206 | .padding(16.dp) 207 | .fillMaxWidth() 208 | ) { 209 | Text( 210 | text = email.title, 211 | fontWeight = FontWeight.Bold 212 | ) 213 | 214 | Spacer(modifier = Modifier.height(8.dp)) 215 | 216 | Text( 217 | text = email.description, 218 | fontSize = 14.sp, 219 | maxLines = 2, 220 | overflow = TextOverflow.Ellipsis 221 | ) 222 | } 223 | } 224 | 225 | } 226 | 227 | @Composable 228 | fun EmptyState( 229 | modifier: Modifier = Modifier, 230 | inboxEventListener: (inboxEvent: InboxEvent) -> Unit 231 | ) { 232 | Column( 233 | modifier = modifier.padding(16.dp), 234 | horizontalAlignment = Alignment.CenterHorizontally 235 | ) { 236 | Text( 237 | text = stringResource( 238 | id = 239 | R.string.message_empty_content 240 | ) 241 | ) 242 | Spacer(modifier = Modifier.height(12.dp)) 243 | Button(onClick = { 244 | inboxEventListener(InboxEvent.RefreshContent) 245 | }) { 246 | Text( 247 | text = stringResource( 248 | id = 249 | R.string.label_check_again 250 | ) 251 | ) 252 | } 253 | } 254 | } 255 | 256 | @Composable 257 | fun ErrorState( 258 | modifier: Modifier = Modifier, 259 | inboxEventListener: (inboxEvent: InboxEvent) -> Unit 260 | ) { 261 | Column( 262 | modifier = modifier, 263 | horizontalAlignment = Alignment.CenterHorizontally 264 | ) { 265 | Text( 266 | text = stringResource( 267 | id = R.string.message_content_error 268 | ) 269 | ) 270 | 271 | Spacer(modifier = Modifier.height(12.dp)) 272 | Button(onClick = { 273 | inboxEventListener(InboxEvent.RefreshContent) 274 | }) { 275 | Text( 276 | text = stringResource( 277 | id = R.string.label_try_again 278 | ) 279 | ) 280 | } 281 | } 282 | } 283 | 284 | @Composable 285 | fun Loading() { 286 | 287 | CircularProgressIndicator() 288 | } 289 | 290 | @OptIn(ExperimentalMaterial3Api::class) 291 | @Preview(showBackground = false) 292 | @Composable 293 | fun EmailInboxPreview() { 294 | Inbox() 295 | 296 | } 297 | -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/emailInbox/InboxViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.emailInbox 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.compose.practical.ui.emailInbox.model.EmailFactory 5 | import com.compose.practical.ui.emailInbox.state.InboxState 6 | import com.compose.practical.ui.emailInbox.state.InboxStatus 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | 9 | class InboxViewModel : ViewModel() { 10 | 11 | val uiState = MutableStateFlow(InboxState()) 12 | 13 | fun loadContent() { 14 | uiState.value = uiState.value.copy( 15 | status = InboxStatus.LOADING 16 | ) 17 | 18 | uiState.value = uiState.value.copy( 19 | status = InboxStatus.SUCCESS, 20 | content = EmailFactory.makeEmailList() 21 | ) 22 | } 23 | 24 | private fun deleteEmail(id: String) { 25 | uiState.value = uiState.value.copy( 26 | content = uiState.value.content?.filter { 27 | it.id != id 28 | } 29 | ) 30 | } 31 | 32 | fun handleEvent(inboxEvent: InboxEvent) { 33 | when (inboxEvent) { 34 | InboxEvent.RefreshContent -> { 35 | loadContent() 36 | } 37 | 38 | is InboxEvent.DeleteEmail -> { 39 | deleteEmail(inboxEvent.id) 40 | } 41 | 42 | is InboxEvent.NewEmail -> { 43 | loadContent() 44 | } 45 | } 46 | } 47 | } 48 | 49 | sealed class InboxEvent { 50 | object RefreshContent : InboxEvent() 51 | class DeleteEmail(val id: String) : InboxEvent() 52 | 53 | class NewEmail(val id: String) : InboxEvent() 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/emailInbox/model/Email.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.emailInbox.model 2 | 3 | data class Email( 4 | val id: String, 5 | val title: String, 6 | val description: String 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/emailInbox/model/EmailFactory.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.emailInbox.model 2 | 3 | import com.compose.practical.ui.emailInbox.model.Email 4 | 5 | object EmailFactory { 6 | fun makeEmailList(): List { 7 | return listOf( 8 | Email( 9 | "1", 10 | "Did you get my email?", 11 | "Hey, I just wanted to check that you got my last email " + 12 | "- I know you're pretty busy these days!" 13 | ), 14 | Email( 15 | "2", 16 | "Welcome!", 17 | "Thanks for signing up to our mailing list. " + 18 | "You\'ll need to confirm your email address to receive future emails from us." 19 | ), 20 | Email( 21 | "3", 22 | "Thanks for your order!", 23 | "Your order is on its way! Keep an eye on its progress using our tracking system." 24 | ), 25 | Email( 26 | "4", 27 | "Join our team? :)", 28 | "Thanks for spending the time to interview with us over the last few weeks - we'd love to invite you to join our team!" 29 | ), 30 | Email( 31 | "5", 32 | "RE: Coffee", 33 | "Was great to bump into your last week - I'd love to catch-up properly, maybe we could meet at the weekend? There's a lovely new coffee bar on my street!" 34 | ), 35 | Email( 36 | "6", 37 | "You didn't win this time", 38 | "Thanks for entering our competition. Unfortunately you didn't win this time! Please try again soon, you might have better luck in future :)" 39 | ) 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/emailInbox/state/InboxState.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.emailInbox.state 2 | 3 | import com.compose.practical.ui.emailInbox.model.Email 4 | 5 | enum class InboxStatus { 6 | LOADING, HAS_EMAILS, ERROR, EMPTY, SUCCESS 7 | } 8 | 9 | data class InboxState( 10 | val status: InboxStatus = InboxStatus.LOADING, 11 | val content: List? = null 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package com.compose.practical.ui.homeScreen 4 | 5 | import androidx.compose.material3.DrawerState 6 | import androidx.compose.material3.DrawerValue 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.ModalNavigationDrawer 10 | import androidx.compose.material3.rememberDrawerState 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.derivedStateOf 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.rememberCoroutineScope 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.navigation.compose.currentBackStackEntryAsState 19 | import androidx.navigation.compose.rememberNavController 20 | import com.compose.practical.ui.homeScreen.components.DrawerContent 21 | import com.compose.practical.ui.homeScreen.components.HomeContent 22 | import com.compose.practical.ui.homeScreen.model.Destinations 23 | import kotlinx.coroutines.launch 24 | 25 | @OptIn(ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun Home( 28 | modifier: Modifier = Modifier, 29 | drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed) 30 | ) { 31 | val navController = rememberNavController() 32 | val scope = rememberCoroutineScope() 33 | val navBackStackEntry = navController.currentBackStackEntryAsState() 34 | val currentDestination by remember(navBackStackEntry) { 35 | derivedStateOf { 36 | navBackStackEntry.value?.destination?.route?.let { 37 | Destinations.fromString(it) 38 | } ?: run { 39 | Destinations.Home 40 | } 41 | } 42 | } 43 | 44 | ModalNavigationDrawer( 45 | modifier = modifier, 46 | drawerState = drawerState, 47 | drawerContent = { 48 | DrawerContent( 49 | modifier = Modifier, 50 | onLogout = { 51 | scope.launch { drawerState.close() } 52 | }, 53 | onNavigationSelected = { 54 | scope.launch { drawerState.close() } 55 | navController.navigate(it.path) 56 | }, 57 | selectedItem = currentDestination 58 | ) 59 | }, 60 | content = { 61 | HomeContent( 62 | drawerState = drawerState, 63 | navController = navController, 64 | currentDestination = currentDestination 65 | ) 66 | } 67 | ) 68 | } 69 | 70 | @OptIn(ExperimentalMaterial3Api::class) 71 | @Preview(showBackground = false) 72 | @Composable 73 | fun HomeScreenPreview() { 74 | MaterialTheme { 75 | Home() 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class HomeViewModel: ViewModel() { 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/Tags.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen 2 | 3 | object Tags { 4 | const val TAG_ROOT_TOP_BAR = "root_top_bar" 5 | const val TAG_CHILD_TOP_BAR = "child_top_bar" 6 | const val TAG_BOTTOM_NAVIGATION = "bottom_navigation" 7 | const val TAG_RAIL_NAVIGATION = "rail_navigation" 8 | const val TAG_RAIL_CREATE = "rail_create" 9 | const val TAG_CONTENT_TITLE = "content_title" 10 | const val TAG_CONTENT_ICON = "content_icon" 11 | const val TAG_FLOATING_BUTTON = "floating_button" 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/components/BottomNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Add 5 | import androidx.compose.material3.BottomAppBar 6 | import androidx.compose.material3.BottomAppBarDefaults 7 | import androidx.compose.material3.FloatingActionButton 8 | import androidx.compose.material3.FloatingActionButtonDefaults 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.NavigationBarItem 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.testTag 15 | import androidx.compose.ui.res.stringResource 16 | import com.compose.practical.R 17 | import com.compose.practical.ui.homeScreen.Tags.TAG_BOTTOM_NAVIGATION 18 | import com.compose.practical.ui.homeScreen.model.Destinations 19 | 20 | @Composable 21 | fun BottomNavigationBar( 22 | modifier: Modifier = Modifier, 23 | currentDestination: Destinations, 24 | onNavigate: (destination: Destinations) -> Unit, 25 | onFloatingBtnClick: () -> Unit 26 | ) { 27 | BottomAppBar( 28 | modifier = modifier.testTag(TAG_BOTTOM_NAVIGATION), 29 | actions = { 30 | listOf( 31 | Destinations.Feed, 32 | Destinations.Contacts, 33 | Destinations.Calendar 34 | ).forEach { 35 | 36 | NavigationBarItem( 37 | selected = currentDestination.path == it.path, 38 | icon = { 39 | Icon( 40 | imageVector = it.icon!!, 41 | contentDescription = it.path, 42 | ) 43 | }, 44 | onClick = { onNavigate(it) }, 45 | label = { Text(text = it.title) }) 46 | 47 | } 48 | 49 | }, 50 | floatingActionButton = { 51 | FloatingActionButton( 52 | onClick = { onFloatingBtnClick() }, 53 | containerColor = BottomAppBarDefaults.bottomAppBarFabColor, 54 | elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation() 55 | ) { 56 | Icon(imageVector = Icons.Filled.Add, contentDescription = stringResource(R.string.cd_create_item)) 57 | } 58 | } 59 | ) 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/components/Content.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.platform.testTag 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import com.compose.practical.ui.homeScreen.Tags.TAG_CONTENT_ICON 17 | import com.compose.practical.ui.homeScreen.Tags.TAG_CONTENT_TITLE 18 | import com.compose.practical.ui.homeScreen.model.Destinations 19 | 20 | @Composable 21 | fun ContentArea( 22 | modifier: Modifier = Modifier, 23 | destinations: Destinations 24 | ) { 25 | Column( 26 | modifier = modifier.testTag(destinations.path), 27 | verticalArrangement = Arrangement.Center, 28 | horizontalAlignment = Alignment.CenterHorizontally 29 | ) { 30 | 31 | destinations.icon?.let { icon -> 32 | Icon( 33 | modifier = Modifier.size(80.dp).testTag(TAG_CONTENT_ICON), 34 | imageVector = icon, 35 | contentDescription = destinations.title 36 | ) 37 | Spacer(modifier = Modifier.height(16.dp)) 38 | } 39 | Text( modifier = Modifier.testTag(TAG_CONTENT_TITLE),text = destinations.title, fontSize = 18.sp) 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/components/Drawer.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen.components 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Logout 9 | import androidx.compose.material3.Divider 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.ModalDrawerSheet 13 | import androidx.compose.material3.NavigationDrawerItem 14 | import androidx.compose.material3.NavigationDrawerItemDefaults 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import com.compose.practical.R 22 | import com.compose.practical.ui.homeScreen.model.Destinations 23 | 24 | @OptIn(ExperimentalMaterial3Api::class) 25 | @Composable 26 | fun DrawerContent( 27 | modifier: Modifier = Modifier, 28 | onNavigationSelected: (destination: Destinations) -> Unit, 29 | onLogout: () -> Unit, 30 | selectedItem: Destinations 31 | ) { 32 | val items = 33 | listOf( 34 | Destinations.Upgrade, 35 | Destinations.Settings 36 | ) 37 | ModalDrawerSheet(modifier = modifier) { 38 | Spacer(Modifier.height(12.dp)) 39 | Text( 40 | modifier = Modifier.padding(16.dp), 41 | text = stringResource(id = R.string.label_name), 42 | fontSize = 20.sp 43 | ) 44 | Spacer(modifier = Modifier.height(8.dp)) 45 | Text( 46 | modifier = Modifier.padding(16.dp), 47 | text = stringResource(id = R.string.label_email_drawer), 48 | fontSize = 16.sp 49 | ) 50 | Divider( 51 | modifier = Modifier 52 | .fillMaxWidth() 53 | .padding(horizontal = 16.dp, vertical = 8.dp) 54 | ) 55 | 56 | items.forEach { item -> 57 | NavigationDrawerItem( 58 | icon = { Icon(item.icon!!, contentDescription = null) }, 59 | label = { Text(item.path) }, 60 | selected = item.path == selectedItem.path, 61 | onClick = { 62 | onNavigationSelected(item) 63 | }, 64 | modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) 65 | ) 66 | } 67 | 68 | NavigationDrawerItem( 69 | icon = { Icon(Icons.Default.Logout, contentDescription = null) }, 70 | label = { Text(text = stringResource(id = R.string.log_out)) }, 71 | selected = false, 72 | onClick = { 73 | onLogout() 74 | }, 75 | modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) 76 | ) 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/components/Home.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.DrawerState 5 | import androidx.compose.material3.DrawerValue 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.Scaffold 8 | import androidx.compose.material3.SnackbarHost 9 | import androidx.compose.material3.SnackbarHostState 10 | import androidx.compose.material3.rememberDrawerState 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.navigation.NavHostController 15 | import androidx.navigation.compose.rememberNavController 16 | import com.compose.practical.ui.homeScreen.model.Destinations 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Composable 20 | fun HomeContent( 21 | modifier: Modifier = Modifier, 22 | drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), 23 | navController: NavHostController = rememberNavController(), 24 | currentDestination: Destinations 25 | ) { 26 | val snackbarHostState = remember { SnackbarHostState() } 27 | 28 | Scaffold( 29 | snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, 30 | modifier = modifier, 31 | topBar = { 32 | 33 | RootTopBar( 34 | snackbarHostState = snackbarHostState, 35 | currentDestination = currentDestination, 36 | drawerState = drawerState 37 | ) 38 | }, 39 | bottomBar = { 40 | /*val orientation = LocalConfiguration.current.orientation 41 | if (orientation != Configuration.ORIENTATION_LANDSCAPE 42 | && currentDestination.isRootDestination 43 | ) {*/ 44 | BottomNavigationBar( 45 | currentDestination = currentDestination, 46 | onNavigate = { 47 | navController.navigate(it.path) { 48 | /* popUpTo( 49 | navController.graph.findStartDestination().id 50 | ) { 51 | saveState = true 52 | }*/ 53 | launchSingleTop = true 54 | restoreState = true 55 | 56 | } 57 | }, 58 | onFloatingBtnClick = { 59 | navController.navigate(Destinations.Creation.path) 60 | } 61 | ) 62 | // } 63 | } 64 | 65 | ) { 66 | Navigation( 67 | modifier = modifier.padding(it), navController = navController 68 | ) 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/components/Navigation.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen.components 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.navigation.NavHostController 7 | import androidx.navigation.compose.NavHost 8 | import androidx.navigation.compose.composable 9 | import androidx.navigation.navigation 10 | import com.compose.practical.ui.homeScreen.model.Destinations 11 | 12 | @Composable 13 | fun Navigation( 14 | modifier: Modifier = Modifier, 15 | navController: NavHostController 16 | ) { 17 | NavHost( 18 | modifier = modifier, 19 | navController = navController, 20 | startDestination = Destinations.Home.path 21 | ) { 22 | 23 | navigation( 24 | startDestination = Destinations.Feed.path, 25 | route = Destinations.Home.path 26 | ) { 27 | composable(route = Destinations.Feed.path) { 28 | 29 | ContentArea( 30 | modifier = Modifier.fillMaxSize(), 31 | destinations = Destinations.Feed 32 | ) 33 | } 34 | 35 | composable(route = Destinations.Contacts.path) { 36 | ContentArea( 37 | modifier = Modifier.fillMaxSize(), 38 | destinations = Destinations.Contacts 39 | ) 40 | } 41 | 42 | composable(route = Destinations.Calendar.path) { 43 | ContentArea( 44 | modifier = Modifier.fillMaxSize(), 45 | destinations = Destinations.Calendar 46 | ) 47 | } 48 | } 49 | 50 | 51 | composable(route = Destinations.Upgrade.path) { 52 | ContentArea( 53 | modifier = Modifier.fillMaxSize(), 54 | Destinations.Upgrade 55 | ) 56 | } 57 | composable(route = Destinations.Settings.path) { 58 | ContentArea( 59 | modifier = Modifier.fillMaxSize(), 60 | Destinations.Settings 61 | ) 62 | } 63 | 64 | navigation( 65 | startDestination = Destinations.Add.path, 66 | route = Destinations.Creation.path 67 | ) { 68 | composable(route = Destinations.Add.path) { 69 | ContentArea( 70 | modifier = Modifier.fillMaxSize(), 71 | destinations = Destinations.Add 72 | ) 73 | } 74 | } 75 | 76 | } 77 | 78 | } 79 | 80 | //fun addStrings(a: String, b: String): String { 81 | // val maxLength = maxOf(a.length, b.length) 82 | // val sb = StringBuilder(maxLength) 83 | // 84 | // var carry = 0 85 | // var i = a.length - 1 86 | // var j = b.length - 1 87 | // 88 | // while (i >= 0 || j >= 0 || carry != 0) { 89 | // val digitA = if (i >= 0) a[i] - '0' else 0 90 | // val digitB = if (j >= 0) b[j] - '0' else 0 91 | // 92 | // val sum = digitA + digitB + carry 93 | // carry = sum / 10 94 | // val digitSum = sum % 10 95 | // 96 | // sb.append(digitSum) 97 | // i-- 98 | // j-- 99 | // } 100 | // 101 | // return sb.reverse().toString() 102 | //} 103 | // 104 | //fun main() { 105 | // val a = "1234" 106 | // val b = "993" 107 | // val result = addStrings(a, b) 108 | // println(result) // Output: 2233 109 | //} -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/components/TopBar.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Info 5 | import androidx.compose.material.icons.filled.Menu 6 | import androidx.compose.material3.CenterAlignedTopAppBar 7 | import androidx.compose.material3.DrawerState 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.IconButton 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.SnackbarHostState 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TopAppBarDefaults 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.rememberCoroutineScope 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.res.stringResource 22 | import com.compose.practical.R 23 | import com.compose.practical.ui.homeScreen.model.Destinations 24 | import kotlinx.coroutines.launch 25 | 26 | 27 | @OptIn(ExperimentalMaterial3Api::class) 28 | @Composable 29 | fun RootTopBar( 30 | snackbarHostState: SnackbarHostState, 31 | currentDestination: Destinations, 32 | drawerState: DrawerState 33 | ) { 34 | val scope = rememberCoroutineScope() 35 | var clickCount by remember { mutableStateOf(0) } 36 | val snackbarMessage = stringResource(id = R.string.not_available_yet) 37 | 38 | CenterAlignedTopAppBar(colors = TopAppBarDefaults.centerAlignedTopAppBarColors( 39 | MaterialTheme.colorScheme.surfaceTint 40 | ), 41 | title = { 42 | Text( 43 | text = currentDestination.title 44 | ) 45 | /*path.replaceFirstChar { char -> 46 | char.titlecase(Locale.getDefault()) 47 | })*/ 48 | }, 49 | navigationIcon = { 50 | IconButton(onClick = { 51 | scope.launch { 52 | drawerState.open() 53 | } 54 | }) { 55 | Icon( 56 | imageVector = Icons.Default.Menu, 57 | contentDescription = stringResource( 58 | id = R.string.cd_open_menu 59 | ) 60 | ) 61 | 62 | } 63 | }, 64 | actions = { 65 | 66 | if (currentDestination != Destinations.Feed) { 67 | IconButton(onClick = { 68 | scope.launch { 69 | snackbarHostState.showSnackbar( 70 | "$snackbarMessage # ${++clickCount}" 71 | ) 72 | } 73 | }) { 74 | Icon( 75 | imageVector = Icons.Default.Info, 76 | contentDescription = stringResource( 77 | id = R.string.cd_more_information 78 | ) 79 | ) 80 | } 81 | } 82 | } 83 | ) 84 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/homeScreen/model/Destinations.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.homeScreen.model 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Add 5 | import androidx.compose.material.icons.filled.DateRange 6 | import androidx.compose.material.icons.filled.List 7 | import androidx.compose.material.icons.filled.Person 8 | import androidx.compose.material.icons.filled.Settings 9 | import androidx.compose.material.icons.filled.Star 10 | import androidx.compose.ui.graphics.vector.ImageVector 11 | 12 | sealed class Destinations( 13 | val path: String, 14 | val icon: ImageVector? = null, 15 | val isRootDestination: Boolean = true 16 | 17 | ) { 18 | 19 | companion object { 20 | 21 | fun fromString(route: String): Destinations { 22 | 23 | return when (route) { 24 | Feed.path -> Feed 25 | Calendar.path -> Calendar 26 | Contacts.path -> Contacts 27 | Upgrade.path -> Upgrade 28 | Settings.path -> Settings 29 | Add.path -> Add 30 | Creation.path -> Creation 31 | else -> Home 32 | } 33 | } 34 | } 35 | 36 | val title = path.replaceFirstChar { 37 | it.uppercase() 38 | } 39 | 40 | 41 | object Home : Destinations("home") 42 | 43 | object Feed : Destinations( 44 | "feed", 45 | Icons.Default.List) 46 | 47 | object Contacts : Destinations( 48 | "contacts", 49 | Icons.Default.Person 50 | ) 51 | 52 | object Calendar : Destinations( 53 | "calendar", 54 | Icons.Default.DateRange 55 | ) 56 | 57 | object Settings : Destinations( 58 | "settings", 59 | Icons.Default.Settings, 60 | isRootDestination = false 61 | ) 62 | object Upgrade : Destinations("upgrade", 63 | Icons.Default.Star, 64 | isRootDestination = false 65 | ) 66 | 67 | object Creation : Destinations( 68 | path = "creation", 69 | isRootDestination = false 70 | ) 71 | 72 | object Add : Destinations( 73 | path = "add", 74 | icon = Icons.Default.Add, 75 | isRootDestination = false 76 | ) 77 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/onboardingScreen/OnboardingScreen.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.onboardingScreen 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material3.Button 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Surface 17 | import androidx.compose.material3.Tab 18 | import androidx.compose.material3.TabRow 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.graphics.Brush 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.graphics.graphicsLayer 28 | import androidx.compose.ui.layout.ContentScale 29 | import androidx.compose.ui.platform.testTag 30 | import androidx.compose.ui.res.painterResource 31 | import androidx.compose.ui.text.style.TextAlign 32 | import androidx.compose.ui.tooling.preview.Preview 33 | import androidx.compose.ui.unit.dp 34 | import com.compose.practical.R 35 | import com.compose.practical.ui.onboardingScreen.Tags.TAG_ONBOARD_SCREEN_IMAGE_VIEW 36 | import com.compose.practical.ui.onboardingScreen.Tags.TAG_ONBOARD_SCREEN_NAV_BUTTON 37 | import com.compose.practical.ui.onboardingScreen.Tags.TAG_ONBOARD_TAG_ROW 38 | 39 | val onboardPagesList = listOf( 40 | OnboardPage( 41 | imageRes = R.drawable.image1, 42 | title = "Welcome to Onboarding", 43 | description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." 44 | ), OnboardPage( 45 | imageRes = R.drawable.image2, 46 | title = "Explore Exciting Features", 47 | description = "Praesent at semper est, nec consectetur justo." 48 | ), OnboardPage( 49 | imageRes = R.drawable.image3, 50 | title = "Get Started Now", 51 | description = "In auctor ultrices turpis at blandit." 52 | ) 53 | ) 54 | 55 | object Tags { 56 | const val TAG_ONBOARD_SCREEN = "onboard_screen" 57 | const val TAG_ONBOARD_SCREEN_IMAGE_VIEW = "onboard_screen_image" 58 | const val TAG_ONBOARD_SCREEN_NAV_BUTTON = "nav_button" 59 | const val TAG_ONBOARD_TAG_ROW = "tag_row" 60 | } 61 | 62 | 63 | @Composable 64 | fun OnboardScreen() { 65 | 66 | val onboardPages = onboardPagesList 67 | 68 | val currentPage = remember { mutableStateOf(0) } 69 | 70 | Column( 71 | modifier = Modifier 72 | .fillMaxSize() 73 | .testTag(Tags.TAG_ONBOARD_SCREEN) 74 | ) { 75 | 76 | OnBoardImageView( 77 | modifier = Modifier 78 | .weight(1f) 79 | .fillMaxWidth(), 80 | currentPage = onboardPages[currentPage.value] 81 | ) 82 | 83 | OnBoardDetails( 84 | modifier = Modifier 85 | .weight(1f) 86 | .padding(16.dp), 87 | currentPage = onboardPages[currentPage.value] 88 | ) 89 | 90 | OnBoardNavButton( 91 | modifier = Modifier 92 | .align(Alignment.CenterHorizontally) 93 | .padding(top = 16.dp), 94 | currentPage = currentPage.value, 95 | noOfPages = onboardPages.size 96 | ) { 97 | currentPage.value++ 98 | } 99 | 100 | TabSelector( 101 | onboardPages = onboardPages, 102 | currentPage = currentPage.value 103 | ) { index -> 104 | currentPage.value = index 105 | } 106 | } 107 | } 108 | 109 | @Composable 110 | fun OnBoardDetails( 111 | modifier: Modifier = Modifier, currentPage: OnboardPage 112 | ) { 113 | Column( 114 | modifier = modifier 115 | ) { 116 | Text( 117 | text = currentPage.title, 118 | style = MaterialTheme.typography.displaySmall, 119 | textAlign = TextAlign.Center, 120 | modifier = Modifier.fillMaxWidth() 121 | ) 122 | Spacer(modifier = Modifier.height(16.dp)) 123 | Text( 124 | text = currentPage.description, 125 | style = MaterialTheme.typography.bodyMedium, 126 | textAlign = TextAlign.Center, 127 | modifier = Modifier.fillMaxWidth() 128 | ) 129 | } 130 | } 131 | 132 | @Composable 133 | fun OnBoardNavButton( 134 | modifier: Modifier = Modifier, currentPage: Int, noOfPages: Int, onNextClicked: () -> Unit 135 | ) { 136 | Button( 137 | onClick = { 138 | if (currentPage < noOfPages - 1) { 139 | onNextClicked() 140 | } else { 141 | // Handle onboarding completion 142 | } 143 | }, modifier = modifier.testTag(TAG_ONBOARD_SCREEN_NAV_BUTTON) 144 | ) { 145 | Text(text = if (currentPage < noOfPages - 1) "Next" else "Get Started") 146 | } 147 | } 148 | 149 | 150 | @Composable 151 | fun OnBoardImageView(modifier: Modifier = Modifier, currentPage: OnboardPage) { 152 | val imageRes = currentPage.imageRes 153 | Box( 154 | modifier = modifier 155 | .testTag(TAG_ONBOARD_SCREEN_IMAGE_VIEW + currentPage.title) 156 | ) { 157 | Image( 158 | painter = painterResource(id = imageRes), 159 | contentDescription = null, 160 | modifier = Modifier.fillMaxSize(), 161 | contentScale = ContentScale.FillWidth 162 | ) 163 | Box(modifier = Modifier 164 | .fillMaxSize() 165 | .align(Alignment.BottomCenter) 166 | .graphicsLayer { 167 | // Apply alpha to create the fading effect 168 | alpha = 0.6f 169 | } 170 | .background( 171 | Brush.verticalGradient( 172 | colorStops = arrayOf( 173 | Pair(0.8f, Color.Transparent), Pair(1f, Color.White) 174 | ) 175 | ) 176 | )) 177 | } 178 | } 179 | 180 | @Composable 181 | fun TabSelector(onboardPages: List, currentPage: Int, onTabSelected: (Int) -> Unit) { 182 | TabRow( 183 | selectedTabIndex = currentPage, 184 | modifier = Modifier 185 | .fillMaxWidth() 186 | .background(MaterialTheme.colorScheme.primary) 187 | .testTag(TAG_ONBOARD_TAG_ROW) 188 | 189 | ) { 190 | onboardPages.forEachIndexed { index, _ -> 191 | Tab(selected = index == currentPage, onClick = { 192 | onTabSelected(index) 193 | }, modifier = Modifier.padding(16.dp), content = { 194 | Box( 195 | modifier = Modifier 196 | .testTag("$TAG_ONBOARD_TAG_ROW$index") 197 | .size(8.dp) 198 | .background( 199 | color = if (index == currentPage) MaterialTheme.colorScheme.onPrimary 200 | else Color.LightGray, shape = RoundedCornerShape(4.dp) 201 | ) 202 | ) 203 | }) 204 | } 205 | } 206 | } 207 | 208 | data class OnboardPage( 209 | val imageRes: Int, 210 | val title: String, 211 | val description: String 212 | ) 213 | 214 | @Preview 215 | @Composable 216 | fun PreviewOnboardScreen() { 217 | MaterialTheme { 218 | Surface { 219 | OnboardScreen() 220 | } 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun PracticalComposeTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/compose/practical/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.compose.practical.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/drawable/image1.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/drawable/image2.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/image3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/drawable/image3.jpg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PracticalCompose 3 | 4 | 5 | At least 8 characters 6 | 7 | 8 | 9 | At least 1 upper-case letter 10 | 11 | 12 | At least 1 digit 13 | 14 | 15 | 16 | Sign In to your account 17 | 18 | 19 | Sign Up for an account 20 | 21 | 22 | Email Address 23 | Password 24 | 25 | Show Password 26 | Hide Password 27 | 28 | 29 | %s, satisfied 30 | 31 | 32 | %s, needed 33 | 34 | 35 | Sign Up 36 | Sign In 37 | 38 | 39 | Need an account? 40 | 41 | 42 | Already have an account? 43 | 44 | 45 | Whoops 46 | OK 47 | 48 | 49 | Create a new item 50 | More information 51 | Not available yet 52 | 53 | Compose Academy 54 | contact@compose.academy 55 | Logout 56 | Open Menu 57 | Inbox (%s) 58 | 59 | Whoops, there was a problem loading the content. 60 | 61 | Try again? 62 | 63 | 64 | Nothing to show here yet. 65 | 66 | Check again 67 | Delete Email 68 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |