├── .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 |
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 |
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 | 
70 | 
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 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/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/majid2851/kotlin_modularization/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.kotlin_modularization
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 |
3 | buildscript {
4 |
5 | dependencies {
6 | classpath "com.google.dagger:hilt-android-gradle-plugin:$Hilt.hiltVersion"
7 | }
8 | }
9 |
10 | plugins {
11 | id 'com.android.application' version '7.4.2' apply false
12 | id 'com.android.library' version '7.4.2' apply false
13 | id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
14 | id 'org.jetbrains.kotlin.jvm' version '1.7.20' apply false
15 | // id 'plugin.serialization' version '1.7.20' apply false
16 |
17 | }
--------------------------------------------------------------------------------
/buildSrc/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | repositories{
2 | mavenCentral()
3 | }
4 |
5 |
6 | plugins{
7 | `kotlin-dsl`
8 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Accompanist.kt:
--------------------------------------------------------------------------------
1 | object Accompanist
2 | {
3 | private const val animationsVersion="0.16.0"
4 | const val animations="com.google.accompanist:accompanist-navigation-animation:$animationsVersion"
5 |
6 |
7 |
8 |
9 |
10 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Android.kt:
--------------------------------------------------------------------------------
1 | object Android {
2 | const val appId = "com.majid2851.kotlin_modularization"
3 | const val compileSdk = 33
4 | // const val buildTools = "30.0.3"
5 | const val minSdk = 24
6 | const val targetSdk = 33
7 | const val versionCode = 1
8 | const val versionName = "1.0"
9 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/AndroidX.kt:
--------------------------------------------------------------------------------
1 | object AndroidX {
2 | private const val coreKtxVersion = "1.9.0"
3 | const val coreKtx = "androidx.core:core-ktx:$coreKtxVersion"
4 |
5 | private const val appCompatVersion = "1.6.0"
6 | const val appCompat = "androidx.appcompat:appcompat:$appCompatVersion"
7 |
8 | private const val lifecycleVmKtxVersion = "2.4.0-alpha02"
9 | const val lifecycleVmKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVmKtxVersion"
10 | }
11 |
12 | object AndroidXTest {
13 | private const val version = "1.3.0"
14 | const val runner = "androidx.test:runner:$version"
15 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Build.kt:
--------------------------------------------------------------------------------
1 | object Build {
2 | private const val androidBuildToolsVersion = "7.1.0-alpha03"
3 | const val androidBuildTools = "com.android.tools.build:gradle:$androidBuildToolsVersion"
4 |
5 | const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Kotlin.version}"
6 |
7 | const val hiltAndroid = "com.google.dagger:hilt-android-gradle-plugin:${Hilt.hiltVersion}"
8 |
9 | const val sqlDelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${SqlDelight.version}"
10 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Coil.kt:
--------------------------------------------------------------------------------
1 | object Coil {
2 | // https://coil-kt.github.io/coil/compose/
3 | private const val version = "1.4.0"//""2.2.2"
4 | const val coil = "io.coil-kt:coil-compose:$version"
5 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Compose.kt:
--------------------------------------------------------------------------------
1 | object Compose {
2 | private const val activityComposeVersion = "1.3.0-rc01"
3 | const val activity = "androidx.activity:activity-compose:$activityComposeVersion"
4 |
5 | const val composeVersion = "1.0.0"
6 | const val ui = "androidx.compose.ui:ui:$composeVersion"
7 | const val material = "androidx.compose.material:material:$composeVersion"
8 | const val tooling = "androidx.compose.ui:ui-tooling:$composeVersion"
9 |
10 | private const val navigationVersion = "2.5.3"
11 | const val navigation = "androidx.navigation:navigation-compose:$navigationVersion"
12 |
13 | private const val hiltNavigationComposeVersion = "1.0.0-alpha03"
14 | const val hiltNavigation = "androidx.hilt:hilt-navigation-compose:$hiltNavigationComposeVersion"
15 | }
16 |
17 | object ComposeTest {
18 | const val uiTestJunit4 = "androidx.compose.ui:ui-test-junit4:${Compose.composeVersion}"
19 | const val uiTestManifest = "androidx.compose.ui:ui-test-manifest:${Compose.composeVersion}"
20 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Google.kt:
--------------------------------------------------------------------------------
1 | object Google {
2 |
3 | private const val materialVersion = "1.4.0"
4 | const val material = "com.google.android.material:material:$materialVersion"
5 |
6 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Hilt.kt:
--------------------------------------------------------------------------------
1 | object Hilt {
2 | const val hiltVersion = "2.44.1"
3 | const val hiltLifeCycleVersion="1.0.0-alpha03"
4 | const val android = "com.google.dagger:hilt-android:$hiltVersion"
5 | const val compiler = "com.google.dagger:hilt-android-compiler:$hiltVersion"
6 | const val hiltLifeCycle="androidx.hilt:hilt-lifecycle-viewmodel:$hiltLifeCycleVersion"
7 | }
8 |
9 | object HiltTest {
10 | const val hiltAndroidTesting = "com.google.dagger:hilt-android-testing:${Hilt.hiltVersion}"
11 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Junit.kt:
--------------------------------------------------------------------------------
1 | object Junit {
2 | private const val version = "4.12"
3 | const val junit4 = "junit:junit:4.12"
4 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Kotlin.kt:
--------------------------------------------------------------------------------
1 | object Kotlin {
2 | const val version = "1.5.10"
3 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/KotlinPlugins.kt:
--------------------------------------------------------------------------------
1 | object KotlinPlugins {
2 | const val serialization = "plugin.serialization"
3 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Kotlinx.kt:
--------------------------------------------------------------------------------
1 | object Kotlinx {
2 | private const val kotlinxDatetimeVersion = "0.1.1"
3 | const val datetime = "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetimeVersion"
4 |
5 | private const val coroutinesCoreVersion = "1.5.1"
6 | const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesCoreVersion"
7 |
8 | // Need for tests. Plugin doesn't work.
9 | private const val serializationVersion = "1.2.2"
10 | const val serialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion"
11 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Ktor.kt:
--------------------------------------------------------------------------------
1 | object Ktor {
2 | private const val ktorVersion = "1.5.2"
3 | const val core = "io.ktor:ktor-client-core:$ktorVersion"
4 | const val clientSerialization = "io.ktor:ktor-client-serialization:$ktorVersion"
5 | const val android = "io.ktor:ktor-client-android:$ktorVersion"
6 |
7 | const val ktorClientMock = "io.ktor:ktor-client-mock:$ktorVersion"
8 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Modules.kt:
--------------------------------------------------------------------------------
1 | object Modules {
2 |
3 | const val app = ":app"
4 |
5 | const val core = ":core"
6 |
7 | const val components = ":component"
8 |
9 | const val constants = ":constants"
10 |
11 | const val hero = ":hero"
12 | const val heroDataSource = ":hero:hero-datasource"
13 | const val heroDataSourceTest = ":hero:hero-datasource-test"
14 | const val heroDomain = ":hero:hero-domain"
15 | const val heroInteractors = ":hero:hero-interactors"
16 |
17 |
18 | const val ui_heroDetail = ":hero:ui-heroDetail"
19 | const val ui_heroList = ":hero:ui-heroList"
20 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/SqlDelight.kt:
--------------------------------------------------------------------------------
1 | object SqlDelight {
2 |
3 | const val version = "1.5.5"
4 | const val runtime = "com.squareup.sqldelight:runtime:${version}"
5 | const val androidDriver = "com.squareup.sqldelight:android-driver:${version}"
6 |
7 | const val plugin = "com.squareup.sqldelight"
8 | }
--------------------------------------------------------------------------------
/component/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/component/src/androidTest/java/com/majid2851/component/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.component
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.majid2851.component.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/component/src/build.gradle.kts:
--------------------------------------------------------------------------------
1 | apply {
2 | from("$rootDir/android-library-build.gradle")
3 |
4 | }
5 |
6 | dependencies{
7 |
8 | }
--------------------------------------------------------------------------------
/component/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/component/src/test/java/com/majid2851/component/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.component
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 | }
--------------------------------------------------------------------------------
/constants/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/constants/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_7
8 | targetCompatibility = JavaVersion.VERSION_1_7
9 |
10 | }
--------------------------------------------------------------------------------
/constants/src/main/java/com/majid2851/constants/Placeholder.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.constants
2 |
3 | class Placeholder {
4 | }
--------------------------------------------------------------------------------
/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_7
8 | targetCompatibility = JavaVersion.VERSION_1_7
9 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/majid2851/core/DataState.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.core
2 |
3 | sealed class DataState
4 | {
5 | data class Response(
6 | val uiComponent:UIComponent
7 | ):DataState()
8 |
9 | data class Data(
10 | val data:T ?=null
11 | ):DataState()
12 |
13 | data class Loading(
14 | val progressBarState:ProgressBarState=ProgressBarState.Idle
15 | ):DataState()
16 |
17 |
18 |
19 |
20 |
21 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/majid2851/core/FilterOrder.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.core
2 |
3 | sealed class FilterOrder
4 | {
5 | object Ascending:FilterOrder()
6 |
7 | object Descending:FilterOrder()
8 |
9 |
10 |
11 |
12 |
13 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/majid2851/core/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.core
2 |
3 |
4 | class Logger(
5 | private val tag:String,
6 | private val isDebug:Boolean=false
7 | )
8 | {
9 | fun log(msg:String)
10 | {
11 | if (!isDebug){
12 |
13 | }else{
14 | printLogD(tag,msg)
15 | }
16 | }
17 | companion object Factory{
18 | fun buildDebug(tag:String): Logger {
19 | return Logger(tag=tag, isDebug = true)
20 | }
21 | fun buildRelease(tag:String): Logger {
22 | return Logger(tag=tag, isDebug = false)
23 | }
24 | }
25 |
26 | private fun printLogD(tag: String, msg: String) {
27 | println("$tag:$msg")
28 | }
29 |
30 |
31 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/majid2851/core/ProgressBarState.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.core
2 |
3 | sealed class ProgressBarState
4 | {
5 | object Loading:ProgressBarState()
6 |
7 | object Idle:ProgressBarState()
8 |
9 |
10 |
11 |
12 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/majid2851/core/Queue.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.core
2 |
3 | /**
4 | * Kotlin version of a java.util Queue
5 | * https://docs.oracle.com/javase/8/docs/api/java/util/Queue.html
6 | */
7 | class Queue (list:MutableList){
8 |
9 | var items: MutableList = list
10 |
11 | fun isEmpty():Boolean = items.isEmpty()
12 |
13 | fun count():Int = items.count()
14 |
15 | override fun toString() = items.toString()
16 |
17 | fun add(element: T){
18 | items.add(element)
19 | }
20 |
21 | @Throws(Exception::class)
22 | fun remove(): T {
23 | if (this.isEmpty()){
24 | throw Exception("fun 'remove' threw an exception: Nothing to remove from the queue.")
25 | } else {
26 | return items.removeAt(0)
27 | }
28 | }
29 |
30 | fun remove(item: T): Boolean {
31 | return items.remove(item)
32 | }
33 |
34 | @Throws(Exception::class)
35 | fun element(): T {
36 | if(this.isEmpty()){
37 | throw Exception("fun 'element' threw an exception: Nothing in the queue.")
38 | }
39 | return items[0]
40 | }
41 |
42 | fun offer(element: T): Boolean{
43 | try{
44 | items.add(element)
45 | }catch (e: Exception){
46 | return false
47 | }
48 | return true
49 | }
50 |
51 | fun poll(): T?{
52 | if(this.isEmpty()) return null
53 | return items.removeAt(0)
54 | }
55 |
56 | fun peek():T?{
57 | if(this.isEmpty()) return null
58 | return items[0]
59 | }
60 |
61 | fun addAll(queue: Queue){
62 | this.items.addAll(queue.items)
63 | }
64 |
65 | fun clear(){
66 | items.removeAll { true }
67 | // items.clear()
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/majid2851/core/UIComponent.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.core
2 |
3 | sealed class UIComponent
4 | {
5 | data class Dialog(
6 | val title:String,
7 | val description:String
8 | ):UIComponent()
9 |
10 | data class None(
11 | val message:String
12 | ):UIComponent()
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | }
--------------------------------------------------------------------------------
/core/src/main/java/com/majid2851/core/UIComponentState.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.core
2 |
3 | sealed class UIComponentState()
4 | {
5 | object Show:UIComponentState()
6 |
7 | object Hide:UIComponentState()
8 |
9 | }
--------------------------------------------------------------------------------
/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/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Apr 28 14:26:09 IRST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-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 |
--------------------------------------------------------------------------------
/hero/hero-datasource-test/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/hero/hero-datasource-test/build.gradle.kts:
--------------------------------------------------------------------------------
1 |
2 | plugins {
3 | id("java-library")
4 | id("org.jetbrains.kotlin.jvm")
5 | id(SqlDelight.plugin) version SqlDelight.version
6 | kotlin(KotlinPlugins.serialization) version Kotlin.version
7 | }
8 |
9 | java {
10 | sourceCompatibility = JavaVersion.VERSION_1_7
11 | targetCompatibility = JavaVersion.VERSION_1_7
12 | }
13 | dependencies{
14 | "implementation"(project(Modules.heroDataSource))
15 | "implementation"(project(Modules.heroDomain))
16 | "implementation"(Ktor.ktorClientMock)
17 | "implementation"(Ktor.clientSerialization)
18 | }
19 |
20 |
21 |
--------------------------------------------------------------------------------
/hero/hero-datasource-test/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/hero/hero-datasource-test/src/main/java/com/majid2851/hero_datasource_test/Util.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource_test
2 |
3 | import com.majid2851.hero_datasource.network.HeroDto
4 | import com.majid2851.hero_datasource.network.toHero
5 | import com.majid2851.hero_domain.Hero
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.decodeFromString
8 | import kotlinx.serialization.json.Json
9 |
10 | private val json= Json {
11 | ignoreUnknownKeys=true
12 | }
13 | @OptIn(ExperimentalSerializationApi::class)
14 | fun serialzeHeroData(jsonData:String):List{
15 | val heros:List = json.decodeFromString>(jsonData)
16 | .map {
17 | it.toHero()
18 | }
19 | return heros
20 | }
--------------------------------------------------------------------------------
/hero/hero-datasource-test/src/main/java/com/majid2851/hero_datasource_test/cache/HeroCacheFake.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource_test.cache
2 |
3 | import com.majid2851.hero_datasource.cache.HeroCache
4 | import com.majid2851.hero_domain.Hero
5 | import com.majid2851.hero_domain.HeroRole
6 |
7 | class HeroCacheFake(
8 | private val db: HeroDatabaseFake
9 | ) : HeroCache {
10 |
11 | override suspend fun getHero(id: Int): Hero? {
12 | return db.heros.find { it.id == id }
13 | }
14 |
15 | override suspend fun removeHero(id: Int) {
16 | db.heros.removeIf { it.id == id }
17 | }
18 |
19 | override suspend fun selectAll(): List {
20 | return db.heros
21 | }
22 |
23 | override suspend fun insert(hero: Hero) {
24 | if(db.heros.isNotEmpty()){
25 | var didInsert = false
26 | for(h in db.heros){
27 | if(h.id == hero.id){
28 | db.heros.remove(h)
29 | db.heros.add(hero)
30 | didInsert = true
31 | break
32 | }
33 | }
34 | if(!didInsert){
35 | db.heros.add(hero)
36 | }
37 | }
38 | else{
39 | db.heros.add(hero)
40 | }
41 | }
42 |
43 | override suspend fun insert(heros: List) {
44 | if(db.heros.isNotEmpty()){
45 | for(hero in heros){
46 | if(db.heros.contains(hero)){
47 | db.heros.remove(hero)
48 | db.heros.add(hero)
49 | }
50 | }
51 | }
52 | else{
53 | db.heros.addAll(heros)
54 | }
55 | }
56 |
57 | override suspend fun searchByName(localizedName: String): List {
58 | return db.heros.find { it.localizedName == localizedName }?.let {
59 | listOf(it)
60 | }?: listOf()
61 | }
62 |
63 | override suspend fun searchByAttr(primaryAttr: String): List {
64 | return db.heros.filter { it.primaryAttribute.uiValue == primaryAttr }
65 | }
66 |
67 | override suspend fun searchByAttackType(attackType: String): List {
68 | return db.heros.filter { it.attackType.uiValue == attackType }
69 | }
70 |
71 | override suspend fun searchByRole(
72 | carry: Boolean,
73 | escape: Boolean,
74 | nuker: Boolean,
75 | initiator: Boolean,
76 | durable: Boolean,
77 | disabler: Boolean,
78 | jungler: Boolean,
79 | support: Boolean,
80 | pusher: Boolean
81 | ): List {
82 | val heros: MutableList = mutableListOf()
83 | if(carry){
84 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Carry) })
85 | }
86 | if(escape){
87 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Escape) })
88 | }
89 | if(nuker){
90 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Nuker) })
91 | }
92 | if(initiator){
93 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Initiator) })
94 | }
95 | if(durable){
96 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Durable) })
97 | }
98 | if(disabler){
99 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Disabler) })
100 | }
101 | if(jungler){
102 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Jungler) })
103 | }
104 | if(support){
105 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Support) })
106 | }
107 | if(pusher){
108 | heros.addAll(db.heros.filter { it.roles.contains(HeroRole.Pusher) })
109 | }
110 | return heros.distinctBy { it.id }
111 | }
112 | }
--------------------------------------------------------------------------------
/hero/hero-datasource-test/src/main/java/com/majid2851/hero_datasource_test/cache/HeroDatabaseFake.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource_test.cache
2 |
3 | import com.majid2851.hero_domain.Hero
4 |
5 | class HeroDatabaseFake ()
6 | {
7 | val heros:MutableList = mutableListOf()
8 |
9 |
10 |
11 |
12 | }
--------------------------------------------------------------------------------
/hero/hero-datasource-test/src/main/java/com/majid2851/hero_datasource_test/network/HeroServiceFake.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource_test.network
2 |
3 | import com.majid2851.hero_datasource.network.HeroService
4 | import com.majid2851.hero_datasource.network.HeroServiceImpl
5 | import com.majid2851.hero_datasource_test.network.data.HeroDataMalformed
6 | import com.majid2851.hero_datasource_test.network.data.HeroDataValid
7 | import com.majid2851.hero_datasource_test.network.data.HeroEmptyData
8 | import io.ktor.client.HttpClient
9 | import io.ktor.client.engine.mock.MockEngine
10 | import io.ktor.client.engine.mock.respond
11 | import io.ktor.client.features.json.JsonFeature
12 | import io.ktor.client.features.json.serializer.KotlinxSerializer
13 | import io.ktor.http.HttpStatusCode
14 | import io.ktor.http.Url
15 | import io.ktor.http.fullPath
16 | import io.ktor.http.headersOf
17 | import io.ktor.http.hostWithPort
18 |
19 | class HeroServiceFake
20 | {
21 | private val Url.hostWithPortIfRequired: String get() = if (port == protocol.defaultPort) host else hostWithPort
22 | private val Url.fullUrl: String get() = "${protocol.name}://$hostWithPortIfRequired$fullPath"
23 | fun build(
24 | type:HeroServiceResponseType
25 | ):HeroService {
26 | val client = HttpClient(MockEngine)
27 | {
28 | install(JsonFeature)
29 | {
30 | serializer = KotlinxSerializer(
31 | kotlinx.serialization.json.Json {
32 | ignoreUnknownKeys = true
33 | }
34 | )
35 | }
36 | engine {
37 | addHandler { request ->
38 | when (request.url.fullUrl) {
39 | "https://api.opendota.com/api/heroStats" -> {
40 | val responseHeaders = headersOf(
41 | "Content-Type" to listOf("application/json", "charset=utf-8")
42 | )
43 | when (type) {
44 | is HeroServiceResponseType.EmptyList -> {
45 | respond(
46 | HeroEmptyData.data,
47 | status = HttpStatusCode.OK,
48 | headers = responseHeaders
49 | )
50 | }
51 |
52 | is HeroServiceResponseType.MalformedData -> {
53 | respond(
54 | HeroDataMalformed.data,
55 | status = HttpStatusCode.OK,
56 | headers = responseHeaders
57 | )
58 | }
59 |
60 | is HeroServiceResponseType.GoodData -> {
61 | respond(
62 | HeroDataValid.data,
63 | status = HttpStatusCode.OK,
64 | headers = responseHeaders
65 | )
66 | }
67 |
68 | is HeroServiceResponseType.Http404 -> {
69 | respond(
70 | HeroEmptyData.data,
71 | status = HttpStatusCode.NotFound,
72 | headers = responseHeaders
73 | )
74 | }
75 | }
76 | }
77 |
78 | else -> error("Unhandled ${request.url.fullUrl}")
79 | }
80 | }
81 | }
82 | }
83 | return HeroServiceImpl(client)
84 | }
85 | }
--------------------------------------------------------------------------------
/hero/hero-datasource-test/src/main/java/com/majid2851/hero_datasource_test/network/HeroServiceResponseType.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource_test.network
2 |
3 | sealed class HeroServiceResponseType()
4 | {
5 | object EmptyList:HeroServiceResponseType()
6 | object MalformedData:HeroServiceResponseType()
7 | object GoodData:HeroServiceResponseType()
8 | object Http404:HeroServiceResponseType()
9 |
10 |
11 |
12 |
13 | }
--------------------------------------------------------------------------------
/hero/hero-datasource-test/src/main/java/com/majid2851/hero_datasource_test/network/data/HeroEmptyData.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource_test.network.data
2 |
3 | object HeroEmptyData
4 | {
5 | val data="[]"
6 | }
--------------------------------------------------------------------------------
/hero/hero-datasource/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/hero/hero-datasource/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | id(SqlDelight.plugin) version SqlDelight.version
5 | kotlin(KotlinPlugins.serialization) version Kotlin.version
6 | }
7 |
8 | java {
9 | sourceCompatibility = JavaVersion.VERSION_1_7
10 | targetCompatibility = JavaVersion.VERSION_1_7
11 | }
12 | dependencies{
13 | "implementation"(project(Modules.heroDomain))
14 |
15 | "implementation"(Ktor.core)
16 | "implementation"(Ktor.clientSerialization)
17 | "implementation"(Ktor.android)
18 |
19 | "implementation"(SqlDelight.runtime)
20 | }
21 | sqldelight{
22 | database("HeroDatabase"){
23 | packageName="com.majid2851.hero_datasource.cache"
24 | sourceFolders= listOf("sqldelight")
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/hero/hero-datasource/src/main/java/com/majid2851/hero_datasource/cache/HeroCache.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource.cache
2 |
3 | import com.majid2851.hero_domain.Hero
4 | import com.squareup.sqldelight.db.SqlDriver
5 |
6 |
7 | interface HeroCache {
8 |
9 | suspend fun getHero(id: Int): Hero?
10 |
11 | suspend fun removeHero(id: Int)
12 |
13 | suspend fun selectAll(): List
14 |
15 | suspend fun insert(hero: Hero)
16 |
17 | suspend fun insert(heros: List)
18 |
19 | suspend fun searchByName(localizedName: String): List
20 |
21 | suspend fun searchByAttr(primaryAttr: String): List
22 |
23 | suspend fun searchByAttackType(attackType: String): List
24 |
25 | // Can select multiple roles
26 | suspend fun searchByRole(
27 | carry: Boolean = false,
28 | escape: Boolean = false,
29 | nuker: Boolean = false,
30 | initiator: Boolean = false,
31 | durable: Boolean = false,
32 | disabler: Boolean = false,
33 | jungler: Boolean = false,
34 | support: Boolean = false,
35 | pusher: Boolean = false,
36 | ): List
37 |
38 | companion object Factory {
39 | fun build(sqlDriver: SqlDriver): HeroCache {
40 | return HeroCacheImpl(HeroDatabase(sqlDriver))
41 | }
42 | val schema: SqlDriver.Schema = HeroDatabase.Schema
43 |
44 | val dbName: String = "heros.db"
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/hero/hero-datasource/src/main/java/com/majid2851/hero_datasource/cache/HeroCacheImpl.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource.cache
2 |
3 | import com.majid2851.hero_domain.Hero
4 | import com.majid2851.hero_domain.HeroRole
5 | import com.majid2851.herodatasource.cache.HeroDbQueries
6 |
7 | class HeroCacheImpl(
8 | private val heroDatabase: HeroDatabase,
9 | ): HeroCache {
10 |
11 | private var queries: HeroDbQueries = heroDatabase.heroDbQueries
12 |
13 | override suspend fun getHero(id: Int): Hero {
14 | return queries.getHero(id.toLong()).executeAsOne().toHero()
15 | }
16 |
17 | override suspend fun removeHero(id: Int) {
18 | queries.removeHero(id.toLong())
19 | }
20 |
21 | override suspend fun selectAll(): List {
22 | return queries.selectAll().executeAsList().map { it.toHero() }
23 | }
24 |
25 | override suspend fun insert(hero: Hero) {
26 | return hero.run {
27 | queries.insertHero(
28 | id = id.toLong(),
29 | localizedName = localizedName,
30 | primaryAttribute = primaryAttribute.abbreviation,
31 | attackType = attackType.uiValue,
32 | roleCarry = if(roles.contains(HeroRole.Carry)) 1L else 0L,
33 | roleEscape = if(roles.contains(HeroRole.Escape)) 1L else 0L,
34 | roleNuker = if(roles.contains(HeroRole.Nuker)) 1L else 0L,
35 | roleInitiator = if(roles.contains(HeroRole.Initiator)) 1L else 0L,
36 | roleDurable = if(roles.contains(HeroRole.Durable)) 1L else 0L,
37 | roleDisabler = if(roles.contains(HeroRole.Disabler)) 1L else 0L,
38 | roleJungler = if(roles.contains(HeroRole.Jungler)) 1L else 0L,
39 | roleSupport = if(roles.contains(HeroRole.Support)) 1L else 0L,
40 | rolePusher = if(roles.contains(HeroRole.Pusher)) 1L else 0L,
41 | img = img,
42 | icon = icon,
43 | baseHealth = baseHealth.toDouble(),
44 | baseHealthRegen = baseHealthRegen?.toDouble(),
45 | baseMana = baseMana.toDouble(),
46 | baseManaRegen = baseManaRegen?.toDouble(),
47 | baseArmor = baseArmor.toDouble(),
48 | baseMoveRate = baseMoveRate.toDouble(),
49 | baseAttackMin = baseAttackMin.toDouble(),
50 | baseAttackMax = baseAttackMax.toDouble(),
51 | baseStr = baseStr.toLong(),
52 | baseAgi = baseAgi.toLong(),
53 | baseInt = baseInt.toLong(),
54 | strGain = strGain.toDouble(),
55 | agiGain = agiGain.toDouble(),
56 | intGain = intGain.toDouble(),
57 | attackRange = attackRange.toLong(),
58 | projectileSpeed = projectileSpeed.toLong(),
59 | attackRate = attackRate.toDouble(),
60 | moveSpeed = moveSpeed.toLong(),
61 | turnRate = turnRate?.toDouble(),
62 | legs = legs.toLong(),
63 | turboPicks = turboPicks.toLong(),
64 | turboWins = turboWins.toLong(),
65 | proWins = proWins.toLong(),
66 | proPick = proPick.toLong(),
67 | firstPick = firstPick.toLong(),
68 | firstWin = firstWin.toLong(),
69 | secondPick = secondPick.toLong(),
70 | secondWin = secondWin.toLong(),
71 | thirdPick = thirdPick.toLong(),
72 | thirdWin = thirdWin.toLong(),
73 | fourthPick = fourthPick.toLong(),
74 | fourthWin = fourthWin.toLong(),
75 | fifthPick = fifthPick.toLong(),
76 | fifthWin = fifthWin.toLong(),
77 | sixthPick = sixthPick.toLong(),
78 | sixthWin = sixthWin.toLong(),
79 | seventhPick = seventhPick.toLong(),
80 | seventhWin = seventhWin.toLong(),
81 | eighthWin = eighthWin.toLong(),
82 | eighthPick = eighthPick.toLong(),
83 | )
84 | }
85 | }
86 |
87 | override suspend fun insert(heros: List) {
88 | for(hero in heros){
89 | try {
90 | insert(hero)
91 | }catch (e: Exception){
92 | e.printStackTrace()
93 | // if one has an error just continue with others
94 | }
95 | }
96 | }
97 |
98 | override suspend fun searchByName(localizedName: String): List {
99 | return queries.searchHeroByName(localizedName).executeAsList().map { it.toHero() }
100 | }
101 |
102 | override suspend fun searchByAttr(primaryAttr: String): List {
103 | return queries.searchHeroByAttr(primaryAttr).executeAsList().map { it.toHero() }
104 | }
105 |
106 | override suspend fun searchByAttackType(attackType: String): List {
107 | return queries.searchHeroByAttackType(attackType).executeAsList().map { it.toHero() }
108 | }
109 |
110 | override suspend fun searchByRole(
111 | carry: Boolean,
112 | escape: Boolean,
113 | nuker: Boolean,
114 | initiator: Boolean,
115 | durable: Boolean,
116 | disabler: Boolean,
117 | jungler: Boolean,
118 | support: Boolean,
119 | pusher: Boolean
120 | ): List {
121 | return queries.searchHeroByRole(
122 | roleCarry = if(carry) 1L else 0L,
123 | roleEscape = if(escape) 1L else 0L,
124 | roleNuker = if(nuker) 1L else 0L,
125 | roleInitiator = if(initiator) 1L else 0L,
126 | roleDurable = if(durable) 1L else 0L,
127 | roleDisabler = if(disabler) 1L else 0L,
128 | roleJungler = if(jungler) 1L else 0L,
129 | roleSupport = if(support) 1L else 0L,
130 | rolePusher = if(pusher) 1L else 0L,
131 | ).executeAsList().map { it.toHero() }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/hero/hero-datasource/src/main/java/com/majid2851/hero_datasource/cache/HeroEntity.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource.cache
2 |
3 | import com.majid2851.hero_datasource.network.EndPoints
4 | import com.majid2851.hero_domain.Hero
5 | import com.majid2851.hero_domain.HeroAttribute
6 | import com.majid2851.hero_domain.HeroAttribute.Agility.getHeroAttrFromAbbreviation
7 | import com.majid2851.hero_domain.HeroRole
8 | import com.majid2851.hero_domain.getHeroAttackType
9 | import com.majid2851.hero_domain.getHeroRole
10 | import com.majid2851.herodatasource.cache.Hero_Entity
11 |
12 | fun Hero_Entity.toHero(): Hero {
13 | return Hero(
14 | id = id.toInt(),
15 | localizedName = localizedName,
16 | primaryAttribute = getHeroAttrFromAbbreviation(primaryAttribute),
17 | attackType = getHeroAttackType(attackType),
18 | roles = rolesToList(
19 | carry = roleCarry?.toInt() == 1,
20 | escape = roleEscape?.toInt() == 1,
21 | nuker = roleNuker?.toInt() == 1,
22 | initiator = roleInitiator?.toInt() == 1,
23 | durable = roleDurable?.toInt() == 1,
24 | disabler = roleDisabler?.toInt() == 1,
25 | jungler = roleJungler?.toInt() == 1,
26 | support = roleSupport?.toInt() == 1,
27 | pusher = rolePusher?.toInt() == 1,
28 | ),
29 | img = img,
30 | icon = icon,
31 | baseHealth = baseHealth.toFloat(),
32 | baseHealthRegen = baseHealthRegen?.toFloat(),
33 | baseMana = baseMana.toFloat(),
34 | baseManaRegen = baseManaRegen?.toFloat(),
35 | baseArmor = baseArmor.toFloat(),
36 | baseMoveRate = baseMoveRate.toFloat(),
37 | baseAttackMin = baseAttackMin.toInt(),
38 | baseAttackMax = baseAttackMax.toInt(),
39 | baseStr = baseStr.toInt(),
40 | baseAgi = baseAgi.toInt(),
41 | baseInt = baseInt.toInt(),
42 | strGain = strGain.toFloat(),
43 | agiGain = agiGain.toFloat(),
44 | intGain = intGain.toFloat(),
45 | attackRange = attackRange.toInt(),
46 | projectileSpeed = projectileSpeed.toInt(),
47 | attackRate = attackRate.toFloat(),
48 | moveSpeed = moveSpeed.toInt(),
49 | turnRate = turnRate?.toFloat(),
50 | legs = legs.toInt(),
51 | turboPicks = turboPicks.toInt(),
52 | turboWins = turboWins.toInt(),
53 | proWins = proWins.toInt(),
54 | proPick = proPick.toInt(),
55 | firstPick = firstPick.toInt(),
56 | firstWin = firstWin.toInt(),
57 | secondPick = secondPick.toInt(),
58 | secondWin = secondWin.toInt(),
59 | thirdPick = thirdPick.toInt(),
60 | thirdWin = thirdWin.toInt(),
61 | fourthPick = fourthPick.toInt(),
62 | fourthWin = fourthWin.toInt(),
63 | fifthPick = fifthPick.toInt(),
64 | fifthWin = fifthWin.toInt(),
65 | sixthPick = sixthPick.toInt(),
66 | sixthWin = sixthWin.toInt(),
67 | seventhPick = seventhPick.toInt(),
68 | seventhWin = seventhWin.toInt(),
69 | eighthWin = eighthWin.toInt(),
70 | eighthPick = eighthPick.toInt(),
71 | )
72 | }
73 |
74 | fun rolesToList(
75 | carry: Boolean,
76 | escape: Boolean,
77 | nuker: Boolean,
78 | initiator: Boolean,
79 | durable: Boolean,
80 | disabler: Boolean,
81 | jungler: Boolean,
82 | support: Boolean,
83 | pusher: Boolean,
84 | ): List{
85 | val roles: MutableList = mutableListOf()
86 | if(carry) roles.add(HeroRole.Carry)
87 | if(escape) roles.add(HeroRole.Escape)
88 | if(nuker) roles.add(HeroRole.Nuker)
89 | if(initiator) roles.add(HeroRole.Initiator)
90 | if(durable) roles.add(HeroRole.Durable)
91 | if(disabler) roles.add(HeroRole.Disabler)
92 | if(jungler) roles.add(HeroRole.Jungler)
93 | if(support) roles.add(HeroRole.Support)
94 | if(pusher) roles.add(HeroRole.Pusher)
95 | return roles.toList()
96 | }
--------------------------------------------------------------------------------
/hero/hero-datasource/src/main/java/com/majid2851/hero_datasource/network/EndPoints.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource.network
2 |
3 | object EndPoints
4 | {
5 | const val BASE_URL="https://api.opendota.com"
6 | const val HERO_STATS="$BASE_URL/api/heroStats"
7 |
8 |
9 |
10 |
11 | }
--------------------------------------------------------------------------------
/hero/hero-datasource/src/main/java/com/majid2851/hero_datasource/network/HeroDto.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource.network
2 |
3 | import com.majid2851.hero_datasource.network.EndPoints.BASE_URL
4 | import com.majid2851.hero_domain.*
5 | import com.majid2851.hero_domain.HeroAttribute.Agility.getHeroAttrFromAbbreviation
6 | import kotlinx.serialization.SerialName
7 | import kotlinx.serialization.Serializable
8 |
9 | @Serializable
10 | data class HeroDto(
11 |
12 | @SerialName("id")
13 | val id: Int,
14 |
15 | @SerialName("localized_name")
16 | val localizedName: String,
17 |
18 | @SerialName("primary_attr")
19 | val primaryAttribute: String,
20 |
21 | @SerialName("attack_type")
22 | val attackType: String,
23 |
24 | @SerialName("roles")
25 | val roles: List,
26 |
27 | @SerialName("img")
28 | val img: String,
29 |
30 | @SerialName("icon")
31 | val icon: String,
32 |
33 | @SerialName("base_health")
34 | val baseHealth: Float,
35 |
36 | @SerialName("base_health_regen")
37 | val baseHealthRegen: Float?,
38 |
39 | @SerialName("base_mana")
40 | val baseMana: Float,
41 |
42 | @SerialName("base_mana_regen")
43 | val baseManaRegen: Float?,
44 |
45 | @SerialName("base_armor")
46 | val baseArmor: Float,
47 |
48 | @SerialName("base_mr")
49 | val baseMoveRate: Float,
50 |
51 | @SerialName("base_attack_min")
52 | val baseAttackMin: Int,
53 |
54 | @SerialName("base_attack_max")
55 | val baseAttackMax: Int,
56 |
57 | @SerialName("base_str")
58 | val baseStr: Int,
59 |
60 | @SerialName("base_agi")
61 | val baseAgi: Int,
62 |
63 | @SerialName("base_int")
64 | val baseInt: Int,
65 |
66 | @SerialName("str_gain")
67 | val strGain: Float, // Strength gain per lvl
68 |
69 | @SerialName("agi_gain")
70 | val agiGain: Float, // Agility gain per lvl
71 |
72 | @SerialName("int_gain")
73 | val intGain: Float, // Intelligence gain per lvl
74 |
75 | @SerialName("attack_range")
76 | val attackRange: Int,
77 |
78 | @SerialName("projectile_speed")
79 | val projectileSpeed: Int,
80 |
81 | @SerialName("attack_rate")
82 | val attackRate: Float,
83 |
84 | @SerialName("move_speed")
85 | val moveSpeed: Int,
86 |
87 | @SerialName("turn_rate")
88 | val turnRate: Float? = 0F,
89 |
90 | @SerialName("legs")
91 | val legs: Int, // How many legs does this hero have?
92 |
93 | @SerialName("turbo_picks")
94 | val turboPicks: Int, // How many times picked for turbo matches?
95 |
96 | @SerialName("turbo_wins")
97 | val turboWins: Int, // How many times won a turbo match?
98 |
99 | @SerialName("pro_win")
100 | val proWins: Int? = 0, // How many times won a pro match?
101 |
102 | @SerialName("pro_pick")
103 | val proPick: Int? = 0, // How many times picked in pro match?
104 |
105 | @SerialName("1_pick")
106 | val firstPick: Int, // How many times picked first?
107 |
108 | @SerialName("1_win")
109 | val firstWin: Int, // How many times picked first and won?
110 |
111 | @SerialName("2_pick")
112 | val secondPick: Int, // How many times picked second?
113 |
114 | @SerialName("2_win")
115 | val secondWin: Int, // How many times picked second and won?
116 |
117 | @SerialName("3_pick")
118 | val thirdPick: Int, // How many times picked third?
119 |
120 | @SerialName("3_win")
121 | val thirdWin: Int, // How many times picked third and won?
122 |
123 | @SerialName("4_pick")
124 | val fourthPick: Int, // How many times picked in fourth round?
125 |
126 | @SerialName("4_win")
127 | val fourthWin: Int, // How many times picked in fourth round and won?
128 |
129 | @SerialName("5_pick")
130 | val fifthPick: Int, // How many times picked fifth?
131 |
132 | @SerialName("5_win")
133 | val fifthWin: Int, // How many times picked fifth and won?
134 |
135 | @SerialName("6_pick")
136 | val sixthPick: Int, // How many times picked sixth?
137 |
138 | @SerialName("6_win")
139 | val sixthWin: Int, // How many times picked sixth and won?
140 |
141 | @SerialName("7_pick")
142 | val seventhPick: Int, // How many times picked seventh?
143 |
144 | @SerialName("7_win")
145 | val seventhWin: Int, // How many times picked seventh and won?
146 |
147 | @SerialName("8_pick")
148 | val eighthPick: Int, // How many times picked eighth round?
149 |
150 | @SerialName("8_win")
151 | val eighthWin: Int, // How many times picked eighth and won?
152 | )
153 |
154 | fun HeroDto.toHero(): Hero{
155 | return Hero(
156 | id = id,
157 | localizedName = localizedName,
158 | primaryAttribute = getHeroAttrFromAbbreviation(primaryAttribute),
159 | attackType = getHeroAttackType(attackType),
160 | roles = roles.map { getHeroRole(it) },
161 | img = "$BASE_URL$img",
162 | icon = "$BASE_URL$icon",
163 | baseHealth = baseHealth,
164 | baseHealthRegen = baseHealthRegen,
165 | baseMana = baseMana,
166 | baseManaRegen = baseManaRegen,
167 | baseArmor = baseArmor,
168 | baseMoveRate = baseMoveRate,
169 | baseAttackMin = baseAttackMin,
170 | baseAttackMax = baseAttackMax,
171 | baseStr = baseStr,
172 | baseAgi = baseAgi,
173 | baseInt = baseInt,
174 | strGain = strGain,
175 | agiGain = agiGain,
176 | intGain = intGain,
177 | attackRange = attackRange,
178 | projectileSpeed = projectileSpeed,
179 | attackRate = attackRate,
180 | moveSpeed = moveSpeed,
181 | turnRate = turnRate,
182 | legs = legs,
183 | turboPicks = turboPicks,
184 | turboWins = turboWins,
185 | proWins = proWins ?: 0,
186 | proPick = proPick?: 0,
187 | firstPick = firstPick,
188 | firstWin = firstWin,
189 | secondPick = secondPick,
190 | secondWin = secondWin,
191 | thirdPick = thirdPick,
192 | thirdWin = thirdWin,
193 | fourthPick = fourthPick,
194 | fourthWin = fourthWin,
195 | fifthPick = fifthPick,
196 | fifthWin = fifthWin,
197 | sixthPick = sixthPick,
198 | sixthWin = sixthWin,
199 | seventhPick = seventhPick,
200 | seventhWin = seventhWin,
201 | eighthWin = eighthWin,
202 | eighthPick = eighthPick,
203 | )
204 | }
--------------------------------------------------------------------------------
/hero/hero-datasource/src/main/java/com/majid2851/hero_datasource/network/HeroService.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource.network
2 |
3 | import com.majid2851.hero_domain.Hero
4 | import io.ktor.client.HttpClient
5 | import io.ktor.client.engine.android.Android
6 | import io.ktor.client.features.json.Json
7 | import io.ktor.client.features.json.JsonFeature
8 | import io.ktor.client.features.json.serializer.KotlinxSerializer
9 |
10 | interface HeroService
11 | {
12 | suspend fun getHeroStats():List
13 |
14 | companion object Factory{
15 | fun build():HeroService{
16 | return HeroServiceImpl(
17 | httpClient = HttpClient(Android){
18 | install(JsonFeature){
19 | serializer=KotlinxSerializer(
20 | kotlinx.serialization.json.Json {
21 | ignoreUnknownKeys=true //if the server returns extra field , ignore them
22 | }
23 | )
24 | }
25 | }
26 | )
27 | }
28 | }
29 |
30 |
31 |
32 | }
--------------------------------------------------------------------------------
/hero/hero-datasource/src/main/java/com/majid2851/hero_datasource/network/HeroServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_datasource.network
2 |
3 | import com.majid2851.hero_domain.Hero
4 | import io.ktor.client.HttpClient
5 | import io.ktor.client.request.*
6 |
7 | class HeroServiceImpl(
8 | private val httpClient:HttpClient,
9 | ):HeroService
10 | {
11 | override suspend fun getHeroStats(): List {
12 | return httpClient.get>{
13 | url(EndPoints.HERO_STATS)
14 | }.map { it.toHero() }
15 | }
16 |
17 |
18 | }
--------------------------------------------------------------------------------
/hero/hero-datasource/src/main/sqldelight/com/majid2851/hero_datasource/cache/heroDb.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE hero_Entity(
2 | id INTEGER NOT NULL PRIMARY KEY,
3 | localizedName TEXT NOT NULL,
4 | primaryAttribute TEXT NOT NULL,
5 | attackType TEXT NOT NULL,
6 |
7 | -- Instead of doing comma separated strings I decided to make the roles booleans.
8 | -- If they contain a particular role then '1'. Otherwise '0'.
9 | roleCarry INTEGER DEFAULT 0, -- 0 == false
10 | roleEscape INTEGER DEFAULT 0,
11 | roleNuker INTEGER DEFAULT 0,
12 | roleInitiator INTEGER DEFAULT 0,
13 | roleDurable INTEGER DEFAULT 0,
14 | roleDisabler INTEGER DEFAULT 0,
15 | roleJungler INTEGER DEFAULT 0,
16 | roleSupport INTEGER DEFAULT 0,
17 | rolePusher INTEGER DEFAULT 0,
18 | -- end roles
19 |
20 | img TEXT NOT NULL,
21 | icon TEXT NOT NULL,
22 | baseHealth REAL NOT NULL,
23 | baseHealthRegen REAL,
24 | baseMana REAL NOT NULL,
25 | baseManaRegen REAL,
26 | baseArmor REAL NOT NULL,
27 | baseMoveRate REAL NOT NULL,
28 | baseAttackMin REAL NOT NULL,
29 | baseAttackMax REAL NOT NULL,
30 | baseStr INTEGER NOT NULL,
31 | baseAgi INTEGER NOT NULL,
32 | baseInt INTEGER NOT NULL,
33 | strGain REAL NOT NULL,
34 | agiGain REAL NOT NULL,
35 | intGain REAL NOT NULL,
36 | attackRange INTEGER NOT NULL,
37 | projectileSpeed INTEGER NOT NULL,
38 | attackRate REAL NOT NULL,
39 | moveSpeed INTEGER NOT NULL,
40 | turnRate REAL,
41 | legs INTEGER NOT NULL,
42 | turboPicks INTEGER NOT NULL,
43 | turboWins INTEGER NOT NULL,
44 | proWins INTEGER NOT NULL,
45 | proPick INTEGER NOT NULL,
46 | firstPick INTEGER NOT NULL,
47 | firstWin INTEGER NOT NULL,
48 | secondPick INTEGER NOT NULL,
49 | secondWin INTEGER NOT NULL,
50 | thirdPick INTEGER NOT NULL,
51 | thirdWin INTEGER NOT NULL,
52 | fourthPick INTEGER NOT NULL,
53 | fourthWin INTEGER NOT NULL,
54 | fifthPick INTEGER NOT NULL,
55 | fifthWin INTEGER NOT NULL,
56 | sixthPick INTEGER NOT NULL,
57 | sixthWin INTEGER NOT NULL,
58 | seventhPick INTEGER NOT NULL,
59 | seventhWin INTEGER NOT NULL,
60 | eighthPick INTEGER NOT NULL,
61 | eighthWin INTEGER NOT NULL
62 | );
63 |
64 | selectAll:
65 | SELECT *
66 | FROM hero_Entity;
67 |
68 | insertHero:
69 | INSERT OR REPLACE
70 | INTO hero_Entity (
71 | id,
72 | localizedName,
73 | primaryAttribute,
74 | attackType,
75 | roleCarry,
76 | roleEscape,
77 | roleNuker,
78 | roleInitiator,
79 | roleDurable,
80 | roleDisabler,
81 | roleJungler,
82 | roleSupport,
83 | rolePusher,
84 | img,
85 | icon,
86 | baseHealth,
87 | baseHealthRegen ,
88 | baseMana,
89 | baseManaRegen ,
90 | baseArmor,
91 | baseMoveRate,
92 | baseAttackMin,
93 | baseAttackMax,
94 | baseStr,
95 | baseAgi,
96 | baseInt,
97 | strGain,
98 | agiGain,
99 | intGain,
100 | attackRange,
101 | projectileSpeed,
102 | attackRate,
103 | moveSpeed,
104 | turnRate ,
105 | legs,
106 | turboPicks,
107 | turboWins,
108 | proWins,
109 | proPick,
110 | firstPick,
111 | firstWin,
112 | secondPick,
113 | secondWin,
114 | thirdPick,
115 | thirdWin,
116 | fourthPick,
117 | fourthWin,
118 | fifthPick,
119 | fifthWin,
120 | sixthPick,
121 | sixthWin,
122 | seventhPick,
123 | seventhWin,
124 | eighthPick,
125 | eighthWin
126 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
127 |
128 |
129 | searchHeroByName:
130 | SELECT * FROM hero_Entity
131 | WHERE localizedName LIKE ('%' || :query || '%');
132 |
133 | searchHeroByAttr:
134 | SELECT * FROM hero_Entity
135 | WHERE primaryAttribute = :primaryAttr;
136 |
137 | searchHeroByAttackType:
138 | SELECT * FROM hero_Entity
139 | WHERE attackType = :attackType;
140 |
141 | searchHeroByRole:
142 | SELECT * FROM hero_Entity
143 | WHERE roleCarry = :roleCarry
144 | AND roleEscape = :roleEscape
145 | AND roleNuker = :roleNuker
146 | AND roleInitiator = :roleInitiator
147 | AND roleDurable = :roleDurable
148 | AND roleDisabler = :roleDisabler
149 | AND roleJungler = :roleJungler
150 | AND roleSupport = :roleSupport
151 | AND rolePusher = :rolePusher
152 | ;
153 |
154 | getHero:
155 | SELECT * FROM hero_Entity
156 | WHERE id = :id;
157 |
158 | removeHero:
159 | DELETE FROM hero_Entity
160 | WHERE id = :id;
--------------------------------------------------------------------------------
/hero/hero-domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/hero/hero-domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_7
8 | targetCompatibility = JavaVersion.VERSION_1_7
9 | }
10 |
11 | dependencies{
12 | "implementation"(project(Modules.core))
13 |
14 |
15 |
16 | }
--------------------------------------------------------------------------------
/hero/hero-domain/src/main/java/com/majid2851/hero_domain/Hero.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_domain
2 |
3 | data class Hero(
4 | val id: Int,
5 | val localizedName: String,
6 | val primaryAttribute: HeroAttribute,
7 | val attackType: HeroAttackType,
8 | val roles: List,
9 | val img: String,
10 | val icon: String,
11 | val baseHealth: Float,
12 | val baseHealthRegen: Float?,
13 | val baseMana: Float,
14 | val baseManaRegen: Float?,
15 | val baseArmor: Float,
16 | val baseMoveRate: Float,
17 | val baseAttackMin: Int,
18 | val baseAttackMax: Int,
19 | val baseStr: Int,
20 | val baseAgi: Int,
21 | val baseInt: Int,
22 | val strGain: Float, // Strength gain per lvl
23 | val agiGain: Float, // Agility gain per lvl
24 | val intGain: Float, // Intelligence gain per lvl
25 | val attackRange: Int,
26 | val projectileSpeed: Int,
27 | val attackRate: Float,
28 | val moveSpeed: Int,
29 | val turnRate: Float? = 0F,
30 | val legs: Int, // How many legs does this hero have?
31 | val turboPicks: Int, // How many times picked for turbo matches?
32 | val turboWins: Int, // How many times won a turbo match?
33 | val proWins: Int = 0, // How many times won a pro match?
34 | val proPick: Int = 0, // How many times picked in pro match?
35 | val firstPick: Int, // How many times picked first?
36 | val firstWin: Int, // How many times picked first and won?
37 | val secondPick: Int, // How many times picked second?
38 | val secondWin: Int, // How many times picked second and won?
39 | val thirdPick: Int, // How many times picked third?
40 | val thirdWin: Int, // How many times picked third and won?
41 | val fourthPick: Int, // How many times picked in fourth round?
42 | val fourthWin: Int, // How many times picked in fourth round and won?
43 | val fifthPick: Int, // How many times picked fifth?
44 | val fifthWin: Int, // How many times picked fifth and won?
45 | val sixthPick: Int, // How many times picked sixth?
46 | val sixthWin: Int, // How many times picked sixth and won?
47 | val seventhPick: Int, // How many times picked seventh?
48 | val seventhWin: Int, // How many times picked seventh and won?
49 | val eighthPick: Int, // How many times picked eighth round?
50 | val eighthWin: Int, // How many times picked eighth and won?
51 | )
--------------------------------------------------------------------------------
/hero/hero-domain/src/main/java/com/majid2851/hero_domain/HeroAttackType.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_domain
2 |
3 |
4 | sealed class HeroAttackType(
5 | val uiValue: String,
6 | ){
7 |
8 | object Melee: HeroAttackType(
9 | uiValue = "Melee",
10 | )
11 |
12 | object Ranged: HeroAttackType(
13 | uiValue = "Ranged",
14 | )
15 |
16 | object Unknown: HeroAttackType(
17 | uiValue = "Unknown",
18 | )
19 | }
20 |
21 | fun getHeroAttackType(uiValue: String): HeroAttackType{
22 | return when(uiValue){
23 | HeroAttackType.Melee.uiValue -> {
24 | HeroAttackType.Melee
25 | }
26 | HeroAttackType.Ranged.uiValue -> {
27 | HeroAttackType.Ranged
28 | }
29 | else -> {
30 | HeroAttackType.Unknown
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/hero/hero-domain/src/main/java/com/majid2851/hero_domain/HeroAttribute.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_domain
2 |
3 | sealed class HeroAttribute(
4 | val uiValue:String,
5 | val abbreviation:String
6 | ){
7 | object Agility:HeroAttribute(
8 | uiValue = "Agility",
9 | abbreviation = "agi"
10 | )
11 |
12 | object Strength:HeroAttribute(
13 | uiValue = "Strength",
14 | abbreviation = "str"
15 | )
16 |
17 |
18 | object Intelligence:HeroAttribute(
19 | uiValue = "Intelligence",
20 | abbreviation = "int"
21 | )
22 |
23 | object Unknown:HeroAttribute(
24 | uiValue = "Unknown",
25 | abbreviation = "unknown"
26 | )
27 |
28 | fun getHeroAttrFromUiValue(uiValue: String): HeroAttribute{
29 | return when(uiValue){
30 | HeroAttribute.Agility.uiValue -> {
31 | HeroAttribute.Agility
32 | }
33 | HeroAttribute.Strength.uiValue -> {
34 | HeroAttribute.Strength
35 | }
36 | HeroAttribute.Intelligence.uiValue -> {
37 | HeroAttribute.Intelligence
38 | }
39 | else -> HeroAttribute.Unknown
40 | }
41 | }
42 |
43 | fun getHeroAttrFromAbbreviation(abbreviation: String): HeroAttribute{
44 | return when(abbreviation){
45 | HeroAttribute.Agility.abbreviation -> {
46 | HeroAttribute.Agility
47 | }
48 | HeroAttribute.Strength.abbreviation -> {
49 | HeroAttribute.Strength
50 | }
51 | HeroAttribute.Intelligence.abbreviation -> {
52 | HeroAttribute.Intelligence
53 | }
54 | else -> HeroAttribute.Unknown
55 | }
56 | }
57 |
58 | fun Hero.minAttackDmg(): Int {
59 | return when(primaryAttribute){
60 | is HeroAttribute.Strength -> {
61 | baseAttackMin + baseStr
62 | }
63 | is HeroAttribute.Agility -> {
64 | baseAttackMin + baseAgi
65 | }
66 | is HeroAttribute.Intelligence -> {
67 | baseAttackMin + baseInt
68 | }
69 | is HeroAttribute.Unknown -> {
70 | 0
71 | }
72 | }
73 | }
74 |
75 | fun Hero.maxAttackDmg(): Int {
76 | return when (primaryAttribute) {
77 | is HeroAttribute.Strength -> {
78 | baseAttackMax + baseStr
79 | }
80 |
81 | is HeroAttribute.Agility -> {
82 | baseAttackMax + baseAgi
83 | }
84 |
85 | is HeroAttribute.Intelligence -> {
86 | baseAttackMax + baseInt
87 | }
88 |
89 | is HeroAttribute.Unknown -> {
90 | 0
91 | }
92 | }
93 | }
94 |
95 | }
--------------------------------------------------------------------------------
/hero/hero-domain/src/main/java/com/majid2851/hero_domain/HeroFilter.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_domain
2 |
3 | import com.majid2851.core.FilterOrder
4 |
5 | sealed class HeroFilter(val uiValue:String)
6 | {
7 | data class Hero(
8 | val order:FilterOrder= FilterOrder.Descending
9 | ):HeroFilter("Hero")
10 |
11 | data class ProWins(
12 | val order:FilterOrder= FilterOrder.Descending
13 | ):HeroFilter("Pro win-rate")
14 |
15 |
16 | }
--------------------------------------------------------------------------------
/hero/hero-domain/src/main/java/com/majid2851/hero_domain/HeroRole.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_domain
2 |
3 | sealed class HeroRole(
4 | val uiValue: String,
5 | ){
6 |
7 | object Carry: HeroRole(
8 | uiValue = "Carry"
9 | )
10 |
11 | object Escape: HeroRole(
12 | uiValue = "Escape"
13 | )
14 |
15 | object Nuker: HeroRole(
16 | uiValue = "Nuker"
17 | )
18 |
19 | object Initiator: HeroRole(
20 | uiValue = "Initiator"
21 | )
22 |
23 | object Durable: HeroRole(
24 | uiValue = "Durable"
25 | )
26 |
27 | object Disabler: HeroRole(
28 | uiValue = "Disabler"
29 | )
30 |
31 | object Jungler: HeroRole(
32 | uiValue = "Jungler"
33 | )
34 |
35 | object Support: HeroRole(
36 | uiValue = "Support"
37 | )
38 |
39 | object Pusher: HeroRole(
40 | uiValue = "Pusher"
41 | )
42 |
43 | object Unknown: HeroRole(
44 | uiValue = "Unknown"
45 | )
46 | }
47 |
48 | fun getHeroRole(uiValue: String): HeroRole{
49 | return when(uiValue){
50 | HeroRole.Carry.uiValue -> {
51 | HeroRole.Carry
52 | }
53 | HeroRole.Escape.uiValue -> {
54 | HeroRole.Escape
55 | }
56 | HeroRole.Nuker.uiValue -> {
57 | HeroRole.Nuker
58 | }
59 | HeroRole.Initiator.uiValue -> {
60 | HeroRole.Initiator
61 | }
62 | HeroRole.Durable.uiValue -> {
63 | HeroRole.Durable
64 | }
65 | HeroRole.Disabler.uiValue -> {
66 | HeroRole.Disabler
67 | }
68 | HeroRole.Jungler.uiValue -> {
69 | HeroRole.Jungler
70 | }
71 | HeroRole.Support.uiValue -> {
72 | HeroRole.Support
73 | }
74 | HeroRole.Pusher.uiValue -> {
75 | HeroRole.Pusher
76 | }
77 | else -> {
78 | HeroRole.Unknown
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/hero/hero-interactors/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/hero/hero-interactors/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_7
8 | targetCompatibility = JavaVersion.VERSION_1_7
9 | }
10 |
11 | dependencies{
12 | "implementation"(project(Modules.core))
13 | "implementation"(project(Modules.heroDataSource))
14 |
15 | "implementation"(project(Modules.heroDomain))
16 | "implementation"(Kotlinx.coroutinesCore)
17 |
18 |
19 | "testImplementation"(project(Modules.heroDataSourceTest))
20 | "testImplementation"(Junit.junit4)
21 | "testImplementation"(Ktor.ktorClientMock)
22 | "testImplementation"(Ktor.clientSerialization)
23 |
24 |
25 | }
--------------------------------------------------------------------------------
/hero/hero-interactors/src/main/java/com/majid2851/hero_interactors/FilterHeros.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_interactors
2 |
3 | import com.majid2851.core.DataState
4 | import com.majid2851.core.FilterOrder
5 | import com.majid2851.hero_domain.Hero
6 | import com.majid2851.hero_domain.HeroAttribute
7 | import com.majid2851.hero_domain.HeroFilter
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlin.math.round
10 |
11 | class FilterHeros
12 | {
13 | fun excecute(
14 | current:List,
15 | heroName:String,
16 | heroFilter: HeroFilter,
17 | attrFilter:HeroAttribute
18 | ): List{
19 | var filterdList:MutableList = current.filter {
20 | it.localizedName.lowercase().contains(heroName.lowercase())
21 | }.toMutableList()
22 |
23 | when(heroFilter)
24 | {
25 | is HeroFilter.Hero ->{
26 | when(heroFilter.order)
27 | {
28 | is FilterOrder.Descending ->{
29 | filterdList.sortByDescending { it.localizedName }
30 | }
31 | is FilterOrder.Ascending ->{
32 | filterdList.sortBy { it.localizedName}
33 | }
34 | else -> {}
35 | }
36 | }
37 |
38 |
39 | is HeroFilter.ProWins ->{
40 | when(heroFilter.order)
41 | {
42 | is FilterOrder.Descending ->{
43 | filterdList.sortByDescending {
44 | getWinRate(it.proWins.toDouble(),it.proPick.toDouble())
45 | }
46 | }
47 | is FilterOrder.Ascending ->{
48 | filterdList.sortBy {
49 | getWinRate(it.proWins.toDouble(),it.proPick.toDouble())
50 | }
51 | }
52 | else -> {}
53 | }
54 |
55 |
56 |
57 | }
58 |
59 |
60 | else -> {}
61 | }
62 |
63 | when(attrFilter)
64 | {
65 | is HeroAttribute.Strength ->{
66 | filterdList=filterdList.filter {
67 | it.primaryAttribute is HeroAttribute.Strength
68 | }.toMutableList()
69 | }
70 | is HeroAttribute.Agility ->{
71 | filterdList=filterdList.filter {
72 | it.primaryAttribute is HeroAttribute.Agility
73 | }.toMutableList()
74 | }
75 | is HeroAttribute.Intelligence ->{
76 | filterdList=filterdList.filter {
77 | it.primaryAttribute is HeroAttribute.Intelligence
78 | }.toMutableList()
79 | }
80 | is HeroAttribute.Unknown ->{
81 | //don't filter
82 | }
83 |
84 |
85 | else -> {}
86 | }
87 | return filterdList
88 |
89 |
90 | }
91 |
92 | private fun getWinRate(proWins:Double,proPick:Double) = if (proPick <= 0) {
93 | 0
94 | } else {
95 | val winRate: Int = round(
96 | proWins.toDouble() /
97 | proPick.toDouble() * 100
98 | ).toInt()
99 | winRate
100 | }
101 |
102 |
103 | }
--------------------------------------------------------------------------------
/hero/hero-interactors/src/main/java/com/majid2851/hero_interactors/GetHeroFromCache.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_interactors
2 |
3 | import com.majid2851.core.DataState
4 | import com.majid2851.core.ProgressBarState
5 | import com.majid2851.core.UIComponent
6 | import com.majid2851.hero_datasource.cache.HeroCache
7 | import com.majid2851.hero_domain.Hero
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flow
10 | import javax.xml.crypto.Data
11 |
12 | class GetHeroFromCache(
13 | private val cache:HeroCache,
14 | )
15 | {
16 | fun execute(
17 | id:Int,
18 | ): Flow> = flow {
19 | try {
20 | emit(DataState.Loading(progressBarState = ProgressBarState.Idle))
21 |
22 | val cacheHero=cache.getHero(id)
23 | if(cacheHero==null)
24 | {
25 | throw Exception("That hero does not exist in the cache")
26 | }
27 | emit(DataState.Data(cacheHero))
28 |
29 | }catch (e:Exception){
30 | e.printStackTrace()
31 | emit(
32 | DataState.Response(
33 | uiComponent = UIComponent.Dialog(
34 | title = "Error",
35 | description = e.message ?:"Unknown Error"
36 | )
37 | )
38 | )
39 | }
40 | finally {
41 | emit(DataState.Loading(progressBarState = ProgressBarState.Idle))
42 | }
43 | }
44 |
45 |
46 |
47 |
48 |
49 | }
--------------------------------------------------------------------------------
/hero/hero-interactors/src/main/java/com/majid2851/hero_interactors/GetHeros.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_interactors
2 |
3 | import com.majid2851.core.DataState
4 | import com.majid2851.core.ProgressBarState
5 | import com.majid2851.core.UIComponent
6 | import com.majid2851.hero_datasource.cache.HeroCache
7 | import com.majid2851.hero_datasource.network.HeroService
8 | import com.majid2851.hero_domain.Hero
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flow
11 | import kotlinx.coroutines.flow.merge
12 |
13 | class GetHeros(
14 | private val heroCache: HeroCache,
15 | private val service:HeroService
16 | )
17 | {
18 | fun excecute():Flow>> = flow{
19 | try {
20 | emit(DataState.Loading(progressBarState = ProgressBarState.Loading))
21 | val heros:List = try {
22 | service.getHeroStats()
23 | }catch (e:Exception){
24 | e.printStackTrace()
25 | emit(
26 | DataState.Response(
27 | uiComponent = UIComponent.Dialog(
28 | title = "Network Data ERROR",
29 | description = e.message?:"Unknown Error"
30 | )
31 | )
32 | )
33 | listOf()
34 | }
35 | heroCache.insert(heros)
36 | val cachedHeros=heroCache.selectAll()
37 |
38 | emit(DataState.Data(data = cachedHeros))
39 |
40 |
41 | }catch (e:Exception){
42 | e.printStackTrace()
43 | emit(
44 | DataState.Response(
45 | uiComponent = UIComponent.Dialog(
46 | title = "Error",
47 | description = e.message ?:"Unknown Error"
48 | )
49 | )
50 | )
51 | }
52 | finally {
53 | emit(DataState.Loading(progressBarState = ProgressBarState.Idle))
54 | }
55 | }
56 |
57 |
58 | }
--------------------------------------------------------------------------------
/hero/hero-interactors/src/main/java/com/majid2851/hero_interactors/HeroInteractors.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_interactors
2 |
3 | import com.majid2851.hero_datasource.cache.HeroCache
4 | import com.majid2851.hero_datasource.network.HeroService
5 | import com.squareup.sqldelight.db.SqlDriver
6 |
7 | class HeroInteractors(
8 | val getHeros: GetHeros,
9 | val getHeroFromCache: GetHeroFromCache,
10 | val filterHeros: FilterHeros
11 | )
12 | {
13 | companion object Factory{
14 | fun build(sqlDriver: SqlDriver):HeroInteractors{
15 | val service=HeroService.build()
16 | val cache=HeroCache.build(sqlDriver)
17 |
18 | return HeroInteractors(
19 | getHeros = GetHeros(
20 | service=service,
21 | heroCache = cache,
22 | ),
23 | getHeroFromCache = GetHeroFromCache(
24 | cache=cache
25 | ),
26 | filterHeros = FilterHeros()
27 | )
28 | }
29 |
30 | val schema: SqlDriver.Schema = HeroCache.schema
31 |
32 | val dbName: String = HeroCache.dbName
33 |
34 |
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/hero/hero-interactors/src/test/java/com/majid2851/hero_interactors/GetHerosTest.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.hero_interactors
2 |
3 | import com.majid2851.core.DataState
4 | import com.majid2851.core.ProgressBarState
5 | import com.majid2851.core.UIComponent
6 | import com.majid2851.hero_datasource.cache.HeroDatabase
7 | import com.majid2851.hero_datasource_test.cache.HeroCacheFake
8 | import com.majid2851.hero_datasource_test.cache.HeroDatabaseFake
9 | import com.majid2851.hero_datasource_test.network.HeroServiceFake
10 | import com.majid2851.hero_datasource_test.network.HeroServiceResponseType
11 | import com.majid2851.hero_datasource_test.network.data.HeroDataMalformed
12 | import com.majid2851.hero_datasource_test.network.data.HeroDataValid
13 | import com.majid2851.hero_datasource_test.network.data.HeroDataValid.NUM_HEROS
14 | import com.majid2851.hero_datasource_test.serialzeHeroData
15 | import com.majid2851.hero_domain.Hero
16 | import kotlinx.coroutines.flow.toList
17 | import kotlinx.coroutines.runBlocking
18 | import org.junit.Test
19 |
20 | class GetHerosTest
21 | {
22 | private lateinit var getHeros: GetHeros
23 |
24 | @Test
25 | fun getHeros_success()
26 | {
27 | runBlocking {
28 | val heroDatabase= HeroDatabaseFake()
29 | val heroCache=HeroCacheFake(
30 | heroDatabase
31 | )
32 | val heroService=HeroServiceFake().build(
33 | type=HeroServiceResponseType.GoodData
34 | )
35 |
36 | getHeros=GetHeros(
37 | heroCache = heroCache,
38 | service = heroService
39 | )
40 |
41 |
42 | var cachedHeros=heroCache.selectAll()
43 | assert(cachedHeros.isEmpty())
44 |
45 |
46 | val emission=getHeros.excecute().toList()
47 |
48 | assert(emission[0]==DataState.Loading>(ProgressBarState.Loading))
49 |
50 |
51 | assert(emission[1] is DataState.Data)
52 |
53 | assert((emission[1] as DataState.Data).data?.size ?: 0 == NUM_HEROS)
54 |
55 | cachedHeros=heroCache.selectAll()
56 |
57 |
58 | assert(
59 | cachedHeros.size== NUM_HEROS
60 | )
61 |
62 | assert(emission[2]==DataState.Loading>(ProgressBarState.Idle))
63 |
64 | }
65 |
66 | }
67 |
68 |
69 |
70 | @Test
71 | fun getHeros_malformed()= runBlocking {
72 | val heroDatabase= HeroDatabaseFake()
73 | val heroCache=HeroCacheFake(
74 | heroDatabase
75 | )
76 | val heroService=HeroServiceFake().build(
77 | type=HeroServiceResponseType.MalformedData
78 | )
79 |
80 | getHeros=GetHeros(
81 | heroCache = heroCache,
82 | service = heroService
83 | )
84 |
85 | var cachedHeros=heroCache.selectAll()
86 | assert(cachedHeros.isEmpty())
87 |
88 | val heroData= serialzeHeroData(HeroDataValid.data)
89 | heroCache.insert(heroData)
90 |
91 | cachedHeros=heroCache.selectAll()
92 | assert(!cachedHeros.isEmpty())
93 |
94 | val emission=getHeros.excecute().toList()
95 |
96 |
97 | assert(emission[0]==DataState.Loading>(ProgressBarState.Loading))
98 |
99 | assert(emission[1] is DataState.Response)
100 |
101 | println("------------------------------->"+
102 | ((emission[1] as DataState.Response).uiComponent as UIComponent.Dialog).title)
103 | assert(
104 | ((emission[1] as DataState.Response).uiComponent as UIComponent.Dialog).title
105 | =="Error"
106 | )
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | assert(emission[2]==DataState.Loading>(ProgressBarState.Idle))
115 |
116 |
117 |
118 |
119 |
120 | }
121 |
122 | @Test
123 | fun getHero_EmptyList()= runBlocking {
124 | val heroDatabase= HeroDatabaseFake()
125 | val heroCache=HeroCacheFake(
126 | heroDatabase
127 | )
128 | val heroService=HeroServiceFake().build(
129 | type=HeroServiceResponseType.EmptyList
130 | )
131 |
132 | getHeros=GetHeros(
133 | heroCache = heroCache,
134 | service = heroService
135 | )
136 |
137 | val emission=getHeros.excecute().toList()
138 |
139 | assert(emission[0]==DataState.Loading>(ProgressBarState.Loading))
140 |
141 | assert(emission[1] is DataState.Data)
142 |
143 | assert((emission[1] as DataState.Data).data?.size ?: 0 == 0)
144 |
145 | assert(emission[2]==DataState.Loading>(ProgressBarState.Idle))
146 | }
147 |
148 |
149 |
150 | }
--------------------------------------------------------------------------------
/hero/ui-heroDetail/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 |
6 | id("com.google.dagger.hilt.android")
7 | }
8 |
9 | android {
10 | namespace 'com.majid2851.ui_herodetail'
11 | compileSdk 33
12 |
13 | defaultConfig {
14 | minSdk 24
15 | targetSdk 33
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 | kotlinOptions {
36 | jvmTarget = '1.8'
37 | }
38 | buildFeatures {
39 | compose true
40 | }
41 |
42 | composeOptions {
43 | kotlinCompilerExtensionVersion '1.3.2'
44 | }
45 | packagingOptions {
46 | resources {
47 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
48 | }
49 | }
50 | }
51 |
52 | dependencies {
53 | "implementation"(project(Modules.heroInteractors))
54 | "implementation"(project(Modules.core))
55 |
56 | "implementation"(project(Modules.components))
57 | "implementation"(project(Modules.heroDomain))
58 |
59 | "implementation"(Hilt.android)
60 | "kapt"(Hilt.compiler)
61 | "implementation"(Coil.coil)
62 |
63 |
64 | "androidTestImplementation"(project(Modules.heroDataSourceTest))
65 | "androidTestImplementation"(ComposeTest.uiTestJunit4)
66 | "debugImplementation"(ComposeTest.uiTestManifest)
67 | "androidTestImplementation"(Junit.junit4)
68 |
69 |
70 |
71 |
72 |
73 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20")
74 |
75 |
76 | implementation 'androidx.core:core-ktx:1.10.0'
77 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
78 | implementation 'androidx.activity:activity-compose:1.7.1'
79 | implementation platform('androidx.compose:compose-bom:2022.10.00')
80 | implementation 'androidx.compose.ui:ui'
81 | implementation 'androidx.compose.ui:ui-graphics'
82 | implementation 'androidx.compose.ui:ui-tooling-preview'
83 | implementation 'androidx.compose.material3:material3'
84 | implementation 'com.google.android.material:material:1.8.0'
85 |
86 |
87 | testImplementation 'junit:junit:4.13.2'
88 |
89 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
90 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
91 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
92 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
93 |
94 | debugImplementation 'androidx.compose.ui:ui-tooling'
95 | debugImplementation 'androidx.compose.ui:ui-test-manifest'
96 |
97 |
98 |
99 | }
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/androidTest/java/com/majid2851/ui_herodetail/coil/FakeImageLoader.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herodetail.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 | }
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/androidTest/java/com/majid2851/ui_herodetail/ui/HeroDetailTest.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.ui
2 |
3 | import androidx.compose.runtime.remember
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithText
7 | import androidx.test.platform.app.InstrumentationRegistry
8 | import coil.ImageLoader
9 | import com.majid2851.hero_datasource_test.network.data.HeroDataValid
10 | import com.majid2851.hero_datasource_test.serialzeHeroData
11 | import com.majid2851.ui_herodetail.coil.FakeImageLoader
12 | import com.majid2851.ui_herodetail.ui.HeroDetail
13 | import com.majid2851.ui_herodetail.ui.HeroDetailState
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import kotlin.random.Random
17 |
18 | class HeroDetailTest
19 | {
20 |
21 | @get:Rule
22 | val composeTestRule= createComposeRule()
23 |
24 | private val context=InstrumentationRegistry.getInstrumentation().targetContext
25 | private val imageLoader:ImageLoader = FakeImageLoader.build(context)
26 | private val heroData= serialzeHeroData(HeroDataValid.data)
27 |
28 | @Test
29 | fun isHeroDetailShown()
30 | {
31 | val hero=heroData.get(
32 | Random.nextInt(0,heroData.size-1)
33 | )
34 | composeTestRule.setContent {
35 | val state=remember{
36 | HeroDetailState(
37 | hero=hero
38 | )
39 | }
40 | HeroDetail(
41 | state = state,
42 | event = {},
43 | imageLoader = imageLoader
44 |
45 | )
46 | }
47 | composeTestRule.onNodeWithText(
48 | hero.localizedName
49 | ).assertIsDisplayed()
50 | }
51 |
52 |
53 | }
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/java/com/majid2851/ui_herodetail/di/HeroDetailModule.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herodetail.di
2 |
3 | import com.majid2851.hero_interactors.GetHeroFromCache
4 | import com.majid2851.hero_interactors.HeroInteractors
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class )
13 | object HeroDetailModule
14 | {
15 | @Provides
16 | @Singleton
17 | fun provideGetHeroFromCache(
18 | interactors:HeroInteractors
19 | ):GetHeroFromCache
20 | {
21 | return interactors.getHeroFromCache
22 | }
23 |
24 |
25 |
26 |
27 | }
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/java/com/majid2851/ui_herodetail/ui/HeroDetailEvent.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herodetail.ui
2 |
3 | sealed class HeroDetailEvent
4 | {
5 | data class GetHeroFromCache(
6 | val id:Int
7 | ):HeroDetailEvent()
8 |
9 | object OnRemoveHeadFromQueue:HeroDetailEvent()
10 |
11 |
12 |
13 |
14 |
15 |
16 | }
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/java/com/majid2851/ui_herodetail/ui/HeroDetailState.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herodetail.ui
2 |
3 | import com.majid2851.core.ProgressBarState
4 | import com.majid2851.core.Queue
5 | import com.majid2851.core.UIComponent
6 | import com.majid2851.hero_domain.Hero
7 |
8 | data class HeroDetailState(
9 | val progressBarState:ProgressBarState=ProgressBarState.Idle,
10 | val hero: Hero?=null,
11 | val errorQueue: Queue = Queue(mutableListOf())
12 | )
13 | {
14 |
15 | }
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/java/com/majid2851/ui_herodetail/ui/HeroDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herodetail.ui
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.SavedStateHandle
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.majid2851.core.DataState
9 | import com.majid2851.core.Logger
10 | import com.majid2851.core.ProgressBarState
11 | import com.majid2851.core.Queue
12 | import com.majid2851.core.UIComponent
13 | import com.majid2851.hero_interactors.GetHeroFromCache
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.flow.launchIn
16 | import kotlinx.coroutines.flow.onEach
17 | import javax.inject.Inject
18 | import javax.inject.Named
19 |
20 | @HiltViewModel
21 | class HeroDetailViewModel @Inject
22 | constructor(
23 | private val getHeroFromCache: GetHeroFromCache,
24 | private val savedStateHandle: SavedStateHandle,
25 | // private @Named(HERO_LIST_LOGGER) val logger:Logger
26 | ):ViewModel()
27 | {
28 | val state:MutableState = mutableStateOf(HeroDetailState())
29 |
30 | init {
31 | savedStateHandle.get("heroId")?.let {id->
32 | onTriggerEvent(HeroDetailEvent.GetHeroFromCache(id))
33 | }
34 |
35 | }
36 | fun onTriggerEvent(event: HeroDetailEvent)
37 | {
38 | when(event)
39 | {
40 | is HeroDetailEvent.GetHeroFromCache ->{
41 | getHeroFromCache(event.id)
42 | }
43 | is HeroDetailEvent.OnRemoveHeadFromQueue->{
44 | removeHeadMessage()
45 | }
46 |
47 |
48 | else -> {}
49 | }
50 |
51 | }
52 |
53 | private fun getHeroFromCache(id: Int)
54 | {
55 | getHeroFromCache.execute(id).onEach {dataState->
56 | when(dataState)
57 | {
58 | is DataState.Loading ->{
59 | state.value=state.value.copy(progressBarState = dataState.progressBarState)
60 | }
61 | is DataState.Data ->{
62 | state.value=state.value.copy(hero = dataState.data)
63 | }
64 | is DataState.Response ->{
65 | if(dataState.uiComponent is UIComponent.None){
66 | // logger.log("getHeros: ${(it.uiComponent as UIComponent.None).message}")
67 | }
68 | else{
69 | appendToMessageQueue(dataState.uiComponent)
70 | }
71 | }
72 |
73 |
74 | else -> {}
75 | }
76 | }.launchIn(viewModelScope)
77 | }
78 |
79 |
80 | private fun appendToMessageQueue(uiComponent: UIComponent)
81 | {
82 | val queue=state.value.errorQueue
83 | queue.add(uiComponent)
84 | state.value=state.value.copy(errorQueue = Queue(mutableListOf()))
85 | state.value=state.value.copy(errorQueue = queue)
86 |
87 | }
88 |
89 | private fun removeHeadMessage()
90 | {
91 | try {
92 | val queue=state.value.errorQueue
93 | queue.remove()
94 | state.value = state.value.copy(errorQueue = Queue(mutableListOf())) // force recompose
95 | state.value = state.value.copy(errorQueue = queue)
96 |
97 |
98 | }catch (e:Exception){
99 | // logger.log("nothing for removing for dialogQueue")
100 | }
101 | }
102 |
103 |
104 | }
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/res/drawable/black_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/hero/ui-heroDetail/src/main/res/drawable/black_background.png
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/res/drawable/error_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/hero/ui-heroDetail/src/main/res/drawable/error_image.png
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/res/drawable/white_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/hero/ui-heroDetail/src/main/res/drawable/white_background.png
--------------------------------------------------------------------------------
/hero/ui-heroDetail/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Strength
4 | Agility
5 | Intelligence
6 | Attack Range
7 | Projectile Speed
8 | Health
9 | Move Speed
10 | Attack dmg
11 |
--------------------------------------------------------------------------------
/hero/ui-heroList/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 |
6 | id("com.google.dagger.hilt.android")
7 |
8 | }
9 |
10 | android {
11 | compileSdk 33
12 |
13 | defaultConfig {
14 | minSdk 24
15 | targetSdk 33
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | }
24 |
25 | kapt {
26 | correctErrorTypes = true
27 | }
28 | buildTypes {
29 | release {
30 | minifyEnabled false
31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
32 | }
33 | }
34 | compileOptions {
35 | sourceCompatibility JavaVersion.VERSION_1_8
36 | targetCompatibility JavaVersion.VERSION_1_8
37 | }
38 | kotlinOptions {
39 | jvmTarget = '1.8'
40 | }
41 | buildFeatures {
42 | compose true
43 | }
44 |
45 | composeOptions {
46 | kotlinCompilerExtensionVersion '1.3.2'
47 | }
48 | packagingOptions {
49 | resources {
50 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
51 | }
52 | }
53 | }
54 |
55 | dependencies {
56 | "implementation"(project(Modules.core))
57 | "implementation"(project(Modules.heroDomain))
58 | "implementation"(project(Modules.heroInteractors))
59 | "implementation"(SqlDelight.androidDriver)
60 | "implementation"(Coil.coil)
61 |
62 | "implementation"(project(Modules.components))
63 |
64 | "implementation"(Hilt.android)
65 | "kapt"(Hilt.compiler)
66 |
67 | "androidTestImplementation"(project(Modules.heroDataSourceTest))
68 | "androidTestImplementation"(ComposeTest.uiTestJunit4)
69 | "debugImplementation"(ComposeTest.uiTestManifest)
70 | "androidTestImplementation"(Junit.junit4)
71 |
72 |
73 |
74 |
75 |
76 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20")
77 | implementation 'androidx.core:core-ktx:1.10.0'
78 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
79 | implementation 'androidx.activity:activity-compose:1.7.1'
80 | implementation platform('androidx.compose:compose-bom:2022.10.00')
81 | implementation 'androidx.compose.ui:ui'
82 | implementation 'androidx.compose.ui:ui-graphics'
83 | implementation 'androidx.compose.ui:ui-tooling-preview'
84 | implementation 'androidx.compose.material3:material3'
85 | implementation 'com.google.android.material:material:1.8.0'
86 |
87 |
88 | testImplementation 'junit:junit:4.13.2'
89 |
90 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
91 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
92 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
93 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
94 |
95 | debugImplementation 'androidx.compose.ui:ui-tooling'
96 | debugImplementation 'androidx.compose.ui:ui-test-manifest'
97 |
98 |
99 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/androidTest/java/com/majid2851/ui_herolist/coil/FakeImageLoader.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.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 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/androidTest/java/com/majid2851/ui_herolist/ui/HeroListTest.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.ui
2 |
3 | import androidx.compose.runtime.remember
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithText
7 | import androidx.test.platform.app.InstrumentationRegistry
8 | import coil.ImageLoader
9 | import com.majid2851.hero_datasource_test.network.data.HeroDataValid
10 | import com.majid2851.hero_datasource_test.serialzeHeroData
11 | import com.majid2851.ui_herolist.coil.FakeImageLoader
12 | import org.junit.Rule
13 | import org.junit.Test
14 |
15 | class HeroListTest
16 | {
17 |
18 | @get:Rule
19 | val composeTestRule= createComposeRule()
20 |
21 | private val context=InstrumentationRegistry.getInstrumentation().targetContext
22 | private val imageLoader:ImageLoader = FakeImageLoader.build(context)
23 | private val heroData= serialzeHeroData(HeroDataValid.data)
24 |
25 | @Test
26 | fun areHerosShown()
27 | {
28 | composeTestRule.setContent {
29 | val state=remember{
30 | HeroListState(
31 | heros = heroData,
32 | filterHeros = heroData
33 | )
34 | }
35 | HeroList(
36 | state=state,
37 | events = {},
38 | navigateToDetailScreen = {},
39 | imageLoader = imageLoader
40 | )
41 |
42 | composeTestRule.onNodeWithText(
43 | "Zeus"
44 | ).assertIsDisplayed()
45 | }
46 | }
47 |
48 |
49 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/components/EmptyRow.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.components
2 |
3 |
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.unit.dp
12 |
13 | @Composable
14 | fun EmptyRow(){
15 | Row(
16 | modifier = Modifier
17 | .padding(bottom = 0.dp)
18 | .fillMaxWidth()
19 | ,
20 | ){
21 | Text(
22 | text = "",
23 | style = MaterialTheme.typography.bodySmall,
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/components/HeroListItem.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.isSystemInDarkTheme
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Surface
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.layout.ContentScale
16 | import androidx.compose.ui.platform.testTag
17 | import androidx.compose.ui.semantics.Role.Companion.Image
18 | import androidx.compose.ui.text.style.TextOverflow
19 | import androidx.compose.ui.unit.dp
20 | import coil.ImageLoader
21 | import coil.compose.rememberImagePainter
22 | import com.majid2851.hero_domain.Hero
23 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_NAME
24 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_PRIMARY_ATTRIBUTE
25 | import kotlin.math.round
26 | import com.majid2851.ui_herolist.R
27 |
28 |
29 | @Composable
30 | fun HeroListItem(
31 | hero: Hero,
32 | onSelectHero: (Int) -> Unit,
33 | imageLoader: ImageLoader,
34 | ){
35 | Surface(
36 | modifier = Modifier
37 | .fillMaxWidth()
38 | .padding(bottom = 8.dp)
39 | .background(MaterialTheme.colorScheme.background)
40 | .clickable {
41 | onSelectHero(hero.id)
42 | }
43 | ,
44 | tonalElevation = 8.dp
45 | , shadowElevation = 8.dp
46 | ){
47 | Row(
48 | modifier = Modifier
49 | .fillMaxWidth()
50 | .background(color = Color.White)
51 | ,
52 | verticalAlignment = Alignment.CenterVertically
53 | ){
54 | val painter= rememberImagePainter(
55 | hero.img,
56 | imageLoader=imageLoader,
57 | builder = {
58 | placeholder(if (isSystemInDarkTheme())
59 | R.drawable.black_background
60 | else R.drawable.white_background)
61 | }
62 | )
63 | Image( // TODO(Replace with Image)
64 | modifier = Modifier
65 | .width(120.dp)
66 | .height(70.dp)
67 | .background(Color.LightGray)
68 | , painter = painter
69 | , contentDescription = hero.localizedName
70 | , contentScale = ContentScale.Crop
71 | )
72 | Column(
73 | modifier = Modifier
74 | .fillMaxWidth(.8f) // fill 80% of remaining width
75 | .padding(start = 12.dp)
76 | ) {
77 | Text(
78 | modifier = Modifier
79 | .padding(bottom = 4.dp)
80 | .testTag(TAG_HERO_NAME)
81 | ,
82 | text = hero.localizedName,
83 | style = MaterialTheme.typography.titleMedium,
84 | maxLines = 1,
85 | overflow = TextOverflow.Ellipsis
86 | )
87 | Text(
88 | modifier = Modifier
89 | .testTag(TAG_HERO_PRIMARY_ATTRIBUTE)
90 | ,
91 | text = hero.primaryAttribute.uiValue,
92 | style = MaterialTheme.typography.titleSmall,
93 | )
94 | }
95 | Column(
96 | modifier = Modifier
97 | .fillMaxWidth() // Fill the rest of the width (100% - 80% = 20%)
98 | .padding(end = 12.dp)
99 | ,
100 | horizontalAlignment = Alignment.End
101 | ) {
102 | // Using remember in list item does not behave correctly?
103 | // val proWR: Int = remember{round(hero.proWins.toDouble() / hero.proPick.toDouble() * 100).toInt()}
104 | val proWR: Int = round(hero.proWins.toDouble() / hero.proPick.toDouble() * 100).toInt()
105 | Text(
106 | text = "${proWR}%",
107 | style = MaterialTheme.typography.titleMedium,
108 | color = if(proWR > 50) Color(0xFF009a34) else MaterialTheme.colorScheme.error,
109 | )
110 | }
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/components/HeroListToolbar.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.text.KeyboardActions
9 | import androidx.compose.foundation.text.KeyboardOptions
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.MoreVert
12 | import androidx.compose.material.icons.filled.Search
13 | import androidx.compose.material3.*
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Surface
17 | import androidx.compose.material3.Text
18 | import androidx.compose.material3.TextField
19 | import androidx.compose.material3.TextFieldDefaults
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.ExperimentalComposeUiApi
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController
25 | import androidx.compose.ui.platform.testTag
26 | import androidx.compose.ui.text.TextStyle
27 | import androidx.compose.ui.text.input.ImeAction
28 | import androidx.compose.ui.text.input.KeyboardType
29 | import androidx.compose.ui.unit.dp
30 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_BTN
31 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_SEARCH_BAR
32 |
33 | @OptIn(ExperimentalMaterial3Api::class)
34 | @ExperimentalComposeUiApi
35 | @Composable
36 | fun HeroListToolbar(
37 | heroName: String,
38 | onHeroNameChanged: (String) -> Unit,
39 | onExecuteSearch: () -> Unit,
40 | onShowFilterDialog: () -> Unit,
41 | ) {
42 | val keyboardController = LocalSoftwareKeyboardController.current
43 | Surface(
44 | modifier = Modifier
45 | .fillMaxWidth(),
46 | color = MaterialTheme.colorScheme.secondary,
47 | shadowElevation = 12.dp,
48 | ) {
49 | Row(
50 | modifier = Modifier
51 | .fillMaxWidth()
52 | ) {
53 | TextField(
54 | modifier = Modifier
55 | .fillMaxWidth(.9f)
56 | .padding(8.dp)
57 | .testTag(TAG_HERO_SEARCH_BAR)
58 | ,
59 | value = heroName,
60 | onValueChange = {
61 | onHeroNameChanged(it)
62 | onExecuteSearch()
63 | },
64 | label = { Text(text = "Search") },
65 | keyboardOptions = KeyboardOptions(
66 | keyboardType = KeyboardType.Text,
67 | imeAction = ImeAction.Done,
68 | ),
69 | keyboardActions = KeyboardActions(
70 | onDone = {
71 | onExecuteSearch()
72 | keyboardController?.hide()
73 | },
74 | ),
75 | leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search Icon") },
76 | textStyle = TextStyle(color = MaterialTheme.colorScheme.onSurface),
77 | colors = TextFieldDefaults.textFieldColors(containerColor = MaterialTheme.colorScheme.surface),
78 | )
79 | Column(
80 | modifier = Modifier
81 | .align(Alignment.CenterVertically)
82 | .clickable {
83 | onShowFilterDialog()
84 | }
85 | ) {
86 | Icon(
87 | modifier = Modifier
88 | .padding(8.dp)
89 | .testTag(TAG_HERO_FILTER_BTN)
90 | ,
91 | imageVector = Icons.Filled.MoreVert,
92 | contentDescription = "Filter Icon"
93 | )
94 | }
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/components/OrderSelector.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.components
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.interaction.MutableInteractionSource
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.Checkbox
11 | import androidx.compose.material3.CheckboxDefaults
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.testTag
18 | import androidx.compose.ui.unit.dp
19 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_ASC
20 | import com.majid2851.ui_herolist.ui.test.TAG_HERO_FILTER_DESC
21 |
22 | /**
23 | * @param descString: String displayed in the "descending" checkbox
24 | * @param ascString: String displayed in the "ascending" checkbox
25 | * @param isEnabled: Is this HeroFilter currently the selected HeroFilter?
26 | * @param isDescSelected: Is the "descending" checkbox selected?
27 | * @param isAscSelected: Is the "ascending" checkbox selected?
28 | * @param onUpdateHeroFilterDesc: Set the filter to Descending.
29 | * @param onUpdateHeroFilterAsc: Set the filter to Ascending.
30 | */
31 | @ExperimentalAnimationApi
32 | @Composable
33 | fun OrderSelector(
34 | descString: String,
35 | ascString: String,
36 | isEnabled: Boolean,
37 | isDescSelected: Boolean,
38 | isAscSelected: Boolean,
39 | onUpdateHeroFilterDesc: () -> Unit,
40 | onUpdateHeroFilterAsc: () -> Unit,
41 | ){
42 | // Descending Order
43 | AnimatedVisibility(visible = isEnabled) {
44 | Row(
45 | modifier = Modifier
46 | .fillMaxWidth()
47 | .padding(start = 24.dp, bottom = 8.dp)
48 | .testTag(TAG_HERO_FILTER_DESC)
49 | .clickable(
50 | interactionSource = MutableInteractionSource(),
51 | indication = null, // disable the highlight
52 | enabled = isEnabled,
53 | onClick = {
54 | onUpdateHeroFilterDesc()
55 | },
56 | )
57 | ,
58 | ){
59 | Checkbox(
60 | modifier = Modifier
61 | .padding(end = 8.dp)
62 | .align(Alignment.CenterVertically)
63 | ,
64 | enabled= isEnabled,
65 | checked = isEnabled && isDescSelected,
66 | onCheckedChange = {
67 | onUpdateHeroFilterDesc()
68 | },
69 | colors = CheckboxDefaults.colors(MaterialTheme.colorScheme.primary)
70 | )
71 | Text(
72 | text = descString,
73 | style = MaterialTheme.typography.bodyLarge,
74 | )
75 | }
76 | }
77 |
78 | // Ascending Order
79 | AnimatedVisibility(visible = isEnabled) {
80 | Row(
81 | modifier = Modifier
82 | .fillMaxWidth()
83 | .padding(start = 24.dp, bottom = 8.dp)
84 | .clickable(
85 | interactionSource = MutableInteractionSource(),
86 | indication = null, // disable the highlight
87 | enabled = isEnabled,
88 | onClick = {
89 | onUpdateHeroFilterAsc()
90 | },
91 | )
92 | ,
93 | ){
94 | Checkbox(
95 | modifier = Modifier
96 | .padding(end = 8.dp)
97 | .testTag(TAG_HERO_FILTER_ASC)
98 | .align(Alignment.CenterVertically)
99 | ,
100 | enabled= isEnabled,
101 | checked = isEnabled && isAscSelected,
102 | onCheckedChange = {
103 | onUpdateHeroFilterAsc()
104 | },
105 | colors = CheckboxDefaults.colors(MaterialTheme.colorScheme.primary)
106 | )
107 | Text(
108 | text = ascString,
109 | style = MaterialTheme.typography.bodyLarge,
110 | )
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/di/HeroListModule.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.di
2 |
3 | import com.majid2851.core.Logger
4 | import com.majid2851.hero_interactors.FilterHeros
5 | import com.majid2851.hero_interactors.GetHeros
6 | import com.majid2851.hero_interactors.HeroInteractors
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 HeroListModule
17 | {
18 | const val HERO_LIST_LOGGER="heroListLogger"
19 | @Provides
20 | @Singleton
21 | @Named(HERO_LIST_LOGGER)
22 | fun provideLogger():Logger
23 | {
24 | return Logger(
25 | tag = "HeroList",
26 | isDebug = true
27 | )
28 | }
29 |
30 |
31 |
32 | @Provides
33 | @Singleton
34 | fun provideGetHeros(
35 | interactors:HeroInteractors
36 | ):GetHeros
37 | {
38 | return interactors.getHeros
39 | }
40 |
41 |
42 |
43 | @Provides
44 | @Singleton
45 | fun filterHero(
46 | interactors:HeroInteractors
47 | ):FilterHeros
48 | {
49 | return interactors.filterHeros
50 | }
51 |
52 |
53 |
54 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/ui/HeroList.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.ui
2 |
3 | import android.util.Log
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.lazy.LazyColumn
9 | import androidx.compose.foundation.lazy.items
10 | import androidx.compose.material3.CircularProgressIndicator
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.ExperimentalComposeUiApi
16 | import androidx.compose.ui.Modifier
17 | import coil.ImageLoader
18 | import com.majid2851.components.DefaultScreenUI
19 | import com.majid2851.core.ProgressBarState
20 | import com.majid2851.core.UIComponentState
21 | import com.majid2851.ui_herolist.components.HeroListFilter
22 | import com.majid2851.ui_herolist.components.HeroListItem
23 | import com.majid2851.ui_herolist.components.HeroListToolbar
24 |
25 | @OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
26 | @Composable
27 | fun HeroList(
28 | state: HeroListState,
29 | events: (HeroListEvents)->Unit,
30 | imageLoader:ImageLoader,
31 | navigateToDetailScreen:(Int)->Unit
32 | )
33 | {
34 | DefaultScreenUI(
35 | queue = state.errorQueue,
36 | onRemoveHeadFromQueue={
37 | events(HeroListEvents.OnRemoveHeadFromQueue)
38 | }
39 | )
40 | {
41 | Box(
42 | modifier= Modifier.fillMaxSize(),
43 | )
44 | {
45 | Column()
46 | {
47 |
48 | HeroListToolbar(
49 | heroName = state.heroName,
50 | onHeroNameChanged = { heroName ->
51 | events(HeroListEvents.UpdateHeroName(heroName))
52 | },
53 | onExecuteSearch = {
54 | events(HeroListEvents.FilterHeros)
55 | },
56 | onShowFilterDialog = {
57 | events(HeroListEvents.UpdateFilterDialogState(UIComponentState.Show))
58 | })
59 |
60 |
61 | LazyColumn()
62 | {
63 | Log.i("Sam2231-HeroList==>", "size:" + state.filterHeros.size.toString())
64 | items(state.filterHeros) { hero ->
65 | HeroListItem(
66 | hero = hero,
67 | onSelectHero = { heroId ->
68 | navigateToDetailScreen(heroId)
69 | },
70 | imageLoader = imageLoader
71 | )
72 |
73 |
74 | }
75 | }
76 | }
77 |
78 | if (state.filterDialogState is UIComponentState.Show)
79 | {
80 | HeroListFilter(
81 | heroFilter = state.heroFilter,
82 | onUpdateHeroFilter = {
83 | events(HeroListEvents.UpdateHeroFilter(it))
84 | },
85 | onCloseDialog ={
86 | events(HeroListEvents.UpdateFilterDialogState(UIComponentState.Hide))
87 | },
88 | attributeFilter = state.primaryAttribute,
89 | onUpdateAttributeFilter = {
90 | events(HeroListEvents.UpdateAttributeFilter(it))
91 | }
92 | )
93 | }
94 |
95 | if (state.progressBarState is ProgressBarState.Loading)
96 | {
97 | CircularProgressIndicator(
98 | modifier = Modifier.align(Alignment.Center)
99 | )
100 | }
101 |
102 |
103 | }
104 | }
105 |
106 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/ui/HeroListEvents.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.ui
2 |
3 | import com.majid2851.core.UIComponentState
4 | import com.majid2851.hero_domain.HeroAttribute
5 | import com.majid2851.hero_domain.HeroFilter
6 |
7 | sealed class HeroListEvents
8 | {
9 | object GetHeros:HeroListEvents()
10 |
11 | object FilterHeros:HeroListEvents()
12 |
13 | data class UpdateHeroName(
14 | val heroName:String
15 | ):HeroListEvents()
16 |
17 | data class UpdateHeroFilter(
18 | val heroFilter: HeroFilter
19 | ):HeroListEvents()
20 |
21 | data class UpdateFilterDialogState(
22 | val uiComponentState: UIComponentState
23 | ):HeroListEvents()
24 |
25 | data class UpdateAttributeFilter(
26 | val attribute:HeroAttribute
27 | ):HeroListEvents()
28 |
29 | object OnRemoveHeadFromQueue:HeroListEvents()
30 |
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/ui/HeroListState.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.ui
2 |
3 | import com.majid2851.core.ProgressBarState
4 | import com.majid2851.core.Queue
5 | import com.majid2851.core.UIComponent
6 | import com.majid2851.core.UIComponentState
7 | import com.majid2851.hero_domain.Hero
8 | import com.majid2851.hero_domain.HeroAttribute
9 | import com.majid2851.hero_domain.HeroFilter
10 |
11 | data class HeroListState(
12 | val progressBarState:ProgressBarState= ProgressBarState.Idle,
13 | val heros:List = listOf(),
14 | val filterHeros:List = listOf(),
15 | val heroName:String="",
16 | val heroFilter: HeroFilter=HeroFilter.Hero(),
17 | val primaryAttribute:HeroAttribute = HeroAttribute.Unknown,
18 | val filterDialogState:UIComponentState =UIComponentState.Hide,
19 | val errorQueue: Queue = Queue(mutableListOf())
20 | )
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/ui/HeroListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.ui
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.MutableState
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.majid2851.core.DataState
9 | import com.majid2851.core.Logger
10 | import com.majid2851.core.Queue
11 | import com.majid2851.core.UIComponent
12 | import com.majid2851.hero_domain.HeroAttribute
13 | import com.majid2851.hero_domain.HeroFilter
14 | import com.majid2851.hero_interactors.FilterHeros
15 | import com.majid2851.hero_interactors.GetHeros
16 | import com.majid2851.ui_herolist.di.HeroListModule.HERO_LIST_LOGGER
17 | import com.majid2851.ui_herolist.di.HeroListModule.filterHero
18 | import dagger.hilt.android.lifecycle.HiltViewModel
19 | import kotlinx.coroutines.flow.launchIn
20 | import kotlinx.coroutines.flow.onEach
21 | import javax.inject.Inject
22 | import javax.inject.Named
23 |
24 | @HiltViewModel
25 | class HeroListViewModel @Inject constructor(
26 | private val getHeros: GetHeros,
27 | private val filterHero:FilterHeros,
28 | private @Named(HERO_LIST_LOGGER) val logger: Logger
29 | ):ViewModel() {
30 |
31 | val state: MutableState =
32 | mutableStateOf(HeroListState())
33 |
34 | init {
35 | onTrigerEvent(HeroListEvents.GetHeros)
36 | }
37 |
38 | fun onTrigerEvent(event: HeroListEvents) {
39 | when (event) {
40 | is HeroListEvents.GetHeros -> {
41 | getHeros()
42 | }
43 |
44 | is HeroListEvents.FilterHeros -> {
45 | filterHeros()
46 | Log.i(
47 | "Sam2231-ViewModelFilter==>",
48 | "size:" + state.value.filterHeros.size.toString()
49 | )
50 | }
51 |
52 | is HeroListEvents.UpdateHeroName -> {
53 | updateHeroName(event.heroName)
54 | }
55 |
56 | is HeroListEvents.UpdateHeroFilter -> {
57 | updateHeroFilter(event.heroFilter)
58 | }
59 |
60 | is HeroListEvents.UpdateFilterDialogState -> {
61 | state.value = state.value.copy(filterDialogState = event.uiComponentState)
62 | }
63 |
64 | is HeroListEvents.UpdateAttributeFilter -> {
65 | updateAttributeFilter(event.attribute)
66 | }
67 |
68 | is HeroListEvents.OnRemoveHeadFromQueue -> {
69 | removeHeadMessage()
70 | }
71 |
72 | else -> {}
73 | }
74 |
75 | }
76 |
77 | private fun updateAttributeFilter(attribute: HeroAttribute) {
78 | state.value = state.value.copy(primaryAttribute = attribute)
79 | filterHeros()
80 | }
81 |
82 | private fun updateHeroFilter(heroFilter: HeroFilter) {
83 | state.value = state.value.copy(heroFilter = heroFilter)
84 | filterHeros()
85 |
86 | }
87 |
88 | private fun filterHeros() {
89 | val filteredList = filterHero.excecute(
90 | current = state.value.heros,
91 | heroName = state.value.heroName,
92 | heroFilter = state.value.heroFilter,
93 | attrFilter = state.value.primaryAttribute
94 | )
95 | state.value = state.value.copy(filterHeros = filteredList)
96 | }
97 |
98 | private fun updateHeroName(heroName: String) {
99 | state.value = state.value.copy(heroName = heroName)
100 | }
101 |
102 |
103 | private fun getHeros() {
104 | getHeros.excecute().onEach {
105 | when (it) {
106 | is DataState.Response -> {
107 | if(it.uiComponent is UIComponent.None){
108 | logger.log("getHeros: ${(it.uiComponent as UIComponent.None).message}")
109 | }
110 | else{
111 | appendToMessageQueue(it.uiComponent)
112 | }
113 |
114 |
115 | }
116 |
117 | is DataState.Data -> {
118 | state.value = state.value.copy(heros = it.data ?: listOf())
119 | Log.i(
120 | "Sam2231-dataBeforFilter==>",
121 | "size:" + state.value.filterHeros.size.toString()
122 | )
123 | filterHeros()
124 |
125 | Log.i(
126 | "Sam2231-dataBeforFilter==>",
127 | "size:" + state.value.filterHeros.size.toString()
128 | )
129 | }
130 |
131 | is DataState.Loading -> {
132 | state.value = state.value.copy(progressBarState = it.progressBarState)
133 | }
134 |
135 | }
136 | }.launchIn(viewModelScope)
137 | }
138 |
139 | private fun appendToMessageQueue(uiComponent: UIComponent){
140 | val queue = state.value.errorQueue
141 | queue.add(uiComponent)
142 | state.value = state.value.copy(errorQueue = Queue(mutableListOf())) // force recompose
143 | state.value = state.value.copy(errorQueue = queue)
144 | }
145 |
146 | private fun removeHeadMessage()
147 | {
148 | try {
149 | val queue=state.value.errorQueue
150 | queue.remove()
151 | state.value = state.value.copy(errorQueue = Queue(mutableListOf())) // force recompose
152 | state.value = state.value.copy(errorQueue = queue)
153 |
154 |
155 | }catch (e:Exception){
156 | logger.log("nothing for removing for dialogQueue")
157 | }
158 | }
159 |
160 | }
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/java/com/majid2851/ui_herolist/ui/test/TestTag.kt:
--------------------------------------------------------------------------------
1 | package com.majid2851.ui_herolist.ui.test
2 |
3 | const val TAG_HERO_SEARCH_BAR = "TAG_HERO_SEARCH_BAR"
4 | const val TAG_HERO_NAME = "TAG_HERO_NAME"
5 | const val TAG_HERO_FILTER_BTN = "TAG_HERO_FILTER_BTN"
6 | const val TAG_HERO_FILTER_DIALOG = "TAG_HERO_FILTER_DIALOG"
7 | const val TAG_HERO_FILTER_DIALOG_DONE = "TAG_HERO_FILTER_DIALOG_DONE"
8 | const val TAG_HERO_FILTER_HERO_CHECKBOX = "TAG_HERO_FILTER_HERO_CHECKBOX"
9 | const val TAG_HERO_FILTER_DESC = "TAG_HERO_FILTER_DESC"
10 | const val TAG_HERO_FILTER_ASC = "TAG_HERO_FILTER_ASC"
11 | const val TAG_HERO_FILTER_PROWINS_CHECKBOX = "TAG_HERO_FILTER_PROWINS_CHECKBOX"
12 | const val TAG_HERO_FILTER_STENGTH_CHECKBOX = "TAG_HERO_FILTER_STENGTH_CHECKBOX"
13 | const val TAG_HERO_FILTER_AGILITY_CHECKBOX = "TAG_HERO_FILTER_AGILITY_CHECKBOX"
14 | const val TAG_HERO_FILTER_INT_CHECKBOX = "TAG_HERO_FILTER_INT_CHECKBOX"
15 | const val TAG_HERO_FILTER_UNKNOWN_CHECKBOX = "TAG_HERO_FILTER_UNKNOWN_CHECKBOX"
16 | const val TAG_HERO_PRIMARY_ATTRIBUTE = "TAG_HERO_PRIMARY_ATTRIBUTE"
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/res/drawable/black_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/hero/ui-heroList/src/main/res/drawable/black_background.png
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/res/drawable/error_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/hero/ui-heroList/src/main/res/drawable/error_image.png
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/res/drawable/white_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/majid2851/Kotlin_Modularization/47f4d92f3b620c1cafa2c44d8f12b23d5fcdb65d/hero/ui-heroList/src/main/res/drawable/white_background.png
--------------------------------------------------------------------------------
/hero/ui-heroList/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | None
4 |
5 | Primary Attribute
6 |
--------------------------------------------------------------------------------
/library-build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("java-library")
3 | id("org.jetbrains.kotlin.jvm")
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_7
8 | targetCompatibility = JavaVersion.VERSION_1_7
9 | }
10 | dependencies {
11 |
12 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 |
10 |
11 |
12 | dependencyResolutionManagement {
13 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
14 | repositories {
15 | google()
16 | mavenCentral()
17 | }
18 | }
19 | rootProject.name = "Kotlin_Modularization"
20 | include ':app'
21 | include ':core'
22 | include ':hero'
23 | include ':constants'
24 | include ':component'
25 |
26 | include ':hero:hero-datasource'
27 | include ':hero:hero-datasource-test'
28 | include ':hero:hero-domain'
29 | include ':hero:hero-interactors'
30 |
31 | include ':hero:ui-heroList'
32 | include ':hero:ui-heroDetail'
33 | include ':app'
34 |
--------------------------------------------------------------------------------