├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── androidTestResultsUserPreferences.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── README.md ├── android-library-build.gradle ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── majid2851 │ │ └── kotlin_modularization │ │ └── datainfo │ │ ├── CustomTestRunner.kt │ │ ├── coil │ │ └── FakeImageLoader.kt │ │ └── ui │ │ └── HeroListEndToEnd.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── majid2851 │ │ │ └── kotlin_modularization │ │ │ ├── MainActivity.kt │ │ │ ├── di │ │ │ ├── CoilModule.kt │ │ │ └── HeroInteractorsModule.kt │ │ │ └── ui │ │ │ ├── BaseApplication.kt │ │ │ ├── navigation │ │ │ └── Screen.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── black_background.png │ │ ├── error_image.png │ │ ├── ic_launcher_background.xml │ │ └── white_background.png │ │ ├── font │ │ ├── quicksand_bold.ttf │ │ ├── quicksand_light.ttf │ │ ├── quicksand_medium.ttf │ │ └── quicksand_regular.ttf │ │ ├── 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 │ └── majid2851 │ └── kotlin_modularization │ └── ExampleUnitTest.kt ├── build.gradle ├── buildSrc ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ ├── Accompanist.kt │ ├── Android.kt │ ├── AndroidX.kt │ ├── Build.kt │ ├── Coil.kt │ ├── Compose.kt │ ├── Google.kt │ ├── Hilt.kt │ ├── Junit.kt │ ├── Kotlin.kt │ ├── KotlinPlugins.kt │ ├── Kotlinx.kt │ ├── Ktor.kt │ ├── Modules.kt │ └── SqlDelight.kt ├── component ├── .gitignore └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── majid2851 │ │ └── component │ │ └── ExampleInstrumentedTest.kt │ ├── build.gradle.kts │ ├── main │ └── AndroidManifest.xml │ └── test │ └── java │ └── com │ └── majid2851 │ └── component │ └── ExampleUnitTest.kt ├── constants ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── majid2851 │ └── constants │ └── Placeholder.kt ├── core ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── majid2851 │ └── core │ ├── DataState.kt │ ├── FilterOrder.kt │ ├── Logger.kt │ ├── ProgressBarState.kt │ ├── Queue.kt │ ├── UIComponent.kt │ └── UIComponentState.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hero ├── hero-datasource-test │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── majid2851 │ │ └── hero_datasource_test │ │ ├── Util.kt │ │ ├── cache │ │ ├── HeroCacheFake.kt │ │ └── HeroDatabaseFake.kt │ │ └── network │ │ ├── HeroServiceFake.kt │ │ ├── HeroServiceResponseType.kt │ │ └── data │ │ ├── HeroDataMalformed.kt │ │ ├── HeroDataValid.kt │ │ └── HeroEmptyData.kt ├── hero-datasource │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── majid2851 │ │ │ └── hero_datasource │ │ │ ├── cache │ │ │ ├── HeroCache.kt │ │ │ ├── HeroCacheImpl.kt │ │ │ └── HeroEntity.kt │ │ │ └── network │ │ │ ├── EndPoints.kt │ │ │ ├── HeroDto.kt │ │ │ ├── HeroService.kt │ │ │ └── HeroServiceImpl.kt │ │ └── sqldelight │ │ └── com │ │ └── majid2851 │ │ └── hero_datasource │ │ └── cache │ │ └── heroDb.sq ├── hero-domain │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── majid2851 │ │ └── hero_domain │ │ ├── Hero.kt │ │ ├── HeroAttackType.kt │ │ ├── HeroAttribute.kt │ │ ├── HeroFilter.kt │ │ └── HeroRole.kt ├── hero-interactors │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── majid2851 │ │ │ └── hero_interactors │ │ │ ├── FilterHeros.kt │ │ │ ├── GetHeroFromCache.kt │ │ │ ├── GetHeros.kt │ │ │ └── HeroInteractors.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── majid2851 │ │ └── hero_interactors │ │ └── GetHerosTest.kt ├── ui-heroDetail │ ├── build.gradle │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── majid2851 │ │ │ └── ui_herodetail │ │ │ ├── coil │ │ │ └── FakeImageLoader.kt │ │ │ └── ui │ │ │ └── HeroDetailTest.kt │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── majid2851 │ │ │ └── ui_herodetail │ │ │ ├── di │ │ │ └── HeroDetailModule.kt │ │ │ └── ui │ │ │ ├── HeroDetail.kt │ │ │ ├── HeroDetailEvent.kt │ │ │ ├── HeroDetailState.kt │ │ │ └── HeroDetailViewModel.kt │ │ └── res │ │ ├── drawable │ │ ├── black_background.png │ │ ├── error_image.png │ │ └── white_background.png │ │ └── values │ │ └── strings.xml └── ui-heroList │ ├── build.gradle │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── majid2851 │ │ └── ui_herolist │ │ ├── coil │ │ └── FakeImageLoader.kt │ │ └── ui │ │ └── HeroListTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── majid2851 │ │ └── ui_herolist │ │ ├── components │ │ ├── EmptyRow.kt │ │ ├── HeroListFilter.kt │ │ ├── HeroListItem.kt │ │ ├── HeroListToolbar.kt │ │ └── OrderSelector.kt │ │ ├── di │ │ └── HeroListModule.kt │ │ └── ui │ │ ├── HeroList.kt │ │ ├── HeroListEvents.kt │ │ ├── HeroListState.kt │ │ ├── HeroListViewModel.kt │ │ └── test │ │ └── TestTag.kt │ └── res │ ├── drawable │ ├── black_background.png │ ├── error_image.png │ └── white_background.png │ └── values │ └── strings.xml ├── library-build.gradle └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '11' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.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/androidTestResultsUserPreferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 62 | 63 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin_Modularization 2 | 3 | Project Name 4 | This project is a Kotlin-based Android application built using Clean Architecture, MVI, and modularization to create an offline-first experience. It uses Jetpack Compose for UI rendering, Ktor for network communication, SQL Delight for caching, and Coil for image caching. The project also includes unit and UI tests, as well as Hilt for dependency injection. 5 | 6 | Modularization will: 7 | Decrease build times. 8 | Make it easier to delegate work. 9 | Increase code reusability. 10 | Overall make testing easier and more clear. 11 | 12 | 13 | 14 | #Features 15 | Kotlin 16 | 17 | Clean Architecture 18 | 19 | MVI 20 | 21 | Multi-module 22 | 23 | Compose 24 | 25 | Ktor (Network) 26 | 27 | SQL Delight (Caching) 28 | 29 | Coil (Image Caching) 30 | 31 | Unit Tests 32 | 33 | UI Testing with Compose 34 | 35 | Hilt Dependency Injection 36 | 37 | Testing with Hilt 38 | 39 | Building an "offline first" application 40 | 41 | 42 | 43 | Installation 44 | To use this project, you will need to have Android Studio installed on your machine. Follow these steps to install the project: 45 | 46 | Clone the repository from Github using git clone https://github.com/majid2851/Kotlin_Modularization.git 47 | Open the project in Android Studio 48 | Build and run the project using the Android emulator or a connected device 49 | Usage 50 | The app is designed to showcase an offline-first experience. It loads data from the network when available, and saves it to a local cache for offline use. The app includes a list view and detail view to demonstrate basic functionality. 51 | 52 | #Testing 53 | The project includes both unit and UI tests. To run the tests, use the following steps: 54 | 55 | Open the project in Android Studio 56 | Right-click on the desired test folder (e.g. "src/test") and select "Run tests in 'test'" 57 | Dependencies 58 | The project uses several open-source libraries, including: 59 | 60 | Jetpack Compose 61 | Ktor 62 | SQL Delight 63 | Coil 64 | Hilt 65 | Contributing 66 | If you would like to contribute to this project, please fork the repository and create a pull request. We welcome contributions of all kinds, including bug fixes, new features, and documentation improvements. 67 | 68 | 69 | ![Screenshot_1683359826](https://user-images.githubusercontent.com/46685643/236614321-000dfe74-1303-49a6-b787-7e8032ebf78f.png) 70 | ![Screenshot_1683359856](https://user-images.githubusercontent.com/46685643/236614332-2c018892-6f12-49ba-aabc-f9267ce8a475.png) 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /android-library-build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 33 8 | 9 | defaultConfig { 10 | minSdk 24 11 | targetSdk 33 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | vectorDrawables { 17 | useSupportLibrary true 18 | } 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | } 31 | kotlinOptions { 32 | jvmTarget = '1.8' 33 | } 34 | buildFeatures { 35 | compose true 36 | } 37 | composeOptions { 38 | kotlinCompilerExtensionVersion '1.3.2' 39 | } 40 | packagingOptions { 41 | resources { 42 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 43 | } 44 | } 45 | } 46 | 47 | dependencies { 48 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20") 49 | 50 | 51 | implementation 'androidx.core:core-ktx:1.10.0' 52 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 53 | implementation 'androidx.activity:activity-compose:1.7.1' 54 | implementation platform('androidx.compose:compose-bom:2022.10.00') 55 | implementation 'androidx.compose.ui:ui' 56 | implementation 'androidx.compose.ui:ui-graphics' 57 | implementation 'androidx.compose.ui:ui-tooling-preview' 58 | implementation 'androidx.compose.material3:material3' 59 | implementation 'com.google.android.material:material:1.8.0' 60 | 61 | 62 | testImplementation 'junit:junit:4.13.2' 63 | 64 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 66 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') 67 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4' 68 | 69 | debugImplementation 'androidx.compose.ui:ui-tooling' 70 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 71 | 72 | 73 | 74 | 75 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.execution.Program 2 | 3 | plugins { 4 | id ("com.android.application") 5 | id ("org.jetbrains.kotlin.android") 6 | id ("kotlin-kapt") 7 | 8 | id("com.google.dagger.hilt.android") 9 | 10 | } 11 | 12 | android { 13 | namespace 'com.majid2851.kotlin_modularization' 14 | compileSdk 33 15 | 16 | defaultConfig { 17 | applicationId "com.majid2851.kotlin_modularization" 18 | minSdk 24 19 | targetSdk 33 20 | versionCode 1 21 | versionName "1.0" 22 | 23 | testInstrumentationRunner "com.majid2851.kotlin_modularization.datainfo.CustomTestRunner" 24 | vectorDrawables { 25 | useSupportLibrary true 26 | } 27 | } 28 | 29 | buildTypes { 30 | release { 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | compileOptions { 36 | sourceCompatibility JavaVersion.VERSION_1_8 37 | targetCompatibility JavaVersion.VERSION_1_8 38 | } 39 | kotlinOptions { 40 | jvmTarget = '1.8' 41 | } 42 | buildFeatures { 43 | compose true 44 | } 45 | 46 | composeOptions { 47 | kotlinCompilerExtensionVersion '1.3.2' 48 | } 49 | packagingOptions { 50 | resources { 51 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 52 | } 53 | } 54 | } 55 | 56 | dependencies { 57 | implementation(project(Modules.core)) 58 | implementation(project(Modules.heroDomain)) 59 | implementation(project(Modules.heroDataSource)) 60 | implementation(project(Modules.heroInteractors)) 61 | implementation(project(Modules.ui_heroList)) 62 | implementation(project(Modules.ui_heroDetail)) 63 | 64 | implementation(Coil.coil) 65 | 66 | implementation (SqlDelight.androidDriver) 67 | 68 | implementation(Hilt.android) 69 | kapt(Hilt.compiler) 70 | 71 | implementation(Accompanist.animations) 72 | 73 | androidTestImplementation(project(Modules.heroDataSourceTest)) 74 | androidTestImplementation(AndroidXTest.runner) 75 | androidTestImplementation(ComposeTest.uiTestJunit4) 76 | androidTestImplementation(HiltTest.hiltAndroidTesting) 77 | kaptAndroidTest(Hilt.compiler) 78 | androidTestImplementation(Junit.junit4) 79 | 80 | 81 | 82 | implementation("androidx.navigation:navigation-compose:2.5.3") 83 | implementation("androidx.hilt:hilt-navigation-compose:1.0.0") 84 | 85 | 86 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20") 87 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 88 | implementation 'androidx.activity:activity-compose:1.7.1' 89 | implementation platform('androidx.compose:compose-bom:2023.04.01') 90 | implementation 'androidx.compose.ui:ui' 91 | implementation 'androidx.compose.ui:ui-graphics' 92 | implementation 'androidx.compose.ui:ui-tooling-preview' 93 | implementation 'androidx.compose.material3:material3' 94 | implementation 'com.google.android.material:material:1.8.0' 95 | implementation 'androidx.core:core-ktx:1.10.0' 96 | implementation platform('androidx.compose:compose-bom:2022.10.00') 97 | 98 | 99 | testImplementation 'junit:junit:4.13.2' 100 | 101 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 102 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 103 | androidTestImplementation platform('androidx.compose:compose-bom:2023.04.01') 104 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4' 105 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') 106 | 107 | debugImplementation 'androidx.compose.ui:ui-tooling' 108 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 109 | 110 | 111 | 112 | 113 | } -------------------------------------------------------------------------------- /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/majid2851/kotlin_modularization/datainfo/CustomTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.datainfo 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import dagger.hilt.android.testing.HiltTestApplication 7 | 8 | class CustomTestRunner : AndroidJUnitRunner() { 9 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { 10 | return super.newApplication(cl, HiltTestApplication::class.java.name, context) 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/majid2851/kotlin_modularization/datainfo/coil/FakeImageLoader.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.datainfo.coil 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.graphics.drawable.ColorDrawable 6 | import coil.ImageLoader 7 | import coil.annotation.ExperimentalCoilApi 8 | import coil.bitmap.BitmapPool 9 | import coil.decode.DataSource 10 | import coil.memory.MemoryCache 11 | import coil.request.* 12 | 13 | class FakeImageLoader { 14 | companion object Factory { 15 | fun build(context: Context): ImageLoader { 16 | return object : ImageLoader { 17 | 18 | private val disposable = object : Disposable { 19 | override val isDisposed get() = true 20 | 21 | @ExperimentalCoilApi 22 | override suspend fun await() { 23 | 24 | } 25 | 26 | override fun dispose() {} 27 | } 28 | 29 | override val defaults = DefaultRequestOptions() 30 | 31 | // Optionally, you can add a custom fake memory cache implementation. 32 | override val memoryCache get() = throw UnsupportedOperationException() 33 | 34 | override val bitmapPool = BitmapPool(0) 35 | 36 | override fun enqueue(request: ImageRequest): Disposable { 37 | // Always call onStart before onSuccess. 38 | request.target?.onStart(placeholder = ColorDrawable(Color.BLACK)) 39 | request.target?.onSuccess(result = ColorDrawable(Color.BLACK)) 40 | return disposable 41 | } 42 | 43 | override suspend fun execute(request: ImageRequest): ImageResult { 44 | return SuccessResult( 45 | drawable = ColorDrawable(Color.BLACK), 46 | request = request, 47 | metadata = ImageResult.Metadata( 48 | memoryCacheKey = MemoryCache.Key(""), 49 | isSampled = false, 50 | dataSource = DataSource.MEMORY_CACHE, 51 | isPlaceholderMemoryCacheKeyPresent = false 52 | ) 53 | ) 54 | } 55 | 56 | override fun shutdown() {} 57 | 58 | override fun newBuilder() = ImageLoader.Builder(context) 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/majid2851/kotlin_modularization/datainfo/ui/HeroListEndToEnd.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.datainfo.ui 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.ui.ExperimentalComposeUiApi 5 | import androidx.compose.ui.test.* 6 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 7 | import androidx.hilt.navigation.compose.hiltViewModel 8 | import androidx.navigation.compose.NavHost 9 | import androidx.navigation.compose.composable 10 | import androidx.navigation.compose.rememberNavController 11 | import androidx.test.platform.app.InstrumentationRegistry 12 | import coil.ImageLoader 13 | import com.majid2851.hero_datasource.cache.HeroCache 14 | import com.majid2851.hero_datasource.network.HeroService 15 | import com.majid2851.hero_datasource_test.cache.HeroCacheFake 16 | import com.majid2851.hero_datasource_test.cache.HeroDatabaseFake 17 | import com.majid2851.hero_datasource_test.network.HeroServiceFake 18 | import com.majid2851.hero_datasource_test.network.HeroServiceResponseType 19 | import com.majid2851.hero_domain.HeroAttribute 20 | import com.majid2851.hero_interactors.FilterHeros 21 | import com.majid2851.hero_interactors.GetHeroFromCache 22 | import com.majid2851.hero_interactors.GetHeros 23 | import com.majid2851.hero_interactors.HeroInteractors 24 | import com.majid2851.kotlin_modularization.MainActivity 25 | import com.majid2851.kotlin_modularization.datainfo.coil.FakeImageLoader 26 | import com.majid2851.kotlin_modularization.di.HeroInteractorsModule 27 | import com.majid2851.kotlin_modularization.ui.navigation.Screen 28 | import com.majid2851.kotlin_modularization.ui.theme.DotaInfoTheme 29 | import com.majid2851.ui_herodetail.ui.HeroDetail 30 | import com.majid2851.ui_herodetail.ui.HeroDetailViewModel 31 | import com.majid2851.ui_herolist.ui.HeroList 32 | import com.majid2851.ui_herolist.ui.HeroListViewModel 33 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_AGILITY_CHECKBOX 34 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_ASC 35 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_BTN 36 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_DESC 37 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_DIALOG 38 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_DIALOG_DONE 39 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_HERO_CHECKBOX 40 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_INT_CHECKBOX 41 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_PROWINS_CHECKBOX 42 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_STENGTH_CHECKBOX 43 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_NAME 44 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_PRIMARY_ATTRIBUTE 45 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_SEARCH_BAR 46 | import dagger.Module 47 | import dagger.Provides 48 | import dagger.hilt.InstallIn 49 | import dagger.hilt.android.testing.HiltAndroidRule 50 | import dagger.hilt.android.testing.HiltAndroidTest 51 | import dagger.hilt.android.testing.UninstallModules 52 | import dagger.hilt.components.SingletonComponent 53 | import org.junit.Before 54 | import org.junit.Rule 55 | import org.junit.Test 56 | import javax.inject.Singleton 57 | 58 | @UninstallModules(HeroInteractorsModule::class) 59 | @HiltAndroidTest 60 | class HeroListEndToEnd 61 | { 62 | @Module 63 | @InstallIn(SingletonComponent::class) 64 | object TestHeroInteractorsModule{ 65 | 66 | @Provides 67 | @Singleton 68 | fun provideHeroCache(): HeroCache { 69 | return HeroCacheFake(HeroDatabaseFake()) 70 | } 71 | 72 | @Provides 73 | @Singleton 74 | fun provideHeroService(): HeroService { 75 | return HeroServiceFake().build( 76 | type= HeroServiceResponseType.GoodData 77 | ) 78 | } 79 | 80 | @Provides 81 | @Singleton 82 | fun provideHeroInteractors( 83 | cache: HeroCache, 84 | service: HeroService 85 | ): HeroInteractors { 86 | return HeroInteractors( 87 | getHeros = GetHeros( 88 | heroCache = cache, 89 | service = service, 90 | ), 91 | filterHeros = FilterHeros(), 92 | getHeroFromCache = GetHeroFromCache( 93 | cache = cache, 94 | ), 95 | ) 96 | } 97 | } 98 | 99 | 100 | @get:Rule(order = 0) 101 | var hiltRule = HiltAndroidRule(this) 102 | 103 | @get:Rule(order = 1) 104 | val composeTestRule = createAndroidComposeRule() 105 | 106 | private val context = InstrumentationRegistry.getInstrumentation().targetContext 107 | private val imageLoader: ImageLoader = FakeImageLoader.build(context) 108 | 109 | @Before 110 | fun before(){ 111 | composeTestRule.setContent { 112 | DotaInfoTheme { 113 | val navController = rememberNavController() 114 | NavHost( 115 | navController = navController, 116 | startDestination = Screen.HeroList.route, 117 | builder = { 118 | composable( 119 | route = Screen.HeroList.route, 120 | ){ 121 | val viewModel: HeroListViewModel = hiltViewModel() 122 | HeroList( 123 | state = viewModel.state.value, 124 | events = viewModel::onTrigerEvent, 125 | navigateToDetailScreen = { heroId -> 126 | navController.navigate("${Screen.HeroDetailList.route}/$heroId") 127 | }, 128 | imageLoader = imageLoader, 129 | ) 130 | } 131 | composable( 132 | route = Screen.HeroDetailList.route + "/{heroId}", 133 | arguments = Screen.HeroDetailList.arguments, 134 | ){ 135 | val viewModel: HeroDetailViewModel = hiltViewModel() 136 | HeroDetail( 137 | state = viewModel.state.value, 138 | event = viewModel::onTriggerEvent, 139 | imageLoader = imageLoader 140 | ) 141 | } 142 | } 143 | ) 144 | } 145 | } 146 | } 147 | 148 | @Test 149 | fun testSearchHeroByName(){ 150 | composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG") // For learning the ui tree system 151 | 152 | composeTestRule.onNodeWithTag(TAG_HERO_SEARCH_BAR).performTextInput("Anti-Mage") 153 | composeTestRule.onNodeWithTag(TAG_HERO_NAME, useUnmergedTree = true).assertTextEquals( 154 | "Anti-Mage", 155 | ) 156 | composeTestRule.onNodeWithTag(TAG_HERO_SEARCH_BAR).performTextClearance() 157 | 158 | composeTestRule.onNodeWithTag(TAG_HERO_SEARCH_BAR).performTextInput("Storm Spirit") 159 | composeTestRule.onNodeWithTag(TAG_HERO_NAME, useUnmergedTree = true).assertTextEquals( 160 | "Storm Spirit", 161 | ) 162 | composeTestRule.onNodeWithTag(TAG_HERO_SEARCH_BAR).performTextClearance() 163 | 164 | composeTestRule.onNodeWithTag(TAG_HERO_SEARCH_BAR).performTextInput("Mirana") 165 | composeTestRule.onNodeWithTag(TAG_HERO_NAME, useUnmergedTree = true).assertTextEquals( 166 | "Mirana", 167 | ) 168 | } 169 | 170 | @Test 171 | fun testFilterHeroAlphabetically(){ 172 | // Show the dialog 173 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_BTN).performClick() 174 | 175 | // Confirm the filter dialog is showing 176 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG).assertIsDisplayed() 177 | 178 | // Filter by "Hero" name 179 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_HERO_CHECKBOX).performClick() 180 | 181 | // Order Descending (z-a) 182 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DESC).performClick() 183 | 184 | // Close the dialog 185 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG_DONE).performClick() 186 | 187 | // Confirm the order is correct 188 | composeTestRule.onAllNodesWithTag(TAG_HERO_NAME, useUnmergedTree = true).assertAny(hasText("Zeus")) 189 | 190 | // Show the dialog 191 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_BTN).performClick() 192 | 193 | // Order Ascending (a-z) 194 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_ASC).performClick() 195 | 196 | // Close the dialog 197 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG_DONE).performClick() 198 | 199 | // Confirm the order is correct 200 | composeTestRule.onAllNodesWithTag(TAG_HERO_NAME, useUnmergedTree = true).assertAny(hasText("Abaddon")) 201 | } 202 | 203 | @Test 204 | fun testFilterHeroByProWins(){ 205 | // Show the dialog 206 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_BTN).performClick() 207 | 208 | // Confirm the filter dialog is showing 209 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG).assertIsDisplayed() 210 | 211 | // Filter by ProWin % 212 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_PROWINS_CHECKBOX).performClick() 213 | 214 | // Order Descending (100% - 0%) 215 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DESC).performClick() 216 | 217 | // Close the dialog 218 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG_DONE).performClick() 219 | 220 | // Confirm the order is correct 221 | composeTestRule.onAllNodesWithTag(TAG_HERO_NAME, useUnmergedTree = true).assertAny(hasText("Chen")) 222 | 223 | // Show the dialog 224 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_BTN).performClick() 225 | 226 | // Order Ascending (0% - 100%) 227 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_ASC).performClick() 228 | 229 | // Close the dialog 230 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG_DONE).performClick() 231 | 232 | // Confirm the order is correct 233 | composeTestRule.onAllNodesWithTag(TAG_HERO_NAME, useUnmergedTree = true).assertAny(hasText("Dawnbreaker")) 234 | } 235 | 236 | @Test 237 | fun testFilterHeroByStrength(){ 238 | // Show the dialog 239 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_BTN).performClick() 240 | 241 | // Confirm the filter dialog is showing 242 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG).assertIsDisplayed() 243 | 244 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_STENGTH_CHECKBOX).performClick() 245 | 246 | // Close the dialog 247 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG_DONE).performClick() 248 | 249 | // Confirm that only STRENGTH heros are showing 250 | composeTestRule.onAllNodesWithTag(TAG_HERO_PRIMARY_ATTRIBUTE, useUnmergedTree = true) 251 | .assertAll(hasText(HeroAttribute.Strength.uiValue)) 252 | } 253 | 254 | @Test 255 | fun testFilterHeroByAgility(){ 256 | // Show the dialog 257 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_BTN).performClick() 258 | 259 | // Confirm the filter dialog is showing 260 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG).assertIsDisplayed() 261 | 262 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_AGILITY_CHECKBOX).performClick() 263 | 264 | // Close the dialog 265 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG_DONE).performClick() 266 | 267 | // Confirm that only STRENGTH heros are showing 268 | composeTestRule.onAllNodesWithTag(TAG_HERO_PRIMARY_ATTRIBUTE, useUnmergedTree = true) 269 | .assertAll(hasText(HeroAttribute.Agility.uiValue)) 270 | } 271 | 272 | @Test 273 | fun testFilterHeroByIntelligence(){ 274 | // Show the dialog 275 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_BTN).performClick() 276 | 277 | // Confirm the filter dialog is showing 278 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG).assertIsDisplayed() 279 | 280 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_INT_CHECKBOX).performClick() 281 | 282 | // Close the dialog 283 | composeTestRule.onNodeWithTag(TAG_HERO_FILTER_DIALOG_DONE).performClick() 284 | 285 | // Confirm that only STRENGTH heros are showing 286 | composeTestRule.onAllNodesWithTag(TAG_HERO_PRIMARY_ATTRIBUTE, useUnmergedTree = true).assertAll(hasText( 287 | HeroAttribute.Intelligence.uiValue)) 288 | } 289 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/MainActivity.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalAnimationApi::class) 2 | 3 | package com.majid2851.kotlin_modularization 4 | 5 | import android.os.Bundle 6 | import android.util.Log 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | import androidx.compose.animation.ExperimentalAnimationApi 10 | import androidx.compose.animation.core.FastOutSlowInEasing 11 | import androidx.compose.animation.core.tween 12 | import androidx.compose.animation.fadeIn 13 | import androidx.compose.animation.fadeOut 14 | import androidx.compose.animation.slideInHorizontally 15 | import androidx.compose.animation.slideOutHorizontally 16 | import androidx.hilt.navigation.compose.hiltViewModel 17 | import androidx.interpolator.view.animation.FastOutSlowInInterpolator 18 | import androidx.navigation.NavGraphBuilder 19 | import androidx.navigation.NavHostController 20 | import androidx.navigation.compose.NavHost 21 | import androidx.navigation.compose.composable 22 | import androidx.navigation.compose.rememberNavController 23 | import coil.ImageLoader 24 | import com.google.accompanist.navigation.animation.composable 25 | import com.google.accompanist.navigation.animation.rememberAnimatedNavController 26 | import com.majid2851.kotlin_modularization.ui.navigation.Screen 27 | import com.majid2851.kotlin_modularization.ui.theme.DotaInfoTheme 28 | import com.majid2851.ui_herodetail.ui.HeroDetail 29 | import com.majid2851.ui_herodetail.ui.HeroDetailViewModel 30 | import com.majid2851.ui_herolist.ui.HeroList 31 | import com.majid2851.ui_herolist.ui.HeroListViewModel 32 | import dagger.hilt.android.AndroidEntryPoint 33 | import javax.inject.Inject 34 | 35 | @OptIn(ExperimentalAnimationApi::class) 36 | @AndroidEntryPoint 37 | class MainActivity : ComponentActivity() 38 | { 39 | @Inject 40 | lateinit var imageLoader:ImageLoader 41 | 42 | override fun onCreate(savedInstanceState: Bundle?) 43 | { 44 | super.onCreate(savedInstanceState) 45 | 46 | setContent { 47 | DotaInfoTheme() 48 | { 49 | val navController= rememberNavController() 50 | NavHost( 51 | navController = navController, 52 | startDestination = Screen.HeroList.route, 53 | builder = { 54 | addHeroList( 55 | navController = navController, 56 | imageLoader=imageLoader 57 | ) 58 | addHeroDetail(imageLoader=imageLoader) 59 | 60 | }) 61 | 62 | } 63 | } 64 | } 65 | 66 | 67 | 68 | } 69 | 70 | fun NavGraphBuilder.addHeroDetail(imageLoader:ImageLoader) { 71 | composable( 72 | route = Screen.HeroDetailList.route + "/{heroId}", 73 | arguments = Screen.HeroDetailList.arguments, 74 | // enterTransition={_,_ -> 75 | // slideInHorizontally ( 76 | // initialOffsetX = {300}, 77 | // animationSpec = tween( 78 | // durationMillis = 300, 79 | // easing = FastOutSlowInEasing, 80 | // ) 81 | // ) + fadeIn(animationSpec = tween(300)) 82 | // }, 83 | // popExitTransition = {_,_ -> 84 | // slideOutHorizontally( 85 | // targetOffsetX = {300}, 86 | // animationSpec = tween( 87 | // durationMillis = 300, 88 | // easing = FastOutSlowInEasing, 89 | // ) 90 | // ) + fadeOut(animationSpec = tween(300)) 91 | // } 92 | ) 93 | { 94 | val viewModel:HeroDetailViewModel = hiltViewModel() 95 | HeroDetail( 96 | state = viewModel.state.value, 97 | imageLoader = imageLoader, 98 | event = viewModel::onTriggerEvent 99 | ) 100 | } 101 | } 102 | 103 | 104 | fun NavGraphBuilder.addHeroList( 105 | navController: NavHostController, 106 | imageLoader: ImageLoader 107 | ) { 108 | composable( 109 | route = Screen.HeroList.route, 110 | // exitTransition = {_,_-> 111 | // slideOutHorizontally( 112 | // targetOffsetX = {-300}, 113 | // animationSpec = tween( 114 | // durationMillis = 300, 115 | // easing = FastOutSlowInEasing, 116 | // ) 117 | // ) + fadeOut(animationSpec = tween(300)) 118 | // }, popEnterTransition = {_,_ -> 119 | // slideInHorizontally ( 120 | // initialOffsetX = {-300}, 121 | // animationSpec = tween( 122 | // durationMillis = 300, 123 | // easing = FastOutSlowInEasing, 124 | // ) 125 | // ) + fadeIn(animationSpec = tween(300)) 126 | // } 127 | 128 | ) 129 | { 130 | val viewModel: HeroListViewModel = hiltViewModel() 131 | 132 | Log.i("Sam2231-MainActivity==>","size:"+viewModel.state.value.filterHeros.size.toString()) 133 | HeroList( 134 | state = viewModel.state.value, 135 | events=viewModel::onTrigerEvent, 136 | imageLoader = imageLoader, 137 | navigateToDetailScreen = { heroId -> 138 | navController.navigate( 139 | "${Screen.HeroDetailList.route}/$heroId" 140 | ) 141 | } 142 | ) 143 | } 144 | } 145 | 146 | -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/di/CoilModule.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.di 2 | 3 | import android.app.Application 4 | import coil.ImageLoader 5 | import com.majid2851.kotlin_modularization.R 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object CoilModule 15 | { 16 | @Provides 17 | @Singleton 18 | fun provideImageLoader(app:Application):ImageLoader 19 | { 20 | return ImageLoader.Builder(app) 21 | .error(R.drawable.error_image) 22 | .placeholder(R.drawable.white_background) 23 | .crossfade(true) 24 | .build() 25 | 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/di/HeroInteractorsModule.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.di 2 | 3 | import android.app.Application 4 | import com.majid2851.hero_interactors.HeroInteractors 5 | import com.squareup.sqldelight.android.AndroidSqliteDriver 6 | import com.squareup.sqldelight.db.SqlDriver 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import javax.inject.Named 12 | import javax.inject.Singleton 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | object HeroInteractorsModule 17 | { 18 | const val DBInjectedName="heroAndroidSqlDriver" 19 | @Provides 20 | @Singleton 21 | @Named(DBInjectedName)//when you have multiple database you can give name to them 22 | fun provideAndroidDriver(app:Application):SqlDriver 23 | { 24 | return AndroidSqliteDriver( 25 | schema = HeroInteractors.schema, 26 | context = app, 27 | name = HeroInteractors.dbName 28 | ) 29 | } 30 | 31 | @Provides 32 | @Singleton 33 | fun provideHeroInteractors( 34 | @Named(DBInjectedName) sqlDriver: SqlDriver, 35 | ):HeroInteractors{ 36 | return HeroInteractors.build( 37 | sqlDriver=sqlDriver 38 | ) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/ui/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.ui 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class BaseApplication:Application() 8 | { 9 | 10 | 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/ui/navigation/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.ui.navigation 2 | 3 | import androidx.navigation.NamedNavArgument 4 | import androidx.navigation.NavType 5 | import androidx.navigation.navArgument 6 | 7 | sealed class Screen( 8 | val route:String, 9 | val arguments:List 10 | ) 11 | { 12 | object HeroList:Screen( 13 | route="heroList", 14 | arguments = emptyList() 15 | ) 16 | 17 | object HeroDetailList:Screen( 18 | route = "heroDetail", 19 | arguments = listOf( 20 | navArgument("heroId"){ 21 | type= NavType.IntType 22 | } 23 | ) 24 | ) 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Blue300 = Color(0xFF64B5F6) 6 | val Blue400 = Color(0xFF42A5F5) 7 | val Blue500 = Color(0xFF2196F3) 8 | val Blue600 = Color(0xFF1E88E5) 9 | val Blue700 = Color(0xFF1976D2) 10 | val Blue800 = Color(0xFF1565C0) 11 | 12 | val WHITE=Color(0xFFFFFFFF) 13 | 14 | val Teal300 = Color(0xFF1AC6FF) 15 | 16 | val GreenCheck = Color(0xFF009a34) 17 | 18 | val Grey1 = Color(0xFFF2F2F2) 19 | 20 | val Black1 = Color(0xFF222222) 21 | 22 | val RedErrorDark = Color(0xFFB00020) 23 | val RedErrorLight = Color(0xFFEF5350) -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val AppShapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(6.dp), 10 | large = RoundedCornerShape(8.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.lightColorScheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Color 9 | 10 | private val DarkColorPalette = darkColorScheme( 11 | primary = Blue300, 12 | primaryContainer = Blue700, 13 | onPrimary = Color.White, 14 | secondary = Color.Black, 15 | secondaryContainer = Teal300, 16 | onSecondary = Color.White, 17 | error = RedErrorLight, 18 | onError = RedErrorDark, 19 | background = Color.Black, 20 | onBackground = Color.White, 21 | surface = Black1, 22 | onSurface = Color.White, 23 | ) 24 | 25 | private val LightColorPalette = lightColorScheme( 26 | primary = Blue600, 27 | primaryContainer = Blue400, 28 | onPrimary = Black1, 29 | secondary = Color.White, 30 | secondaryContainer = Teal300, 31 | onSecondary = Color.Black, 32 | error = RedErrorDark, 33 | onError = RedErrorLight, 34 | background = Grey1, 35 | onBackground = Color.Black, 36 | surface = Color.White, 37 | onSurface = Black1, 38 | ) 39 | 40 | @Composable 41 | fun DotaInfoTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { 42 | val colors = if(darkTheme){ 43 | DarkColorPalette 44 | } else{ 45 | LightColorPalette 46 | } 47 | 48 | MaterialTheme( 49 | colorScheme = colors, 50 | typography = QuickSandTypography, 51 | shapes = AppShapes, 52 | content = content 53 | ) 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/majid2851/kotlin_modularization/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.majid2851.kotlin_modularization.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.text.TextStyle 6 | import androidx.compose.ui.text.font.Font 7 | import androidx.compose.ui.text.font.FontFamily 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.unit.sp 10 | import com.majid2851.kotlin_modularization.R 11 | 12 | private val QuickSand = FontFamily( 13 | Font(R.font.quicksand_light, FontWeight.W300), 14 | Font(R.font.quicksand_regular, FontWeight.W400), 15 | Font(R.font.quicksand_medium, FontWeight.W500), 16 | Font(R.font.quicksand_bold, FontWeight.W600) 17 | ) 18 | 19 | val QuickSandTypography = Typography( 20 | titleLarge = TextStyle( 21 | fontFamily = QuickSand, 22 | fontWeight = FontWeight.W500, 23 | fontSize = 32.sp, 24 | ), 25 | titleMedium = TextStyle( 26 | fontFamily = QuickSand, 27 | fontWeight = FontWeight.W500, 28 | fontSize = 22.sp, 29 | ), 30 | titleSmall = TextStyle( 31 | fontFamily = QuickSand, 32 | fontWeight = FontWeight.W400, 33 | fontSize = 18.sp, 34 | ), 35 | labelLarge = TextStyle( 36 | fontFamily = QuickSand, 37 | fontWeight = FontWeight.W400, 38 | fontSize = 16.sp, 39 | ), 40 | labelMedium = TextStyle( 41 | fontFamily = QuickSand, 42 | fontWeight = FontWeight.W500, 43 | fontSize = 15.sp, 44 | ), 45 | labelSmall = TextStyle( 46 | fontFamily = QuickSand, 47 | fontWeight = FontWeight.W400, 48 | fontSize = 14.sp, 49 | ), 50 | bodyLarge = TextStyle( 51 | fontFamily = QuickSand, 52 | fontWeight = FontWeight.Normal, 53 | fontSize = 16.sp 54 | ), 55 | bodyMedium = TextStyle( 56 | fontFamily = QuickSand, 57 | fontSize = 14.sp 58 | ), 59 | headlineLarge = TextStyle( 60 | fontFamily = QuickSand, 61 | fontWeight = FontWeight.W400, 62 | fontSize = 16.sp, 63 | color = Color.White 64 | ), 65 | headlineMedium = TextStyle( 66 | fontFamily = QuickSand, 67 | fontWeight = FontWeight.Normal, 68 | fontSize = 13.sp 69 | ), 70 | headlineSmall = TextStyle( 71 | fontFamily = QuickSand, 72 | fontWeight = FontWeight.W400, 73 | fontSize = 13.sp 74 | ) 75 | ) -------------------------------------------------------------------------------- /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/black_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/drawable/black_background.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/error_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/drawable/error_image.png -------------------------------------------------------------------------------- /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/white_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/drawable/white_background.png -------------------------------------------------------------------------------- /app/src/main/res/font/quicksand_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/font/quicksand_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/quicksand_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/font/quicksand_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/quicksand_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/font/quicksand_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/quicksand_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/font/quicksand_regular.ttf -------------------------------------------------------------------------------- /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/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/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 | Kotlin_Modularization 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |