├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 | |  |  |  |  |
13 |
14 | | Onboarding Screen |
15 | |--------------------------| --------------- | ----------- | ------------------- |
16 | |  |
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 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/compose/practical/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.practical
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id 'com.android.application' version '8.1.4' apply false
4 | id 'com.android.library' version '8.1.4' apply false
5 | id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
6 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 02 17:09:53 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/runtimepermissions/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/runtimepermissions/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'com.compose.runtimepermissions'
8 | compileSdk 34
9 |
10 | defaultConfig {
11 | applicationId "com.compose.runtimepermissions"
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 | testImplementation 'junit:junit:4.13.2'
61 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
62 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
63 | androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01')
64 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
65 | debugImplementation 'androidx.compose.ui:ui-tooling'
66 | debugImplementation 'androidx.compose.ui:ui-test-manifest'
67 |
68 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
69 | }
--------------------------------------------------------------------------------
/runtimepermissions/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
--------------------------------------------------------------------------------
/runtimepermissions/src/androidTest/java/com/compose/runtimepermissions/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions
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.runtimepermissions", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/runtimepermissions/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/runtimepermissions/src/main/java/com/compose/runtimepermissions/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Surface
9 | import androidx.compose.ui.Modifier
10 | import com.compose.runtimepermissions.ui.PermissionsScreen
11 |
12 | class MainActivity : ComponentActivity() {
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | setContent {
16 | MaterialTheme {
17 | // A surface container using the 'background' color from the theme
18 | Surface(
19 | modifier = Modifier.fillMaxSize(),
20 | color = MaterialTheme.colorScheme.background
21 | ) {
22 | PermissionsScreen()
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/runtimepermissions/src/main/java/com/compose/runtimepermissions/RuntimePermissionStates.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions
2 |
3 | enum class PermissionStates(
4 | val permission:String,
5 | val title:String,
6 | val description:String,
7 | val deniedDescription:String
8 | ) {
9 | COARSE_LOCATION(
10 | permission = android.Manifest.permission.ACCESS_COARSE_LOCATION,
11 | title = "Approximate Location Permission",
12 | description = "This permission is needed to get your approximate location. Please grant the permission.",
13 | deniedDescription = "This permission is needed to get your approximate location. Please grant the permission in app settings."
14 | ),
15 | READ_CALENDAR(
16 | permission = android.Manifest.permission.READ_CALENDAR,
17 | title = "Read Calendar Permission",
18 | description = "This permission is needed to read your calendar. Please grant the permission.",
19 | deniedDescription = "This permission is needed to read your calendar. Please grant the permission in app settings."
20 | ),
21 | READ_CONTACTS(
22 | permission = android.Manifest.permission.READ_CONTACTS,
23 | title = "Read Contacts Permission",
24 | description = "This permission is needed to read your contacts. Please grant the permission.",
25 | deniedDescription = "This permission is needed to read your contacts. Please grant the permission in app settings."
26 | ),
27 | RECORD_AUDIO(
28 | permission = android.Manifest.permission.RECORD_AUDIO,
29 | title = "Record Audio Permission",
30 | description = "This permission is needed to record audio. Please grant the permission.",
31 | deniedDescription = "This permission is needed to record audio. Please grant the permission in app settings."
32 | );
33 |
34 | fun permissionTextProvider(isPermanentDenied: Boolean): String {
35 | return if (isPermanentDenied) this.deniedDescription else this.description
36 | }
37 | }
38 |
39 | fun getNeededPermission(permission: String): PermissionStates {
40 | return PermissionStates.values().find { it.permission == permission } ?: throw IllegalArgumentException("Permission $permission is not supported")
41 | }
--------------------------------------------------------------------------------
/runtimepermissions/src/main/java/com/compose/runtimepermissions/ui/PermissionViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions.ui
2 |
3 | import androidx.compose.runtime.mutableStateListOf
4 | import androidx.lifecycle.ViewModel
5 | import com.compose.runtimepermissions.PermissionStates
6 |
7 | class PermissionViewModel : ViewModel() {
8 |
9 | val permissionsDeclined = mutableStateListOf()
10 |
11 | fun removePermissionFromList(permission: PermissionStates) {
12 | permissionsDeclined.remove(permission)
13 | }
14 |
15 | fun addPermissionToList(
16 | permission: PermissionStates
17 | ) {
18 | permissionsDeclined.add(permission)
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/runtimepermissions/src/main/java/com/compose/runtimepermissions/ui/UiComponents.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions.ui
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.provider.Settings
7 | import androidx.activity.compose.rememberLauncherForActivityResult
8 | import androidx.activity.result.contract.ActivityResultContracts
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.material3.AlertDialog
13 | import androidx.compose.material3.Button
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.platform.LocalContext
20 | import androidx.compose.ui.tooling.preview.Preview
21 | import androidx.compose.ui.unit.dp
22 | import androidx.lifecycle.viewmodel.compose.viewModel
23 | import com.compose.runtimepermissions.PermissionStates
24 | import com.compose.runtimepermissions.getNeededPermission
25 |
26 |
27 | @Composable
28 | fun PermissionsScreen() {
29 |
30 | val activity = LocalContext.current as Activity
31 |
32 | val viewModel: PermissionViewModel = viewModel()
33 |
34 | // val permissionDialog = remember {
35 | // mutableStateListOf()
36 | // }
37 |
38 | /*to launch a single permission request*/
39 | val singlePermissionLauncher = rememberLauncherForActivityResult(
40 | contract = ActivityResultContracts.RequestPermission(),
41 | onResult = { isGranted ->
42 | if (!isGranted)
43 | viewModel.addPermissionToList(PermissionStates.RECORD_AUDIO)
44 | }
45 | )
46 |
47 | /*to launch multiple permission request*/
48 | val multiplePermissionLauncher = rememberLauncherForActivityResult(
49 | contract = ActivityResultContracts.RequestMultiplePermissions(),
50 | onResult = { permissions ->
51 | permissions.entries.forEach { entry ->
52 | if (!entry.value)
53 | viewModel.addPermissionToList(getNeededPermission(entry.key))
54 | // permissionDialog.add(getNeededPermission(entry.key))
55 | }
56 | }
57 | )
58 |
59 |
60 | Column(
61 | modifier = Modifier
62 | .fillMaxSize(),
63 | horizontalAlignment = Alignment.CenterHorizontally,
64 | verticalArrangement = Arrangement.spacedBy(
65 | 16.dp,
66 | Alignment.CenterVertically
67 | )
68 | ) {
69 | Button(
70 | onClick = {
71 | singlePermissionLauncher.launch(PermissionStates.RECORD_AUDIO.permission)
72 | }
73 | ) {
74 | Text(text = "Request Single Permission")
75 | }
76 |
77 | Button(
78 | onClick = {
79 | multiplePermissionLauncher.launch(
80 | arrayOf(
81 | PermissionStates.COARSE_LOCATION.permission,
82 | PermissionStates.READ_CALENDAR.permission,
83 | PermissionStates.READ_CONTACTS.permission
84 | )
85 | )
86 | }
87 | ) {
88 | Text(text = "Request multiple Permissions")
89 | }
90 |
91 | }
92 |
93 | viewModel.permissionsDeclined.forEach { permission ->
94 | PermissionAlertDialog(
95 | title = "App Permissions",
96 | neededPermission = permission,
97 | isPermissionDeclined = !activity.shouldShowRequestPermissionRationale(permission.permission),
98 | onDismiss = {
99 | viewModel.removePermissionFromList(permission)
100 | },
101 | onOkClick = {
102 | viewModel.removePermissionFromList(permission)
103 | multiplePermissionLauncher.launch(arrayOf(permission.permission))
104 | }
105 | ) {
106 | viewModel.removePermissionFromList(permission)
107 | activity.goToAppSetting()
108 | }
109 | }
110 | }
111 |
112 |
113 | fun Activity.goToAppSetting() {
114 | val i = Intent(
115 | Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
116 | Uri.fromParts("package", packageName, null)
117 | )
118 | startActivity(i)
119 | }
120 |
121 |
122 | @Composable
123 | fun PermissionAlertDialog(
124 | title: String,
125 | neededPermission: PermissionStates,
126 | isPermissionDeclined: Boolean,
127 | onDismiss: () -> Unit,
128 | onOkClick: () -> Unit,
129 | onGotoAppSettingClick: () -> Unit,
130 | ) {
131 |
132 | AlertDialog(
133 | onDismissRequest = onDismiss,
134 | confirmButton = {
135 | if (isPermissionDeclined) {
136 |
137 | Button(
138 | onClick =
139 | onGotoAppSettingClick
140 | ) {
141 | Text(text = "Open App Settings")
142 | }
143 |
144 | } else {
145 |
146 | Button(
147 | onClick =
148 | onOkClick
149 | ) {
150 | Text(text = "Allow")
151 | }
152 |
153 | }
154 |
155 | },
156 | dismissButton = {
157 | Button(
158 | onClick =
159 | onDismiss
160 | ) {
161 | Text(text = "Deny")
162 | }
163 | },
164 | title = { Text(text = title) },
165 | text = { Text(text = neededPermission.permissionTextProvider(isPermissionDeclined)) }
166 | )
167 |
168 | }
169 |
170 | @Preview(showBackground = true)
171 | @Composable
172 | fun AlertDialogPreview() {
173 | MaterialTheme {
174 | PermissionAlertDialog(
175 | title = "App Permissions",
176 | neededPermission = getNeededPermission(android.Manifest.permission.RECORD_AUDIO),
177 | isPermissionDeclined = true,
178 | onDismiss = {},
179 | onOkClick = {}
180 | ) {}
181 | }
182 | }
183 |
184 | @Preview(showBackground = true)
185 | @Composable
186 | fun PermissionScreenPreview() {
187 | MaterialTheme {
188 | PermissionsScreen()
189 | }
190 | }
--------------------------------------------------------------------------------
/runtimepermissions/src/main/java/com/compose/runtimepermissions/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions.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)
--------------------------------------------------------------------------------
/runtimepermissions/src/main/java/com/compose/runtimepermissions/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions.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 | }
--------------------------------------------------------------------------------
/runtimepermissions/src/main/java/com/compose/runtimepermissions/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions.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 | )
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/runtimepermissions/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 |
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/runtimepermissions/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RuntimePermissions
3 |
--------------------------------------------------------------------------------
/runtimepermissions/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/runtimepermissions/src/test/java/com/compose/runtimepermissions/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.runtimepermissions
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "PracticalCompose"
16 | include ':app'
17 | include ':settings'
18 | include ':runtimepermissions'
19 |
--------------------------------------------------------------------------------
/settings/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/settings/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'com.compose.settings'
8 | compileSdk 34
9 |
10 | defaultConfig {
11 | applicationId "com.compose.settings"
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 | testImplementation 'junit:junit:4.13.2'
61 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
62 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
63 | androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01')
64 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
65 | debugImplementation 'androidx.compose.ui:ui-tooling'
66 | debugImplementation 'androidx.compose.ui:ui-test-manifest'
67 |
68 | androidTestImplementation 'org.mockito.kotlin:mockito-kotlin:5.1.0'
69 | androidTestImplementation 'org.mockito:mockito-android:5.6.0'
70 |
71 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
72 |
73 | }
--------------------------------------------------------------------------------
/settings/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
--------------------------------------------------------------------------------
/settings/src/androidTest/java/com/compose/settings/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings
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.settings", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/settings/src/androidTest/java/com/compose/settings/SettingsTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.assertIsOn
6 | import androidx.compose.ui.test.assertIsSelected
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.test.platform.app.InstrumentationRegistry
13 | import com.compose.settings.ui.Settings
14 | import com.compose.settings.ui.Tags
15 | import com.compose.settings.ui.Tags.TAG_CHECK_ITEM
16 | import com.compose.settings.ui.Tags.TAG_MARKETING_OPTION
17 | import com.compose.settings.ui.Tags.TAG_THEME_DROPDOWN_OPTION
18 | import com.compose.settings.ui.Tags.TAG_THEME_OPTION
19 | import com.compose.settings.ui.Tags.TAG_TOGGLE_ITEM
20 | import com.compose.settings.ui.Theme
21 | import org.junit.Rule
22 | import org.junit.Test
23 |
24 | class SettingsTest {
25 |
26 | @get:Rule
27 | val composeTestRule = createComposeRule()
28 |
29 |
30 | private fun assertSettingIsDisplayed(
31 | @StringRes title: Int
32 | ) {
33 | composeTestRule.setContent {
34 | Settings()
35 | }
36 |
37 | composeTestRule.onNodeWithText(
38 | InstrumentationRegistry.getInstrumentation().targetContext.getString(title)
39 | ).assertIsDisplayed()
40 |
41 | }
42 |
43 | @Test
44 | fun Enable_Notifications_Setting_Is_Displayed() {
45 |
46 | assertSettingIsDisplayed(R.string.setting_enable_notifications)
47 |
48 | }
49 |
50 | @Test
51 | fun Show_Hints_Setting_Is_Displayed() {
52 |
53 | assertSettingIsDisplayed(R.string.setting_enable_hint)
54 | }
55 |
56 | @Test
57 | fun Manage_Subscription_Setting_Is_Displayed() {
58 |
59 | assertSettingIsDisplayed(R.string.setting_manage_subscription)
60 | }
61 |
62 | @Test
63 | fun App_Version_Setting_Is_Displayed() {
64 | assertSettingIsDisplayed(
65 | R.string.setting_app_version_title
66 | )
67 | }
68 |
69 | @Test
70 | fun Theme_Setting_Is_Displayed() {
71 | assertSettingIsDisplayed(
72 | R.string.setting_option_theme
73 | )
74 | }
75 |
76 | @Test
77 | fun Marketing_Options_Setting_Is_Displayed() {
78 | assertSettingIsDisplayed(
79 | R.string.setting_option_marketing
80 | )
81 | }
82 |
83 | @Test
84 | fun Enable_Notifications_Toggles_Selected_State() {
85 | composeTestRule.setContent {
86 | Settings()
87 | }
88 | composeTestRule.onNodeWithText(
89 | InstrumentationRegistry.getInstrumentation().targetContext.getString(
90 | R.string.setting_enable_notifications
91 | )
92 | ).performClick()
93 | composeTestRule.onNodeWithTag(
94 | TAG_TOGGLE_ITEM
95 | ).assertIsOn()
96 |
97 | }
98 |
99 | @Test
100 | fun Show_Hints_Toggles_Selected_State() {
101 | composeTestRule.setContent {
102 | Settings()
103 | }
104 | composeTestRule.onNodeWithText(
105 | InstrumentationRegistry.getInstrumentation().targetContext.getString(
106 | R.string.setting_enable_hint
107 | )
108 | ).performClick()
109 | composeTestRule.onNodeWithTag(
110 | TAG_CHECK_ITEM
111 | ).assertIsOn()
112 |
113 | }
114 |
115 | @Test
116 | fun Marketing_options_Toggles_Selected_State() {
117 |
118 | composeTestRule.setContent {
119 | Settings()
120 | }
121 |
122 | composeTestRule.onNodeWithText(
123 | InstrumentationRegistry.getInstrumentation().targetContext
124 | .resources.getStringArray(R.array.setting_options_marketing_choice)[1]
125 | ).performClick()
126 |
127 | composeTestRule.onNodeWithTag(TAG_MARKETING_OPTION + 1).assertIsSelected()
128 | }
129 |
130 | @Test
131 | fun Theme_options_dropdown_Selected_State() {
132 |
133 | composeTestRule.setContent {
134 | Settings()
135 | }
136 |
137 | composeTestRule.onNodeWithText(
138 | InstrumentationRegistry.getInstrumentation().targetContext
139 | .resources.getString(R.string.setting_option_theme)
140 | ).performClick()
141 |
142 | composeTestRule.onNodeWithTag(Tags.TAG_THEME_DROPDOWN).assertIsDisplayed()
143 |
144 | composeTestRule.onNodeWithText(
145 | InstrumentationRegistry.getInstrumentation().targetContext
146 | .resources.getString(R.string.theme_dark)
147 | ).performClick()
148 |
149 |
150 | composeTestRule.onNodeWithTag(
151 | TAG_THEME_OPTION, useUnmergedTree = true
152 | ).assertTextEquals(
153 | InstrumentationRegistry.getInstrumentation().targetContext
154 | .resources.getString(R.string.theme_dark)
155 | )
156 |
157 | }
158 |
159 | }
--------------------------------------------------------------------------------
/settings/src/androidTest/java/com/compose/settings/settingFeatures/AppVersionSettingItemTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.settingFeatures
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 com.compose.settings.ui.AppVersionSettingItem
7 | import org.junit.Rule
8 | import org.junit.Test
9 |
10 | class AppVersionSettingItemTest {
11 |
12 | @get:Rule
13 | val composeTestRule = createComposeRule()
14 |
15 | @Test
16 | fun App_Version_Displayed() {
17 | val version = "1.0.4"
18 | composeTestRule.setContent {
19 | AppVersionSettingItem(appVersion = version)
20 | }
21 | composeTestRule
22 | .onNodeWithText(version)
23 | .assertIsDisplayed()
24 | }
25 | }
--------------------------------------------------------------------------------
/settings/src/androidTest/java/com/compose/settings/settingFeatures/HintsSettingItemTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.settingFeatures
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.assertIsOn
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.onNodeWithText
8 | import com.compose.settings.ui.HintSettingItem
9 | import com.compose.settings.ui.Tags.TAG_CHECK_ITEM
10 | import org.junit.Rule
11 | import org.junit.Test
12 |
13 | class HintsSettingItemTest {
14 |
15 | @get:Rule
16 | val composeTestRule = createComposeRule()
17 |
18 | @Test
19 | fun Title_Displayed() {
20 | val title = "Show Hints"
21 | composeTestRule.setContent {
22 | HintSettingItem(
23 | title = title,
24 | checked = true,
25 | onCheckedChanged = {}
26 | )
27 | }
28 | composeTestRule.onNodeWithText(title).assertIsDisplayed()
29 | }
30 |
31 |
32 | @Test
33 | fun Setting_Checked() {
34 | composeTestRule.setContent {
35 | HintSettingItem(
36 | title = "Show Hints",
37 | checked = true,
38 | onCheckedChanged = { }
39 | )
40 | }
41 | composeTestRule
42 | .onNodeWithTag(TAG_CHECK_ITEM)
43 | .assertIsOn()
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/settings/src/androidTest/java/com/compose/settings/settingFeatures/ManageSubscriptionSettingItemTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.settingFeatures
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.compose.ui.test.performClick
7 | import com.compose.settings.ui.ManageSubscriptionItem
8 | import org.junit.Rule
9 | import org.junit.Test
10 | import org.mockito.Mockito.verify
11 | import org.mockito.kotlin.mock
12 |
13 |
14 | class ManageSubscriptionSettingItemTest {
15 |
16 | @get:Rule
17 | val composeTestRule = createComposeRule()
18 |
19 | @Test
20 | fun Title_displayed() {
21 | val title = "Manage Subscription"
22 | composeTestRule.setContent {
23 | ManageSubscriptionItem(
24 | title = title,
25 | onSettingClicked = { }
26 | )
27 | }
28 | composeTestRule
29 | .onNodeWithText(title)
30 | .assertIsDisplayed()
31 | }
32 |
33 | @Test
34 | fun On_Setting_Clicked_Triggered() {
35 | val title = "Manage Subscription"
36 | val onSettingClicked: () -> Unit = mock()
37 |
38 | composeTestRule.setContent {
39 | ManageSubscriptionItem(
40 | title = title,
41 | onSettingClicked = onSettingClicked
42 | )
43 | }
44 |
45 | composeTestRule.onNodeWithText(title).performClick()
46 | verify(onSettingClicked).invoke()
47 |
48 | }
49 | }
--------------------------------------------------------------------------------
/settings/src/androidTest/java/com/compose/settings/settingFeatures/MarketingSettingItemTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.settingFeatures
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.assertIsSelected
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.onNodeWithText
8 | import androidx.compose.ui.test.performClick
9 | import androidx.test.platform.app.InstrumentationRegistry
10 | import com.compose.settings.R
11 | import com.compose.settings.ui.MarketingOption
12 | import com.compose.settings.ui.MarketingSettingItem
13 | import com.compose.settings.ui.Tags
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import org.mockito.kotlin.mock
17 | import org.mockito.kotlin.verify
18 |
19 | class MarketingSettingItemTest {
20 |
21 | @get:Rule
22 | val composeTestRule = createComposeRule()
23 |
24 | @Test
25 | fun Title_Displayed(){
26 | val marketingOption: MarketingOption = MarketingOption.ALLOWED
27 | composeTestRule.setContent {
28 | MarketingSettingItem(selectedOption =marketingOption , onOptionSelected = {})
29 | }
30 |
31 | composeTestRule.onNodeWithText(
32 | InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.setting_option_marketing)
33 | ).assertIsDisplayed()
34 | }
35 |
36 | @Test
37 | fun Marketing_Option_Selected(){
38 | val marketingOption: MarketingOption = MarketingOption.NOT_ALLOWED
39 | composeTestRule.setContent {
40 | MarketingSettingItem(selectedOption =marketingOption , onOptionSelected = {})
41 | }
42 |
43 | composeTestRule.onNodeWithTag(Tags.TAG_MARKETING_OPTION + 1).assertIsSelected()
44 | }
45 |
46 | @Test
47 | fun On_Option_Change_Triggered(){
48 | val marketingOption: MarketingOption = MarketingOption.NOT_ALLOWED
49 | val onOptionSelected: (option: MarketingOption) -> Unit = mock()
50 |
51 | composeTestRule.setContent {
52 | MarketingSettingItem(selectedOption =marketingOption , onOptionSelected = onOptionSelected)
53 | }
54 |
55 | composeTestRule.onNodeWithTag(Tags.TAG_MARKETING_OPTION+0).performClick()
56 | verify(onOptionSelected).invoke(MarketingOption.ALLOWED)
57 | }
58 | }
--------------------------------------------------------------------------------
/settings/src/androidTest/java/com/compose/settings/settingFeatures/NotificationSettingItemTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.settingFeatures
2 |
3 | import androidx.compose.ui.test.assertIsDisplayed
4 | import androidx.compose.ui.test.assertIsOn
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.onNodeWithText
8 | import com.compose.settings.ui.NotificationSetting
9 | import com.compose.settings.ui.Tags
10 | import org.junit.Rule
11 | import org.junit.Test
12 |
13 | class NotificationSettingItemTest {
14 |
15 | @get:Rule
16 | val composeTestRule = createComposeRule()
17 |
18 | @Test
19 | fun Title_Displayed(){
20 | val title = "Enable Notification"
21 | composeTestRule.setContent {
22 | NotificationSetting(title = title, checked =true , onCheckedChanged ={} )
23 | }
24 |
25 | composeTestRule.onNodeWithText(title).assertIsDisplayed()
26 |
27 | }
28 |
29 | @Test
30 | fun Setting_Checked() {
31 | composeTestRule.setContent {
32 | NotificationSetting(
33 | title = "Enable Notifications",
34 | checked = true,
35 | onCheckedChanged = { }
36 | )
37 | }
38 |
39 | composeTestRule.onNodeWithTag(
40 | Tags.TAG_TOGGLE_ITEM
41 | ).assertIsOn()
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/settings/src/androidTest/java/com/compose/settings/settingFeatures/ThemeSettingItemTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.settingFeatures
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.test.platform.app.InstrumentationRegistry
9 | import com.compose.settings.ui.Tags
10 | import com.compose.settings.ui.Tags.TAG_SELECT_THEME
11 | import com.compose.settings.ui.Tags.TAG_THEME_OPTION
12 | import com.compose.settings.ui.Theme
13 | import com.compose.settings.ui.ThemeSettingItem
14 | import org.junit.Rule
15 | import org.junit.Test
16 |
17 | class ThemeSettingItemTest {
18 |
19 | @get:Rule
20 | val composeTestRule = createComposeRule()
21 |
22 | @Test
23 | fun Selected_Theme_Displayed(){
24 | val option = Theme.DARK
25 |
26 | composeTestRule.setContent {
27 | ThemeSettingItem(selectedTheme = option, onOptionSelected ={} )
28 | }
29 |
30 | // selected theme tag
31 | composeTestRule.onNodeWithTag(TAG_THEME_OPTION, useUnmergedTree = true).assertTextEquals(
32 | InstrumentationRegistry.getInstrumentation().targetContext.getString(option.label)
33 | )
34 |
35 | }
36 |
37 | @Test
38 | fun Theme_Options_Displayed() {
39 | composeTestRule.setContent {
40 | ThemeSettingItem(
41 | selectedTheme = Theme.DARK,
42 | onOptionSelected = { }
43 | )
44 | }
45 | composeTestRule
46 | .onNodeWithTag(TAG_SELECT_THEME)
47 | .performClick()
48 |
49 | Theme.values().forEach { theme ->
50 | composeTestRule
51 | .onNodeWithTag(
52 | Tags.TAG_THEME_DROPDOWN_OPTION + InstrumentationRegistry
53 | .getInstrumentation().targetContext
54 | .getString(theme.label), true
55 | ).assertIsDisplayed()
56 | }
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/settings/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/settings/src/main/java/com/compose/settings/MainSettingActivity.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Surface
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import com.compose.settings.ui.Settings
14 | import com.compose.settings.ui.theme.PracticalComposeTheme
15 |
16 | class MainSettingActivity : ComponentActivity() {
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContent {
20 | PracticalComposeTheme {
21 | // // A surface container using the 'background' color from the theme
22 | // Surface(
23 | // modifier = Modifier.fillMaxSize(),
24 | // color = MaterialTheme.colorScheme.background
25 | // ) {
26 | // Greeting("Babu")
27 | // }
28 |
29 | Settings()
30 | }
31 | }
32 | }
33 | }
34 |
35 |
36 | @Composable
37 | fun Greeting(name: String, modifier: Modifier = Modifier) {
38 | Text(
39 | text = "Hello $name!",
40 | modifier = modifier
41 | )
42 | }
43 |
44 | @Preview(showBackground = true)
45 | @Composable
46 | fun GreetingPreview() {
47 | PracticalComposeTheme {
48 | Greeting("Android")
49 | }
50 | }
--------------------------------------------------------------------------------
/settings/src/main/java/com/compose/settings/ui/SettingState.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.ui
2 |
3 | import androidx.annotation.StringRes
4 | import com.compose.settings.R
5 |
6 | /*setting states*/
7 | data class SettingsState(
8 | val notificationEnabled: Boolean = false,
9 | val hintsEnabled: Boolean = false,
10 | val marketingOption: MarketingOption = MarketingOption.ALLOWED,
11 | val themeOption: Theme = Theme.SYSTEM
12 | )
13 |
14 | enum class MarketingOption(val id: Int) {
15 | ALLOWED(0), NOT_ALLOWED(1)
16 | }
17 |
18 | enum class Theme(@StringRes val label: Int) {
19 | LIGHT(R.string.theme_light),
20 | DARK(R.string.theme_dark),
21 | SYSTEM(R.string.theme_system)
22 | }
23 |
--------------------------------------------------------------------------------
/settings/src/main/java/com/compose/settings/ui/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.ui
2 |
3 | import androidx.lifecycle.ViewModel
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 |
6 | class SettingsViewModel : ViewModel() {
7 |
8 | val uiState = MutableStateFlow(SettingsState())
9 |
10 | fun toggleNotificationSettings(boolean: Boolean) {
11 | uiState.value = uiState.value.copy(
12 | notificationEnabled =!uiState.value.notificationEnabled
13 | )
14 |
15 | }
16 |
17 | fun toggleHintSettings(boolean: Boolean) {
18 | uiState.value = uiState.value.copy(hintsEnabled = !
19 | uiState.value.hintsEnabled)
20 | }
21 |
22 | fun setMarketingSettings(option: MarketingOption) {
23 | uiState.value = uiState.value.copy(
24 | marketingOption = option)
25 | }
26 |
27 | fun setTheme(theme: Theme) {
28 | uiState.value = uiState.value.copy(themeOption = theme)
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/settings/src/main/java/com/compose/settings/ui/Tags.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.ui
2 |
3 | object Tags {
4 | const val TAG_TOGGLE_ITEM = "toggle_item"
5 | const val TAG_CHECK_ITEM = "check_item"
6 | const val TAG_MARKETING_OPTION = "marketing_option_"
7 | const val TAG_THEME_OPTION = "theme_option"
8 | const val TAG_THEME_DROPDOWN = "theme_option"
9 | const val TAG_SELECT_THEME = "select_theme"
10 | const val TAG_THEME_DROPDOWN_OPTION = "theme_"
11 |
12 |
13 | }
--------------------------------------------------------------------------------
/settings/src/main/java/com/compose/settings/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val md_theme_light_primary = Color(0xFF825500)
6 | val md_theme_light_onPrimary = Color(0xFFFFFFFF)
7 | val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
8 | val md_theme_light_onPrimaryContainer = Color(0xFF291800)
9 | val md_theme_light_secondary = Color(0xFF6F5B40)
10 | val md_theme_light_onSecondary = Color(0xFFFFFFFF)
11 | val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
12 | val md_theme_light_onSecondaryContainer = Color(0xFF271904)
13 | val md_theme_light_tertiary = Color(0xFF51643F)
14 | val md_theme_light_onTertiary = Color(0xFFFFFFFF)
15 | val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
16 | val md_theme_light_onTertiaryContainer = Color(0xFF102004)
17 | val md_theme_light_error = Color(0xFFBA1A1A)
18 | val md_theme_light_errorContainer = Color(0xFFFFDAD6)
19 | val md_theme_light_onError = Color(0xFFFFFFFF)
20 | val md_theme_light_onErrorContainer = Color(0xFF410002)
21 | val md_theme_light_background = Color(0xFFFFFBFF)
22 | val md_theme_light_onBackground = Color(0xFF1F1B16)
23 | val md_theme_light_surface = Color(0xFFFFFBFF)
24 | val md_theme_light_onSurface = Color(0xFF1F1B16)
25 | val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
26 | val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
27 | val md_theme_light_outline = Color(0xFF817567)
28 | val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
29 | val md_theme_light_inverseSurface = Color(0xFF34302A)
30 | val md_theme_light_inversePrimary = Color(0xFFFFB951)
31 | val md_theme_light_shadow = Color(0xFF000000)
32 | val md_theme_light_surfaceTint = Color(0xFF825500)
33 | val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
34 | val md_theme_light_scrim = Color(0xFF000000)
35 |
36 | val md_theme_dark_primary = Color(0xFFFFB951)
37 | val md_theme_dark_onPrimary = Color(0xFF452B00)
38 | val md_theme_dark_primaryContainer = Color(0xFF633F00)
39 | val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
40 | val md_theme_dark_secondary = Color(0xFFDDC2A1)
41 | val md_theme_dark_onSecondary = Color(0xFF3E2D16)
42 | val md_theme_dark_secondaryContainer = Color(0xFF56442A)
43 | val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
44 | val md_theme_dark_tertiary = Color(0xFFB8CEA1)
45 | val md_theme_dark_onTertiary = Color(0xFF243515)
46 | val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
47 | val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
48 | val md_theme_dark_error = Color(0xFFFFB4AB)
49 | val md_theme_dark_errorContainer = Color(0xFF93000A)
50 | val md_theme_dark_onError = Color(0xFF690005)
51 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
52 | val md_theme_dark_background = Color(0xFF1F1B16)
53 | val md_theme_dark_onBackground = Color(0xFFEAE1D9)
54 | val md_theme_dark_surface = Color(0xFF1F1B16)
55 | val md_theme_dark_onSurface = Color(0xFFEAE1D9)
56 | val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
57 | val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
58 | val md_theme_dark_outline = Color(0xFF9C8F80)
59 | val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
60 | val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
61 | val md_theme_dark_inversePrimary = Color(0xFF825500)
62 | val md_theme_dark_shadow = Color(0xFF000000)
63 | val md_theme_dark_surfaceTint = Color(0xFFFFB951)
64 | val md_theme_dark_outlineVariant = Color(0xFF4F4539)
65 | val md_theme_dark_scrim = Color(0xFF000000)
66 |
67 |
68 | val seed = Color(0xFF825500)
--------------------------------------------------------------------------------
/settings/src/main/java/com/compose/settings/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.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 = md_theme_dark_primary,
20 | onPrimary = md_theme_dark_onPrimary,
21 | primaryContainer = md_theme_dark_primaryContainer,
22 | onPrimaryContainer = md_theme_dark_onPrimaryContainer,
23 | secondary = md_theme_dark_secondary,
24 | onSecondary = md_theme_dark_onSecondary,
25 | secondaryContainer = md_theme_dark_secondaryContainer,
26 | onSecondaryContainer = md_theme_dark_onSecondaryContainer,
27 | tertiary = md_theme_dark_tertiary,
28 | onTertiary = md_theme_dark_onTertiary,
29 | tertiaryContainer = md_theme_dark_tertiaryContainer,
30 | onTertiaryContainer = md_theme_dark_onTertiaryContainer,
31 | error = md_theme_dark_error,
32 | errorContainer = md_theme_dark_errorContainer,
33 | onError = md_theme_dark_onError,
34 | onErrorContainer = md_theme_dark_onErrorContainer,
35 | background = md_theme_dark_background,
36 | onBackground = md_theme_dark_onBackground,
37 | surface = md_theme_dark_surface,
38 | onSurface = md_theme_dark_onSurface,
39 | surfaceVariant = md_theme_dark_surfaceVariant,
40 | onSurfaceVariant = md_theme_dark_onSurfaceVariant,
41 | outline = md_theme_dark_outline,
42 | inverseOnSurface = md_theme_dark_inverseOnSurface,
43 | inverseSurface = md_theme_dark_inverseSurface,
44 | inversePrimary = md_theme_dark_inversePrimary,
45 | surfaceTint = md_theme_dark_surfaceTint,
46 | outlineVariant = md_theme_dark_outlineVariant,
47 | scrim = md_theme_dark_scrim,
48 | )
49 |
50 |
51 | private val LightColorScheme = lightColorScheme(
52 | primary = md_theme_light_primary,
53 | onPrimary = md_theme_light_onPrimary,
54 | primaryContainer = md_theme_light_primaryContainer,
55 | onPrimaryContainer = md_theme_light_onPrimaryContainer,
56 | secondary = md_theme_light_secondary,
57 | onSecondary = md_theme_light_onSecondary,
58 | secondaryContainer = md_theme_light_secondaryContainer,
59 | onSecondaryContainer = md_theme_light_onSecondaryContainer,
60 | tertiary = md_theme_light_tertiary,
61 | onTertiary = md_theme_light_onTertiary,
62 | tertiaryContainer = md_theme_light_tertiaryContainer,
63 | onTertiaryContainer = md_theme_light_onTertiaryContainer,
64 | error = md_theme_light_error,
65 | errorContainer = md_theme_light_errorContainer,
66 | onError = md_theme_light_onError,
67 | onErrorContainer = md_theme_light_onErrorContainer,
68 | background = md_theme_light_background,
69 | onBackground = md_theme_light_onBackground,
70 | surface = md_theme_light_surface,
71 | onSurface = md_theme_light_onSurface,
72 | surfaceVariant = md_theme_light_surfaceVariant,
73 | onSurfaceVariant = md_theme_light_onSurfaceVariant,
74 | outline = md_theme_light_outline,
75 | inverseOnSurface = md_theme_light_inverseOnSurface,
76 | inverseSurface = md_theme_light_inverseSurface,
77 | inversePrimary = md_theme_light_inversePrimary,
78 | surfaceTint = md_theme_light_surfaceTint,
79 | outlineVariant = md_theme_light_outlineVariant,
80 | scrim = md_theme_light_scrim,
81 | )
82 |
83 |
84 | @Composable
85 | fun PracticalComposeTheme(
86 | darkTheme: Boolean = isSystemInDarkTheme(),
87 | // Dynamic color is available on Android 12+
88 | dynamicColor: Boolean = true,
89 | content: @Composable () -> Unit
90 | ) {
91 | val colorScheme = when {
92 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
93 | val context = LocalContext.current
94 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
95 | }
96 |
97 | darkTheme -> DarkColorScheme
98 | else -> LightColorScheme
99 | }
100 | val view = LocalView.current
101 | if (!view.isInEditMode) {
102 | SideEffect {
103 | val window = (view.context as Activity).window
104 | window.statusBarColor = colorScheme.primary.toArgb()
105 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
106 | }
107 | }
108 |
109 | MaterialTheme(
110 | colorScheme = colorScheme,
111 | typography = Typography,
112 | content = content
113 | )
114 | }
--------------------------------------------------------------------------------
/settings/src/main/java/com/compose/settings/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings.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 | )
--------------------------------------------------------------------------------
/settings/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/settings/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 |
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/settings/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manishkaushik900/PracticalCompose/f378a0a8a34c7a31aa82f8381efd9ca996563001/settings/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/settings/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/settings/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | settings
3 |
4 | Light
5 | Dark
6 | System
7 |
8 | Go back to previous screen
9 |
10 | Settings
11 |
12 | Notifications Enabled
13 | Notifications Disabled
14 | Enable Notifications
15 |
16 |
17 | Hints Enabled
18 | Hints Disabled
19 | Show Hints
20 |
21 | Manage Subscription
22 | Open Subscription Management
23 |
24 | Receive marketing emails?
25 |
26 | - Opt-in for marketing emails
27 | - Don\'t send me emails
28 |
29 |
30 | Theme
31 | Select theme
32 |
33 | App Version
34 | v1.1
35 |
36 |
--------------------------------------------------------------------------------
/settings/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/settings/src/test/java/com/compose/settings/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.compose.settings
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------