├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ └── com
│ │ └── redmadrobot
│ │ └── mad
│ │ ├── AppRouter.kt
│ │ ├── MADApp.kt
│ │ ├── MainActivity.kt
│ │ └── di
│ │ └── modules
│ │ ├── AppModule.kt
│ │ └── NetworkModule.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
├── base
└── base_cards
│ ├── build.gradle.kts
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ └── com
│ │ └── redmadrobot
│ │ └── base_cards
│ │ ├── CardViewStateConverterExt.kt
│ │ └── model
│ │ ├── CardBorderColors.kt
│ │ ├── CardBorderGradient.kt
│ │ ├── CardColors.kt
│ │ ├── CardGradient.kt
│ │ ├── CardTextNumberColor.kt
│ │ ├── CardTypeIcon.kt
│ │ └── CardViewState.kt
│ └── res
│ └── drawable
│ ├── ic_circle_arrow.xml
│ ├── ic_iron_man.xml
│ ├── ic_master.xml
│ ├── ic_subtract.xml
│ └── ic_visa.xml
├── build.gradle.kts
├── buildSrc
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── AndroidConfig.kt
│ ├── CoreDependency.kt
│ ├── GradlePluginDependency.kt
│ ├── NetworkDependency.kt
│ ├── PresentationDependency.kt
│ └── com
│ └── redmadrobot
│ └── mad
│ └── plugins
│ ├── CommonAndroidConfigPlugin.kt
│ └── Extensions.kt
├── core
├── core
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ └── com
│ │ └── redmadrobot
│ │ └── core
│ │ └── extensions
│ │ └── CoroutinesExtensions.kt
├── core_navigation
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ └── com
│ │ └── redmadrobot
│ │ └── core_navigation
│ │ └── navigation
│ │ ├── Route.kt
│ │ ├── Router.kt
│ │ └── screens
│ │ └── Routes.kt
├── core_network
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── graphql
│ │ └── com
│ │ │ └── redmadrobot
│ │ │ └── core_network
│ │ │ ├── .graphqlconfig
│ │ │ ├── queries.graphql
│ │ │ └── schema.json
│ │ ├── kotlin
│ │ └── com
│ │ │ └── redmadrobot
│ │ │ └── core_network
│ │ │ ├── ApiFactory.kt
│ │ │ ├── ApolloApi.kt
│ │ │ ├── NetworkConstants.kt
│ │ │ └── exception
│ │ │ ├── NetworkException.kt
│ │ │ ├── NetworkExceptionHandler.kt
│ │ │ ├── ServerException.kt
│ │ │ └── ServerExceptionFactory.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
└── core_presentation
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ └── kotlin
│ └── com
│ └── redmadrobot
│ └── core_presentation
│ ├── extensions
│ └── StateFlowExtensions.kt
│ └── model
│ └── State.kt
├── fake_server
├── .gitignore
├── README.md
├── accounts
│ ├── index.js
│ └── permissions.js
├── config.js
├── data.js
├── index.js
├── package-lock.json
└── package.json
├── features
├── auth
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ └── com
│ │ │ └── redmadrobot
│ │ │ └── auth
│ │ │ └── presentation
│ │ │ ├── AuthScreen.kt
│ │ │ ├── AuthState.kt
│ │ │ └── AuthViewModel.kt
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── details
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ └── com
│ │ │ └── redmadrobot
│ │ │ └── details
│ │ │ └── presentation
│ │ │ ├── DetailsScreen.kt
│ │ │ ├── DetailsViewModel.kt
│ │ │ ├── DetailsViewState.kt
│ │ │ ├── model
│ │ │ └── CardDetailsParams.kt
│ │ │ └── view
│ │ │ └── CardDetailsView.kt
│ │ └── res
│ │ └── drawable
│ │ ├── ic_circle_arrow.xml
│ │ └── ic_iron_man.xml
└── home
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ └── kotlin
│ └── com
│ └── redmadrobot
│ └── home
│ └── presentation
│ ├── HomeScreen.kt
│ ├── HomeViewModel.kt
│ ├── state
│ └── HomeViewState.kt
│ └── view
│ └── CardView.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Android template
3 | # Built application files
4 | *.apk
5 | *.ap_
6 | *.aab
7 |
8 | # Files for the ART/Dalvik VM
9 | *.dex
10 |
11 | # Java class files
12 | *.class
13 |
14 | # Generated files
15 | bin/
16 | gen/
17 | out/
18 | release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Log Files
28 | *.log
29 |
30 | # Android Studio Navigation editor temp files
31 | .navigation/
32 |
33 | # Android Studio captures folder
34 | captures/
35 |
36 | # IntelliJ
37 | *.iml
38 | .idea/
39 |
40 | # Keystore files
41 | # Uncomment the following lines if you do not want to check your keystore files in.
42 | #*.jks
43 | #*.keystore
44 |
45 | # External native build folder generated in Android Studio 2.2 and later
46 | .externalNativeBuild
47 |
48 | # Google Services (e.g. APIs or Firebase)
49 | # google-services.json
50 |
51 | # Freeline
52 | freeline.py
53 | freeline/
54 | freeline_project_description.json
55 |
56 | # fastlane
57 | fastlane/report.xml
58 | fastlane/Preview.html
59 | fastlane/screenshots
60 | fastlane/test_output
61 | fastlane/readme.md
62 |
63 | # Version control
64 | vcs.xml
65 |
66 | # lint
67 | lint/intermediates/
68 | lint/generated/
69 | lint/outputs/
70 | lint/tmp/
71 | # lint/reports/
72 |
73 | # Crashlytics plugin (for Android Studio and IntelliJ)
74 | com_crashlytics_export_strings.xml
75 | crashlytics.properties
76 | crashlytics-build.properties
77 | .env
78 | fabric.properties
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Redmadrobot
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android MAD Showcase
2 |
3 | Showcase is a sample Android project that presents Modern Android Development (MAD) libraries and practices.
4 |
5 | The goal of the project is to demonstrate modern tech-stack and take a fresh look at the development of Android applications.
6 |
7 | # Latest News
8 |
9 | **February 11, 2023** Updated to Compose 1.3.3 🎉🎉🎉
10 |
11 | **September 09, 2022** Updated to Compose 1.2.1
12 |
13 | **August 04, 2021** Updated to Compose 1.0.0
14 |
15 | ## Stack
16 |
17 | * Kotlin
18 | * Compose
19 | * Compose Navigation
20 | * Coroutines/Flow
21 | * GraphQl/Appollo
22 | * Jetpack ViewModel
23 | * Hilt
24 | * Gradle Kotlin DSL
25 |
26 | ## Known Issues
27 |
28 | * Cannot use Hilt in a layered multi-module project with `api` gradle configuration https://dagger.dev/hilt/gradle-setup#classpath-aggregation
29 |
30 | ## License
31 | ```
32 | MIT License
33 |
34 | Copyright (c) 2023 Redmadrobot
35 |
36 | Permission is hereby granted, free of charge, to any person obtaining a copy
37 | of this software and associated documentation files (the "Software"), to deal
38 | in the Software without restriction, including without limitation the rights
39 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
40 | copies of the Software, and to permit persons to whom the Software is
41 | furnished to do so, subject to the following conditions:
42 |
43 | The above copyright notice and this permission notice shall be included in all
44 | copies or substantial portions of the Software.
45 |
46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
47 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
48 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
49 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
50 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
51 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
52 | SOFTWARE.
53 | ```
54 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_APPLICATION)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | kotlin(GradlePluginId.KAPT)
5 | id(GradlePluginId.HILT)
6 | }
7 |
8 | android {
9 | defaultConfig {
10 | applicationId = "com.redmadrobot.mad"
11 | }
12 | }
13 |
14 | dependencies {
15 | implementation(project(":core:core"))
16 | implementation(project(":base:base_cards"))
17 |
18 | implementation(project(":features:auth"))
19 | implementation(project(":features:home"))
20 | implementation(project(":features:details"))
21 |
22 | implementation(CoreDependency.HILT)
23 | kapt(CoreDependency.HILT_COMPILER)
24 | }
25 |
--------------------------------------------------------------------------------
/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/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/redmadrobot/mad/AppRouter.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.mad
2 |
3 | import androidx.navigation.NavController
4 | import com.redmadrobot.core_navigation.navigation.Router
5 | import com.redmadrobot.core_navigation.navigation.Route
6 |
7 | class AppRouter : Router {
8 |
9 | private lateinit var navController: NavController
10 |
11 | fun init(navController: NavController) {
12 | this.navController = navController
13 | }
14 |
15 | override fun navigate(route: Route) {
16 | navController.navigate(route.name)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/redmadrobot/mad/MADApp.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.mad
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class MADApp : Application() {
8 |
9 | val appRouter = AppRouter()
10 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/redmadrobot/mad/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.mad
2 |
3 | import android.os.Bundle
4 | import androidx.activity.compose.setContent
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.ExperimentalComposeUiApi
8 | import androidx.hilt.navigation.compose.hiltViewModel
9 | import androidx.navigation.compose.NavHost
10 | import androidx.navigation.compose.rememberNavController
11 | import com.redmadrobot.auth.presentation.AuthRoute
12 | import com.redmadrobot.core_navigation.navigation.composableScreen
13 | import com.redmadrobot.core_navigation.navigation.screens.Routes
14 | import com.redmadrobot.details.presentation.DetailsRoute
15 | import com.redmadrobot.home.presentation.HomeRoute
16 | import com.redmadrobot.home.presentation.HomeScreen
17 | import dagger.hilt.android.AndroidEntryPoint
18 |
19 | @AndroidEntryPoint
20 | @ExperimentalComposeUiApi
21 | class MainActivity : AppCompatActivity() {
22 |
23 | private val router: AppRouter
24 | get() = (application as MADApp).appRouter
25 |
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 | setContent {
29 | Content()
30 | }
31 | }
32 |
33 | @Composable
34 | fun Content() {
35 | val navController = rememberNavController()
36 | router.init(navController)
37 | NavHost(navController, startDestination = Routes.Auth.name) {
38 | composableScreen(Routes.Auth) {
39 | AuthRoute()
40 | }
41 | composableScreen(Routes.Home) {
42 | HomeRoute()
43 | }
44 | composableScreen(Routes.Details) {
45 | DetailsRoute()
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/redmadrobot/mad/di/modules/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.mad.di.modules
2 |
3 | import android.app.Application
4 | import com.redmadrobot.core_navigation.navigation.Router
5 | import com.redmadrobot.mad.MADApp
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | class AppModule {
14 |
15 | @Provides
16 | fun provideRouter(app: Application): Router {
17 | return (app as MADApp).appRouter
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/redmadrobot/mad/di/modules/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.mad.di.modules
2 |
3 | import android.content.Context
4 | import com.redmadrobot.core_network.ApiFactory
5 | import com.redmadrobot.core_network.ApolloApi
6 | import com.redmadrobot.core_network.exception.NetworkExceptionHandler
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | class NetworkModule {
16 |
17 | @Provides
18 | fun provideApi(networkExceptionHandler: NetworkExceptionHandler): ApolloApi {
19 | return ApiFactory.provideApi(networkExceptionHandler)
20 | }
21 |
22 | @Provides
23 | fun provideNetworkExceptionHandler(@ApplicationContext context: Context): NetworkExceptionHandler {
24 | return NetworkExceptionHandler(context)
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #434A74
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MAD
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
--------------------------------------------------------------------------------
/base/base_cards/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIBRARY)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | }
5 |
6 | dependencies {
7 | implementation(project(":core:core"))
8 | }
--------------------------------------------------------------------------------
/base/base_cards/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/base/base_cards/src/main/kotlin/com/redmadrobot/base_cards/CardViewStateConverterExt.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.base_cards
2 |
3 | import androidx.compose.ui.graphics.Brush
4 | import androidx.compose.ui.graphics.Color
5 | import com.redmadrobot.base_cards.model.*
6 | import com.redmadrobot.core_network.CardDetailsQuery
7 | import com.redmadrobot.core_network.CardsListQuery
8 |
9 | // TODO: split two methods, and work with one data stucture by using fragments from appolo library
10 |
11 | fun CardsListQuery.Card.toCardViewState(): CardViewState {
12 | return CardViewState(
13 | id = id,
14 | number = number,
15 | cardColors = toCardColors(color),
16 | cardBorderColors = toCardBorderCardColors(color),
17 | cardTextNumberColor = toCardTextNumberColor(color),
18 | cardGradient = toCardGradient(color),
19 | borderGradient = toCardBorderGradient(color),
20 | iconRes = toIconRes(type),
21 | iconColor = toIconColor(color),
22 | )
23 | }
24 |
25 | fun CardDetailsQuery.Card.toCardViewState(): CardViewState {
26 | return CardViewState(
27 | id = id,
28 | number = number,
29 | cardColors = toCardColors(color),
30 | cardBorderColors = toCardBorderCardColors(color),
31 | cardTextNumberColor = toCardTextNumberColor(color),
32 | cardGradient = toCardGradient(color),
33 | borderGradient = toCardBorderGradient(color),
34 | iconRes = toIconRes(type),
35 | iconColor = toIconColor(color),
36 | )
37 | }
38 |
39 | private const val PINK_COLOR = "pink"
40 | private const val BLUE_COLOR = "blue"
41 | private const val RED_COLOR = "red"
42 | private const val YELLOW_COLOR = "yellow"
43 | private const val GREEN_COLOR = "green"
44 |
45 | private fun toIconColor(color: String): Color {
46 | return when (color) {
47 | PINK_COLOR -> Color(0xFF8D7CE7)
48 | BLUE_COLOR -> Color(0xFF6D94DA)
49 | RED_COLOR -> Color(0xEECF5942)
50 | YELLOW_COLOR -> Color(0xFFDB8A3F)
51 | GREEN_COLOR -> Color(0xFF539A83)
52 | else -> Color(0xFF539A83)
53 | }
54 | }
55 |
56 | private fun toIconRes(type: String): Int {
57 | return when (type) {
58 | "VISA" -> CardTypeIcon.VISA.icon
59 | "MASTER_CARD" -> CardTypeIcon.MASTERCARD.icon
60 | else -> CardTypeIcon.MASTERCARD.icon
61 | }
62 | }
63 |
64 | private fun toCardTextNumberColor(color: String): Color {
65 | return when (color) {
66 | PINK_COLOR -> CardTextNumberColor.PINK.color
67 | BLUE_COLOR -> CardTextNumberColor.BLUE.color
68 | RED_COLOR -> CardTextNumberColor.RED.color
69 | YELLOW_COLOR -> CardTextNumberColor.YELLOW.color
70 | GREEN_COLOR -> CardTextNumberColor.GREEN.color
71 | else -> CardTextNumberColor.PINK.color
72 | }
73 | }
74 |
75 | private fun toCardGradient(color: String): Brush {
76 | return when (color) {
77 | PINK_COLOR -> CardGradient.PINK.brush
78 | BLUE_COLOR -> CardGradient.BLUE.brush
79 | RED_COLOR -> CardGradient.RED.brush
80 | YELLOW_COLOR -> CardGradient.YELLOW.brush
81 | GREEN_COLOR -> CardGradient.GREEN.brush
82 | else -> CardGradient.YELLOW.brush
83 | }
84 | }
85 |
86 | private fun toCardBorderGradient(color: String): Brush {
87 | return when (color) {
88 | PINK_COLOR -> CardBorderGradient.PINK.brush
89 | BLUE_COLOR -> CardBorderGradient.BLUE.brush
90 | RED_COLOR -> CardBorderGradient.RED.brush
91 | YELLOW_COLOR -> CardBorderGradient.YELLOW.brush
92 | GREEN_COLOR -> CardBorderGradient.GREEN.brush
93 | else -> CardBorderGradient.YELLOW.brush
94 | }
95 | }
96 |
97 | private fun toCardColors(color: String): List {
98 | return when (color) {
99 | PINK_COLOR -> CardColors.PINK.colors
100 | BLUE_COLOR -> CardColors.BLUE.colors
101 | RED_COLOR -> CardColors.RED.colors
102 | YELLOW_COLOR -> CardColors.YELLOW.colors
103 | GREEN_COLOR -> CardColors.GREEN.colors
104 | else -> CardColors.PINK.colors
105 | }
106 | }
107 |
108 | private fun toCardBorderCardColors(color: String): List {
109 | return when (color) {
110 | PINK_COLOR -> CardBorderColors.PINK.colors
111 | BLUE_COLOR -> CardBorderColors.BLUE.colors
112 | RED_COLOR -> CardBorderColors.RED.colors
113 | YELLOW_COLOR -> CardBorderColors.YELLOW.colors
114 | GREEN_COLOR -> CardBorderColors.GREEN.colors
115 | else -> CardColors.PINK.colors
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/base/base_cards/src/main/kotlin/com/redmadrobot/base_cards/model/CardBorderColors.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.base_cards.model
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | enum class CardBorderColors(val colors: List) {
6 | BLUE(
7 | listOf(
8 | Color(0xFFB4F4EF),
9 | Color(0xFF5959BA),
10 | )
11 | ),
12 |
13 | RED(
14 | listOf(
15 | Color(0xFFB88BDB),
16 | Color(0xFFCF5942)
17 | )
18 | ),
19 |
20 | YELLOW(
21 | listOf(
22 | Color(0xFFFFF8B6),
23 | Color(0xFFB15F13),
24 | )
25 | ),
26 |
27 | PINK(
28 | listOf(
29 | Color(0xFFCADFFF),
30 | Color(0xFF703AB5),
31 | )
32 | ),
33 |
34 | GREEN(
35 | listOf(
36 | Color(0xFFB6FDDC),
37 | Color(0xFF396466),
38 | )
39 | )
40 | }
--------------------------------------------------------------------------------
/base/base_cards/src/main/kotlin/com/redmadrobot/base_cards/model/CardBorderGradient.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.base_cards.model
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Brush
5 |
6 | enum class CardBorderGradient(val brush: Brush) {
7 | BLUE(
8 | Brush.linearGradient(
9 | colors = CardColors.BLUE.colors,
10 | start = Offset(0f, Float.POSITIVE_INFINITY),
11 | end = Offset(Float.POSITIVE_INFINITY, 0f)
12 | )
13 | ),
14 |
15 | RED(
16 | Brush.linearGradient(
17 | colors = CardColors.RED.colors,
18 | start = Offset(0f, 0f),
19 | )
20 | ),
21 |
22 | YELLOW(
23 | Brush.linearGradient(
24 | colors = CardColors.YELLOW.colors,
25 | start = Offset(0f, Float.POSITIVE_INFINITY),
26 | end = Offset(Float.POSITIVE_INFINITY, 0f)
27 | )
28 | ),
29 |
30 | PINK(
31 | Brush.linearGradient(
32 | colors = CardColors.PINK.colors,
33 | start = Offset(0f, Float.POSITIVE_INFINITY),
34 | end = Offset(Float.POSITIVE_INFINITY, 0f)
35 | )
36 | ),
37 |
38 | GREEN(
39 | Brush.linearGradient(
40 | colors = CardColors.GREEN.colors,
41 | start = Offset(0f, Float.POSITIVE_INFINITY),
42 | end = Offset(Float.POSITIVE_INFINITY, 0f)
43 | )
44 | )
45 | }
--------------------------------------------------------------------------------
/base/base_cards/src/main/kotlin/com/redmadrobot/base_cards/model/CardColors.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.base_cards.model
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | enum class CardColors(val colors: List) {
6 | BLUE(
7 | listOf(
8 | Color(0xFF91EAE4),
9 | Color(0xFF7F7FD5),
10 | )
11 | ),
12 |
13 | RED(
14 | listOf(
15 | Color(0xFFA36BCF),
16 | Color(0xFFE77557)
17 | )
18 | ),
19 |
20 | YELLOW(
21 | listOf(
22 | Color(0xFFF9ED80),
23 | Color(0xFFDF832E),
24 | )
25 | ),
26 |
27 | PINK(
28 | listOf(
29 | Color(0xFFB0D0FF),
30 | Color(0xFF914CE8),
31 | )
32 | ),
33 |
34 | GREEN(
35 | listOf(
36 | Color(0xFF99F2C8),
37 | Color(0xFF396063),
38 | )
39 | )
40 |
41 | }
--------------------------------------------------------------------------------
/base/base_cards/src/main/kotlin/com/redmadrobot/base_cards/model/CardGradient.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.base_cards.model
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.graphics.Brush
5 |
6 | enum class CardGradient(val brush: Brush) {
7 | BLUE(
8 | Brush.linearGradient(
9 | colors = CardColors.BLUE.colors,
10 | start = Offset(0f, Float.POSITIVE_INFINITY),
11 | end = Offset(Float.POSITIVE_INFINITY, 0f)
12 | )
13 | ),
14 |
15 | RED(
16 | Brush.linearGradient(
17 | colors = CardColors.RED.colors,
18 | start = Offset(0f, Float.POSITIVE_INFINITY),
19 | end = Offset(Float.POSITIVE_INFINITY, 0f)
20 | )
21 | ),
22 |
23 | YELLOW(
24 | Brush.linearGradient(
25 | colors = CardColors.YELLOW.colors,
26 | start = Offset(0f, Float.POSITIVE_INFINITY),
27 | end = Offset(Float.POSITIVE_INFINITY, 0f)
28 | )
29 | ),
30 |
31 | PINK(
32 | Brush.linearGradient(
33 | colors = CardColors.PINK.colors,
34 | start = Offset(0f, Float.POSITIVE_INFINITY),
35 | end = Offset(Float.POSITIVE_INFINITY, 0f)
36 | )
37 | ),
38 |
39 | GREEN(
40 | Brush.linearGradient(
41 | colors = CardColors.GREEN.colors,
42 | start = Offset(0f, Float.POSITIVE_INFINITY),
43 | end = Offset(Float.POSITIVE_INFINITY, 0f)
44 | )
45 | )
46 | }
--------------------------------------------------------------------------------
/base/base_cards/src/main/kotlin/com/redmadrobot/base_cards/model/CardTextNumberColor.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.base_cards.model
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | enum class CardTextNumberColor(val color: Color) {
6 | BLUE(Color(0xFF185E66)),
7 | PINK(Color(0xFF2A4786)),
8 | YELLOW(Color(0xFF5B4108)),
9 | RED(Color(0xFF491D67)),
10 | GREEN(Color(0xFF174E34)),
11 | }
--------------------------------------------------------------------------------
/base/base_cards/src/main/kotlin/com/redmadrobot/base_cards/model/CardTypeIcon.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.base_cards.model
2 |
3 | import com.redmadrobot.base_cards.R
4 |
5 | enum class CardTypeIcon(val icon: Int) {
6 | MASTERCARD(R.drawable.ic_master),
7 | VISA(R.drawable.ic_visa)
8 | }
--------------------------------------------------------------------------------
/base/base_cards/src/main/kotlin/com/redmadrobot/base_cards/model/CardViewState.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.base_cards.model
2 |
3 | import androidx.compose.ui.graphics.Brush
4 | import androidx.compose.ui.graphics.Color
5 |
6 | data class CardViewState(
7 | val id: String,
8 | val number: String,
9 | val cardColors: List,
10 | val cardBorderColors: List,
11 | val cardTextNumberColor: Color,
12 | val cardGradient: Brush,
13 | val borderGradient: Brush,
14 | val iconRes: Int,
15 | val iconColor: Color,
16 | )
--------------------------------------------------------------------------------
/base/base_cards/src/main/res/drawable/ic_circle_arrow.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
27 |
28 |
--------------------------------------------------------------------------------
/base/base_cards/src/main/res/drawable/ic_iron_man.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/base/base_cards/src/main/res/drawable/ic_master.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/base/base_cards/src/main/res/drawable/ic_subtract.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/base/base_cards/src/main/res/drawable/ic_visa.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 |
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | maven("https://dl.bintray.com/kotlin/kotlin-eap/")
7 | }
8 | dependencies {
9 | classpath(GradlePluginDependency.ANDROID_BUILD_TOOLS)
10 | classpath(GradlePluginDependency.KOTLIN)
11 | classpath(GradlePluginDependency.HILT)
12 | classpath(GradlePluginDependency.APOLLO)
13 | }
14 | }
15 |
16 | // all projects = root project + sub projects
17 | allprojects {
18 | repositories {
19 | google()
20 | mavenCentral()
21 | maven { url = uri("https://jitpack.io") }
22 | }
23 | }
24 |
25 | tasks.register("clean", Delete::class) {
26 | delete(rootProject.buildDir)
27 | }
--------------------------------------------------------------------------------
/buildSrc/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | mavenCentral()
7 | google()
8 | }
9 |
10 | dependencies {
11 | implementation("com.android.tools.build:gradle:7.4.1")
12 | implementation("com.android.tools.build:gradle-api:7.4.1")
13 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
14 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.8.10")
15 | implementation("com.squareup:javapoet:1.13.0") // https://github.com/google/dagger/issues/3068
16 | }
17 |
18 | gradlePlugin {
19 | plugins {
20 | register("common-android-config") {
21 | id = "common-android-config"
22 | implementationClass = "com.redmadrobot.mad.plugins.CommonAndroidConfigPlugin"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/AndroidConfig.kt:
--------------------------------------------------------------------------------
1 | object AndroidConfig {
2 | const val MIN_SDK_VERSION = 26
3 | const val COMPILE_SDK_VERSION = 33
4 | const val TARGET_SDK_VERSION = 33
5 | }
6 |
7 | interface BuildType {
8 |
9 | companion object {
10 | const val RELEASE = "release"
11 | const val DEBUG = "debug"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/CoreDependency.kt:
--------------------------------------------------------------------------------
1 | object CoreVersions {
2 | const val KOTLIN = "1.8.10"
3 | const val COROUTINES = "1.6.4"
4 |
5 | const val HILT = "2.45"
6 |
7 | const val APOLLO = "2.5.14"
8 | }
9 |
10 | object CoreDependency {
11 | const val KOTLIN = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${CoreVersions.KOTLIN}"
12 |
13 | const val COROUTINES_CORE =
14 | "org.jetbrains.kotlinx:kotlinx-coroutines-core:${CoreVersions.COROUTINES}"
15 | const val COROUTINES_ANDROID =
16 | "org.jetbrains.kotlinx:kotlinx-coroutines-android:${CoreVersions.COROUTINES}"
17 |
18 | const val HILT = "com.google.dagger:hilt-android:${CoreVersions.HILT}"
19 | const val HILT_COMPILER = "com.google.dagger:hilt-compiler:${CoreVersions.HILT}"
20 | }
21 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/GradlePluginDependency.kt:
--------------------------------------------------------------------------------
1 | object GradlePluginVersions {
2 | const val ANDROID_BUILD_TOOLS = "7.4.1"
3 | const val KOTLIN = CoreVersions.KOTLIN
4 | const val HILT = CoreVersions.HILT
5 | const val APOLLO = CoreVersions.APOLLO
6 | }
7 |
8 | object GradlePluginId {
9 | const val ANDROID_COMMON_CONFIG = "common-android-config"
10 | const val ANDROID_APPLICATION = "com.android.application"
11 | const val ANDROID_LIBRARY = "com.android.library"
12 | const val KAPT = "kapt"
13 | const val HILT = "dagger.hilt.android.plugin"
14 | const val APOLLO = "com.apollographql.apollo"
15 | }
16 |
17 | object GradlePluginDependency {
18 | const val ANDROID_BUILD_TOOLS = "com.android.tools.build:gradle:${GradlePluginVersions.ANDROID_BUILD_TOOLS}"
19 | const val KOTLIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:${GradlePluginVersions.KOTLIN}"
20 | const val HILT = "com.google.dagger:hilt-android-gradle-plugin:${GradlePluginVersions.HILT}"
21 | const val APOLLO = "com.apollographql.apollo:apollo-gradle-plugin:${GradlePluginVersions.APOLLO}"
22 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/NetworkDependency.kt:
--------------------------------------------------------------------------------
1 | private object NetworkVersions {
2 | const val APOLLO = CoreVersions.APOLLO
3 | }
4 |
5 | object NetworkDependency {
6 | const val APOLLO_GRAPHQL = "com.apollographql.apollo:apollo-runtime:${NetworkVersions.APOLLO}"
7 | const val APOLLO_COROUTINES = "com.apollographql.apollo:apollo-coroutines-support:${NetworkVersions.APOLLO}"
8 | }
9 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/PresentationDependency.kt:
--------------------------------------------------------------------------------
1 | object PresentationVersions {
2 | const val APPCOMPAT = "1.6.1"
3 |
4 | const val COMPOSE = "1.3.3"
5 |
6 | const val COMPOSE_MATERIAL = "1.3.1"
7 | const val COMPOSE_FOUNDATION = "1.3.1"
8 | const val COMPOSE_COMPILER = "1.4.2"
9 | const val COMPOSE_ACTIVITY = "1.6.1"
10 | const val COMPOSE_NAVIGATION = "2.5.3"
11 | const val COMPOSE_HILT_NAVIGATION = "1.0.0"
12 | const val COMPOSE_LIFECYCLE = "2.6.0-beta01"
13 | }
14 |
15 | object PresentationDependency {
16 | const val APPCOMPAT = "androidx.appcompat:appcompat:${PresentationVersions.APPCOMPAT}"
17 |
18 | const val COMPOSE_UI = "androidx.compose.ui:ui:${PresentationVersions.COMPOSE}"
19 | const val COMPOSE_RUNTIME = "androidx.compose.runtime:runtime:${PresentationVersions.COMPOSE}"
20 | const val COMPOSE_TOOLING = "androidx.compose.ui:ui-tooling:${PresentationVersions.COMPOSE}"
21 | const val COMPOSE_TOOLING_PREVIEW = "androidx.compose.ui:ui-tooling-preview:${PresentationVersions.COMPOSE}"
22 | const val COMPOSE_ANIMATION = "androidx.compose.animation:animation:${PresentationVersions.COMPOSE}"
23 |
24 | const val COMPOSE_MATERIAL = "androidx.compose.material:material:${PresentationVersions.COMPOSE_MATERIAL}"
25 | const val COMPOSE_FOUNDATION = "androidx.compose.foundation:foundation:${PresentationVersions.COMPOSE_FOUNDATION}"
26 | const val COMPOSE_COMPILER = "androidx.compose.compiler:compiler:${PresentationVersions.COMPOSE_COMPILER}"
27 | const val COMPOSE_ACTIVITY = "androidx.activity:activity-compose:${PresentationVersions.COMPOSE_ACTIVITY}"
28 | const val COMPOSE_NAVIGATION = "androidx.navigation:navigation-compose:${PresentationVersions.COMPOSE_NAVIGATION}"
29 | const val COMPOSE_HILT_NAVIGATION = "androidx.hilt:hilt-navigation-compose:${PresentationVersions.COMPOSE_HILT_NAVIGATION}"
30 | const val COMPOSE_LIFECYCLE = "androidx.lifecycle:lifecycle-runtime-compose:${PresentationVersions.COMPOSE_LIFECYCLE}"
31 | }
32 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/com/redmadrobot/mad/plugins/CommonAndroidConfigPlugin.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.mad.plugins
2 |
3 | import org.gradle.api.JavaVersion
4 | import org.gradle.api.Plugin
5 | import org.gradle.api.Project
6 | import org.gradle.kotlin.dsl.withType
7 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
8 |
9 | class CommonAndroidConfigPlugin : Plugin {
10 |
11 | override fun apply(target: Project) {
12 | with(target) {
13 | applyPlugins()
14 | applyAndroidConfig()
15 | applyKotlinConfig()
16 | }
17 | }
18 |
19 | private fun Project.applyPlugins() {
20 | plugins.run {
21 | apply("kotlin-android")
22 | }
23 | }
24 |
25 | private fun Project.applyAndroidConfig() {
26 | android?.run {
27 | defaultConfig {
28 | minSdk = AndroidConfig.MIN_SDK_VERSION
29 | targetSdk = AndroidConfig.TARGET_SDK_VERSION
30 | compileSdkVersion(AndroidConfig.COMPILE_SDK_VERSION)
31 | }
32 |
33 | compileOptions {
34 | sourceCompatibility = JavaVersion.VERSION_1_8
35 | targetCompatibility = JavaVersion.VERSION_1_8
36 | }
37 |
38 | composeOptions {
39 | kotlinCompilerExtensionVersion = PresentationVersions.COMPOSE_COMPILER
40 | }
41 |
42 | sourceSets.forEach { it.java.srcDir("src/${it.name}/kotlin") }
43 | }
44 | app?.run {
45 | buildTypes {
46 | val proguardFiles = rootProject.fileTree("proguard").files +
47 | getDefaultProguardFile("proguard-android-optimize.txt")
48 |
49 | getByName(BuildType.DEBUG) {
50 | }
51 |
52 | getByName(BuildType.RELEASE) {
53 | isDebuggable = false
54 | isMinifyEnabled = true
55 | proguardFiles(*proguardFiles.toTypedArray())
56 | }
57 | }
58 |
59 | buildFeatures {
60 | compose = true
61 | }
62 | }
63 | library?.run {
64 | buildTypes {
65 | val proguardFiles = rootProject.fileTree("proguard").files +
66 | getDefaultProguardFile("proguard-android-optimize.txt")
67 |
68 | getByName(BuildType.DEBUG) {
69 | }
70 |
71 | getByName(BuildType.RELEASE) {
72 | isMinifyEnabled = true
73 | proguardFiles(*proguardFiles.toTypedArray())
74 | }
75 | }
76 |
77 | buildFeatures {
78 | compose = true
79 | }
80 | }
81 | }
82 |
83 | private fun Project.applyKotlinConfig() {
84 | this.tasks.withType {
85 | kotlinOptions {
86 | jvmTarget = JavaVersion.VERSION_1_8.toString()
87 | }
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/com/redmadrobot/mad/plugins/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.mad.plugins
2 |
3 | import com.android.build.gradle.BaseExtension
4 | import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
5 | import com.android.build.gradle.LibraryExtension
6 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
7 | import org.gradle.api.Project
8 |
9 | val Project.android: BaseExtension?
10 | get() = extensions.findByName("android") as? BaseExtension
11 |
12 | val Project.app: BaseAppModuleExtension?
13 | get() = extensions.findByName("android") as? BaseAppModuleExtension
14 |
15 | val Project.library: LibraryExtension?
16 | get() = extensions.findByName("android") as? LibraryExtension
17 |
18 | val Project.kotlin: KotlinAndroidProjectExtension?
19 | get() = extensions.findByName("android") as? KotlinAndroidProjectExtension
--------------------------------------------------------------------------------
/core/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIBRARY)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | }
5 |
6 | dependencies {
7 | api(CoreDependency.KOTLIN)
8 | api(CoreDependency.COROUTINES_CORE)
9 | api(CoreDependency.COROUTINES_ANDROID)
10 |
11 | api(project(":core:core_network"))
12 | api(project(":core:core_presentation"))
13 | api(project(":core:core_navigation"))
14 | }
15 |
--------------------------------------------------------------------------------
/core/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/core/src/main/kotlin/com/redmadrobot/core/extensions/CoroutinesExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core.extensions
2 |
3 | import kotlinx.coroutines.CancellationException
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.launch
6 |
7 | @Suppress("InstanceOfCheckForException")
8 | fun CoroutineScope.safeLaunch(
9 | block: suspend CoroutineScope.() -> Unit,
10 | onError: (Throwable) -> Unit,
11 | onComplete: () -> Unit = {}
12 | ) {
13 | launch {
14 | try {
15 | block.invoke(this)
16 | } catch (exception: Exception) {
17 | if (exception !is CancellationException) {
18 | onError.invoke(exception)
19 | }
20 | } finally {
21 | onComplete()
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/core/core_navigation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/core_navigation/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIBRARY)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | }
5 |
6 | dependencies {
7 | // TODO: delete next line after update compose navigation and hilt navigation because these libs have target 1.0.0-rc01 compose runtime
8 | implementation(PresentationDependency.COMPOSE_RUNTIME)
9 | api(PresentationDependency.COMPOSE_NAVIGATION)
10 | api(PresentationDependency.COMPOSE_HILT_NAVIGATION)
11 | }
12 |
--------------------------------------------------------------------------------
/core/core_navigation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/core_navigation/src/main/kotlin/com/redmadrobot/core_navigation/navigation/Route.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_navigation.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.navigation.NamedNavArgument
5 | import androidx.navigation.NavBackStackEntry
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 |
9 | data class Route(
10 | val name: String,
11 | val arguments: List = emptyList(),
12 | )
13 |
14 | fun NavGraphBuilder.composableScreen(
15 | route: Route,
16 | content: @Composable (NavBackStackEntry) -> Unit
17 | ): Unit = composable(
18 | route = route.name,
19 | arguments = route.arguments,
20 | content = content,
21 | )
--------------------------------------------------------------------------------
/core/core_navigation/src/main/kotlin/com/redmadrobot/core_navigation/navigation/Router.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_navigation.navigation
2 |
3 | interface Router {
4 |
5 | fun navigate(route: Route)
6 | }
--------------------------------------------------------------------------------
/core/core_navigation/src/main/kotlin/com/redmadrobot/core_navigation/navigation/screens/Routes.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_navigation.navigation.screens
2 |
3 | import androidx.navigation.NavType
4 | import androidx.navigation.navArgument
5 | import com.redmadrobot.core_navigation.navigation.Route
6 |
7 | object Routes {
8 | private const val KEY = "id"
9 | val Auth = Route(name = "Auth")
10 | val Home = Route(name = "Home")
11 | val Details = Route(name = "Details/{$KEY}")
12 |
13 | fun toDetails(id: String? = null): Route {
14 | val arguments = listOf(
15 | navArgument(KEY) { type = NavType.StringType }
16 | )
17 | return Route(name = "Details/$id", arguments = arguments)
18 | }
19 | }
--------------------------------------------------------------------------------
/core/core_network/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
--------------------------------------------------------------------------------
/core/core_network/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIBRARY)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | id(GradlePluginId.APOLLO)
5 | }
6 |
7 | android {
8 | buildFeatures {
9 | compose = false
10 | }
11 | }
12 |
13 | apollo {
14 | generateApolloMetadata.set(true)
15 | generateKotlinModels.set(true)
16 | }
17 |
18 | dependencies {
19 | api(NetworkDependency.APOLLO_GRAPHQL)
20 | api(NetworkDependency.APOLLO_COROUTINES)
21 | }
22 |
--------------------------------------------------------------------------------
/core/core_network/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/core_network/src/main/graphql/com/redmadrobot/core_network/.graphqlconfig:
--------------------------------------------------------------------------------
1 | {
2 | "name": "GraphQL Schema",
3 | "schemaPath": "schema.json",
4 | "extensions": {
5 | "endpoints": {
6 | "Default GraphQL Endpoint": {
7 | "url": "https://mad-showcase-fake-server.herokuapp.com/graphql"
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/core/core_network/src/main/graphql/com/redmadrobot/core_network/queries.graphql:
--------------------------------------------------------------------------------
1 | mutation Login($email: String!, $password: String!){
2 | login(email: $email, password: $password)
3 | }
4 |
5 | query CardsList {
6 | cards {
7 | id,
8 | type,
9 | number,
10 | color
11 | }
12 | }
13 |
14 | query CardDetails($id:ID!) {
15 | card(id: $id) {
16 | id,
17 | type,
18 | number,
19 | color
20 | }
21 | }
--------------------------------------------------------------------------------
/core/core_network/src/main/graphql/com/redmadrobot/core_network/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "__schema": {
3 | "queryType": {
4 | "name": "Query"
5 | },
6 | "mutationType": {
7 | "name": "Mutation"
8 | },
9 | "subscriptionType": null,
10 | "types": [
11 | {
12 | "kind": "OBJECT",
13 | "name": "Query",
14 | "description": null,
15 | "fields": [
16 | {
17 | "name": "account",
18 | "description": null,
19 | "args": [
20 | {
21 | "name": "id",
22 | "description": null,
23 | "type": {
24 | "kind": "NON_NULL",
25 | "name": null,
26 | "ofType": {
27 | "kind": "SCALAR",
28 | "name": "ID",
29 | "ofType": null
30 | }
31 | },
32 | "defaultValue": null
33 | }
34 | ],
35 | "type": {
36 | "kind": "OBJECT",
37 | "name": "Account",
38 | "ofType": null
39 | },
40 | "isDeprecated": false,
41 | "deprecationReason": null
42 | },
43 | {
44 | "name": "accounts",
45 | "description": null,
46 | "args": [],
47 | "type": {
48 | "kind": "LIST",
49 | "name": null,
50 | "ofType": {
51 | "kind": "OBJECT",
52 | "name": "Account",
53 | "ofType": null
54 | }
55 | },
56 | "isDeprecated": false,
57 | "deprecationReason": null
58 | },
59 | {
60 | "name": "viewer",
61 | "description": null,
62 | "args": [],
63 | "type": {
64 | "kind": "NON_NULL",
65 | "name": null,
66 | "ofType": {
67 | "kind": "OBJECT",
68 | "name": "Account",
69 | "ofType": null
70 | }
71 | },
72 | "isDeprecated": false,
73 | "deprecationReason": null
74 | },
75 | {
76 | "name": "card",
77 | "description": null,
78 | "args": [
79 | {
80 | "name": "id",
81 | "description": null,
82 | "type": {
83 | "kind": "NON_NULL",
84 | "name": null,
85 | "ofType": {
86 | "kind": "SCALAR",
87 | "name": "ID",
88 | "ofType": null
89 | }
90 | },
91 | "defaultValue": null
92 | }
93 | ],
94 | "type": {
95 | "kind": "NON_NULL",
96 | "name": null,
97 | "ofType": {
98 | "kind": "OBJECT",
99 | "name": "Card",
100 | "ofType": null
101 | }
102 | },
103 | "isDeprecated": false,
104 | "deprecationReason": null
105 | },
106 | {
107 | "name": "cards",
108 | "description": null,
109 | "args": [],
110 | "type": {
111 | "kind": "NON_NULL",
112 | "name": null,
113 | "ofType": {
114 | "kind": "LIST",
115 | "name": null,
116 | "ofType": {
117 | "kind": "NON_NULL",
118 | "name": null,
119 | "ofType": {
120 | "kind": "OBJECT",
121 | "name": "Card",
122 | "ofType": null
123 | }
124 | }
125 | }
126 | },
127 | "isDeprecated": false,
128 | "deprecationReason": null
129 | }
130 | ],
131 | "inputFields": null,
132 | "interfaces": [],
133 | "enumValues": null,
134 | "possibleTypes": null
135 | },
136 | {
137 | "kind": "SCALAR",
138 | "name": "ID",
139 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.",
140 | "fields": null,
141 | "inputFields": null,
142 | "interfaces": null,
143 | "enumValues": null,
144 | "possibleTypes": null
145 | },
146 | {
147 | "kind": "OBJECT",
148 | "name": "Account",
149 | "description": null,
150 | "fields": [
151 | {
152 | "name": "id",
153 | "description": null,
154 | "args": [],
155 | "type": {
156 | "kind": "NON_NULL",
157 | "name": null,
158 | "ofType": {
159 | "kind": "SCALAR",
160 | "name": "ID",
161 | "ofType": null
162 | }
163 | },
164 | "isDeprecated": false,
165 | "deprecationReason": null
166 | },
167 | {
168 | "name": "name",
169 | "description": null,
170 | "args": [],
171 | "type": {
172 | "kind": "SCALAR",
173 | "name": "String",
174 | "ofType": null
175 | },
176 | "isDeprecated": false,
177 | "deprecationReason": null
178 | }
179 | ],
180 | "inputFields": null,
181 | "interfaces": [],
182 | "enumValues": null,
183 | "possibleTypes": null
184 | },
185 | {
186 | "kind": "SCALAR",
187 | "name": "String",
188 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.",
189 | "fields": null,
190 | "inputFields": null,
191 | "interfaces": null,
192 | "enumValues": null,
193 | "possibleTypes": null
194 | },
195 | {
196 | "kind": "OBJECT",
197 | "name": "Card",
198 | "description": null,
199 | "fields": [
200 | {
201 | "name": "id",
202 | "description": null,
203 | "args": [],
204 | "type": {
205 | "kind": "NON_NULL",
206 | "name": null,
207 | "ofType": {
208 | "kind": "SCALAR",
209 | "name": "ID",
210 | "ofType": null
211 | }
212 | },
213 | "isDeprecated": false,
214 | "deprecationReason": null
215 | },
216 | {
217 | "name": "type",
218 | "description": null,
219 | "args": [],
220 | "type": {
221 | "kind": "NON_NULL",
222 | "name": null,
223 | "ofType": {
224 | "kind": "SCALAR",
225 | "name": "String",
226 | "ofType": null
227 | }
228 | },
229 | "isDeprecated": false,
230 | "deprecationReason": null
231 | },
232 | {
233 | "name": "number",
234 | "description": null,
235 | "args": [],
236 | "type": {
237 | "kind": "NON_NULL",
238 | "name": null,
239 | "ofType": {
240 | "kind": "SCALAR",
241 | "name": "String",
242 | "ofType": null
243 | }
244 | },
245 | "isDeprecated": false,
246 | "deprecationReason": null
247 | },
248 | {
249 | "name": "color",
250 | "description": null,
251 | "args": [],
252 | "type": {
253 | "kind": "NON_NULL",
254 | "name": null,
255 | "ofType": {
256 | "kind": "SCALAR",
257 | "name": "String",
258 | "ofType": null
259 | }
260 | },
261 | "isDeprecated": false,
262 | "deprecationReason": null
263 | }
264 | ],
265 | "inputFields": null,
266 | "interfaces": [],
267 | "enumValues": null,
268 | "possibleTypes": null
269 | },
270 | {
271 | "kind": "OBJECT",
272 | "name": "Mutation",
273 | "description": null,
274 | "fields": [
275 | {
276 | "name": "login",
277 | "description": null,
278 | "args": [
279 | {
280 | "name": "email",
281 | "description": null,
282 | "type": {
283 | "kind": "NON_NULL",
284 | "name": null,
285 | "ofType": {
286 | "kind": "SCALAR",
287 | "name": "String",
288 | "ofType": null
289 | }
290 | },
291 | "defaultValue": null
292 | },
293 | {
294 | "name": "password",
295 | "description": null,
296 | "type": {
297 | "kind": "NON_NULL",
298 | "name": null,
299 | "ofType": {
300 | "kind": "SCALAR",
301 | "name": "String",
302 | "ofType": null
303 | }
304 | },
305 | "defaultValue": null
306 | }
307 | ],
308 | "type": {
309 | "kind": "SCALAR",
310 | "name": "String",
311 | "ofType": null
312 | },
313 | "isDeprecated": false,
314 | "deprecationReason": null
315 | }
316 | ],
317 | "inputFields": null,
318 | "interfaces": [],
319 | "enumValues": null,
320 | "possibleTypes": null
321 | },
322 | {
323 | "kind": "OBJECT",
324 | "name": "__Schema",
325 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.",
326 | "fields": [
327 | {
328 | "name": "types",
329 | "description": "A list of all types supported by this server.",
330 | "args": [],
331 | "type": {
332 | "kind": "NON_NULL",
333 | "name": null,
334 | "ofType": {
335 | "kind": "LIST",
336 | "name": null,
337 | "ofType": {
338 | "kind": "NON_NULL",
339 | "name": null,
340 | "ofType": {
341 | "kind": "OBJECT",
342 | "name": "__Type",
343 | "ofType": null
344 | }
345 | }
346 | }
347 | },
348 | "isDeprecated": false,
349 | "deprecationReason": null
350 | },
351 | {
352 | "name": "queryType",
353 | "description": "The type that query operations will be rooted at.",
354 | "args": [],
355 | "type": {
356 | "kind": "NON_NULL",
357 | "name": null,
358 | "ofType": {
359 | "kind": "OBJECT",
360 | "name": "__Type",
361 | "ofType": null
362 | }
363 | },
364 | "isDeprecated": false,
365 | "deprecationReason": null
366 | },
367 | {
368 | "name": "mutationType",
369 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.",
370 | "args": [],
371 | "type": {
372 | "kind": "OBJECT",
373 | "name": "__Type",
374 | "ofType": null
375 | },
376 | "isDeprecated": false,
377 | "deprecationReason": null
378 | },
379 | {
380 | "name": "subscriptionType",
381 | "description": "If this server support subscription, the type that subscription operations will be rooted at.",
382 | "args": [],
383 | "type": {
384 | "kind": "OBJECT",
385 | "name": "__Type",
386 | "ofType": null
387 | },
388 | "isDeprecated": false,
389 | "deprecationReason": null
390 | },
391 | {
392 | "name": "directives",
393 | "description": "A list of all directives supported by this server.",
394 | "args": [],
395 | "type": {
396 | "kind": "NON_NULL",
397 | "name": null,
398 | "ofType": {
399 | "kind": "LIST",
400 | "name": null,
401 | "ofType": {
402 | "kind": "NON_NULL",
403 | "name": null,
404 | "ofType": {
405 | "kind": "OBJECT",
406 | "name": "__Directive",
407 | "ofType": null
408 | }
409 | }
410 | }
411 | },
412 | "isDeprecated": false,
413 | "deprecationReason": null
414 | }
415 | ],
416 | "inputFields": null,
417 | "interfaces": [],
418 | "enumValues": null,
419 | "possibleTypes": null
420 | },
421 | {
422 | "kind": "OBJECT",
423 | "name": "__Type",
424 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.",
425 | "fields": [
426 | {
427 | "name": "kind",
428 | "description": null,
429 | "args": [],
430 | "type": {
431 | "kind": "NON_NULL",
432 | "name": null,
433 | "ofType": {
434 | "kind": "ENUM",
435 | "name": "__TypeKind",
436 | "ofType": null
437 | }
438 | },
439 | "isDeprecated": false,
440 | "deprecationReason": null
441 | },
442 | {
443 | "name": "name",
444 | "description": null,
445 | "args": [],
446 | "type": {
447 | "kind": "SCALAR",
448 | "name": "String",
449 | "ofType": null
450 | },
451 | "isDeprecated": false,
452 | "deprecationReason": null
453 | },
454 | {
455 | "name": "description",
456 | "description": null,
457 | "args": [],
458 | "type": {
459 | "kind": "SCALAR",
460 | "name": "String",
461 | "ofType": null
462 | },
463 | "isDeprecated": false,
464 | "deprecationReason": null
465 | },
466 | {
467 | "name": "fields",
468 | "description": null,
469 | "args": [
470 | {
471 | "name": "includeDeprecated",
472 | "description": null,
473 | "type": {
474 | "kind": "SCALAR",
475 | "name": "Boolean",
476 | "ofType": null
477 | },
478 | "defaultValue": "false"
479 | }
480 | ],
481 | "type": {
482 | "kind": "LIST",
483 | "name": null,
484 | "ofType": {
485 | "kind": "NON_NULL",
486 | "name": null,
487 | "ofType": {
488 | "kind": "OBJECT",
489 | "name": "__Field",
490 | "ofType": null
491 | }
492 | }
493 | },
494 | "isDeprecated": false,
495 | "deprecationReason": null
496 | },
497 | {
498 | "name": "interfaces",
499 | "description": null,
500 | "args": [],
501 | "type": {
502 | "kind": "LIST",
503 | "name": null,
504 | "ofType": {
505 | "kind": "NON_NULL",
506 | "name": null,
507 | "ofType": {
508 | "kind": "OBJECT",
509 | "name": "__Type",
510 | "ofType": null
511 | }
512 | }
513 | },
514 | "isDeprecated": false,
515 | "deprecationReason": null
516 | },
517 | {
518 | "name": "possibleTypes",
519 | "description": null,
520 | "args": [],
521 | "type": {
522 | "kind": "LIST",
523 | "name": null,
524 | "ofType": {
525 | "kind": "NON_NULL",
526 | "name": null,
527 | "ofType": {
528 | "kind": "OBJECT",
529 | "name": "__Type",
530 | "ofType": null
531 | }
532 | }
533 | },
534 | "isDeprecated": false,
535 | "deprecationReason": null
536 | },
537 | {
538 | "name": "enumValues",
539 | "description": null,
540 | "args": [
541 | {
542 | "name": "includeDeprecated",
543 | "description": null,
544 | "type": {
545 | "kind": "SCALAR",
546 | "name": "Boolean",
547 | "ofType": null
548 | },
549 | "defaultValue": "false"
550 | }
551 | ],
552 | "type": {
553 | "kind": "LIST",
554 | "name": null,
555 | "ofType": {
556 | "kind": "NON_NULL",
557 | "name": null,
558 | "ofType": {
559 | "kind": "OBJECT",
560 | "name": "__EnumValue",
561 | "ofType": null
562 | }
563 | }
564 | },
565 | "isDeprecated": false,
566 | "deprecationReason": null
567 | },
568 | {
569 | "name": "inputFields",
570 | "description": null,
571 | "args": [],
572 | "type": {
573 | "kind": "LIST",
574 | "name": null,
575 | "ofType": {
576 | "kind": "NON_NULL",
577 | "name": null,
578 | "ofType": {
579 | "kind": "OBJECT",
580 | "name": "__InputValue",
581 | "ofType": null
582 | }
583 | }
584 | },
585 | "isDeprecated": false,
586 | "deprecationReason": null
587 | },
588 | {
589 | "name": "ofType",
590 | "description": null,
591 | "args": [],
592 | "type": {
593 | "kind": "OBJECT",
594 | "name": "__Type",
595 | "ofType": null
596 | },
597 | "isDeprecated": false,
598 | "deprecationReason": null
599 | }
600 | ],
601 | "inputFields": null,
602 | "interfaces": [],
603 | "enumValues": null,
604 | "possibleTypes": null
605 | },
606 | {
607 | "kind": "ENUM",
608 | "name": "__TypeKind",
609 | "description": "An enum describing what kind of type a given `__Type` is.",
610 | "fields": null,
611 | "inputFields": null,
612 | "interfaces": null,
613 | "enumValues": [
614 | {
615 | "name": "SCALAR",
616 | "description": "Indicates this type is a scalar.",
617 | "isDeprecated": false,
618 | "deprecationReason": null
619 | },
620 | {
621 | "name": "OBJECT",
622 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.",
623 | "isDeprecated": false,
624 | "deprecationReason": null
625 | },
626 | {
627 | "name": "INTERFACE",
628 | "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.",
629 | "isDeprecated": false,
630 | "deprecationReason": null
631 | },
632 | {
633 | "name": "UNION",
634 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.",
635 | "isDeprecated": false,
636 | "deprecationReason": null
637 | },
638 | {
639 | "name": "ENUM",
640 | "description": "Indicates this type is an enum. `enumValues` is a valid field.",
641 | "isDeprecated": false,
642 | "deprecationReason": null
643 | },
644 | {
645 | "name": "INPUT_OBJECT",
646 | "description": "Indicates this type is an input object. `inputFields` is a valid field.",
647 | "isDeprecated": false,
648 | "deprecationReason": null
649 | },
650 | {
651 | "name": "LIST",
652 | "description": "Indicates this type is a list. `ofType` is a valid field.",
653 | "isDeprecated": false,
654 | "deprecationReason": null
655 | },
656 | {
657 | "name": "NON_NULL",
658 | "description": "Indicates this type is a non-null. `ofType` is a valid field.",
659 | "isDeprecated": false,
660 | "deprecationReason": null
661 | }
662 | ],
663 | "possibleTypes": null
664 | },
665 | {
666 | "kind": "SCALAR",
667 | "name": "Boolean",
668 | "description": "The `Boolean` scalar type represents `true` or `false`.",
669 | "fields": null,
670 | "inputFields": null,
671 | "interfaces": null,
672 | "enumValues": null,
673 | "possibleTypes": null
674 | },
675 | {
676 | "kind": "OBJECT",
677 | "name": "__Field",
678 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.",
679 | "fields": [
680 | {
681 | "name": "name",
682 | "description": null,
683 | "args": [],
684 | "type": {
685 | "kind": "NON_NULL",
686 | "name": null,
687 | "ofType": {
688 | "kind": "SCALAR",
689 | "name": "String",
690 | "ofType": null
691 | }
692 | },
693 | "isDeprecated": false,
694 | "deprecationReason": null
695 | },
696 | {
697 | "name": "description",
698 | "description": null,
699 | "args": [],
700 | "type": {
701 | "kind": "SCALAR",
702 | "name": "String",
703 | "ofType": null
704 | },
705 | "isDeprecated": false,
706 | "deprecationReason": null
707 | },
708 | {
709 | "name": "args",
710 | "description": null,
711 | "args": [],
712 | "type": {
713 | "kind": "NON_NULL",
714 | "name": null,
715 | "ofType": {
716 | "kind": "LIST",
717 | "name": null,
718 | "ofType": {
719 | "kind": "NON_NULL",
720 | "name": null,
721 | "ofType": {
722 | "kind": "OBJECT",
723 | "name": "__InputValue",
724 | "ofType": null
725 | }
726 | }
727 | }
728 | },
729 | "isDeprecated": false,
730 | "deprecationReason": null
731 | },
732 | {
733 | "name": "type",
734 | "description": null,
735 | "args": [],
736 | "type": {
737 | "kind": "NON_NULL",
738 | "name": null,
739 | "ofType": {
740 | "kind": "OBJECT",
741 | "name": "__Type",
742 | "ofType": null
743 | }
744 | },
745 | "isDeprecated": false,
746 | "deprecationReason": null
747 | },
748 | {
749 | "name": "isDeprecated",
750 | "description": null,
751 | "args": [],
752 | "type": {
753 | "kind": "NON_NULL",
754 | "name": null,
755 | "ofType": {
756 | "kind": "SCALAR",
757 | "name": "Boolean",
758 | "ofType": null
759 | }
760 | },
761 | "isDeprecated": false,
762 | "deprecationReason": null
763 | },
764 | {
765 | "name": "deprecationReason",
766 | "description": null,
767 | "args": [],
768 | "type": {
769 | "kind": "SCALAR",
770 | "name": "String",
771 | "ofType": null
772 | },
773 | "isDeprecated": false,
774 | "deprecationReason": null
775 | }
776 | ],
777 | "inputFields": null,
778 | "interfaces": [],
779 | "enumValues": null,
780 | "possibleTypes": null
781 | },
782 | {
783 | "kind": "OBJECT",
784 | "name": "__InputValue",
785 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.",
786 | "fields": [
787 | {
788 | "name": "name",
789 | "description": null,
790 | "args": [],
791 | "type": {
792 | "kind": "NON_NULL",
793 | "name": null,
794 | "ofType": {
795 | "kind": "SCALAR",
796 | "name": "String",
797 | "ofType": null
798 | }
799 | },
800 | "isDeprecated": false,
801 | "deprecationReason": null
802 | },
803 | {
804 | "name": "description",
805 | "description": null,
806 | "args": [],
807 | "type": {
808 | "kind": "SCALAR",
809 | "name": "String",
810 | "ofType": null
811 | },
812 | "isDeprecated": false,
813 | "deprecationReason": null
814 | },
815 | {
816 | "name": "type",
817 | "description": null,
818 | "args": [],
819 | "type": {
820 | "kind": "NON_NULL",
821 | "name": null,
822 | "ofType": {
823 | "kind": "OBJECT",
824 | "name": "__Type",
825 | "ofType": null
826 | }
827 | },
828 | "isDeprecated": false,
829 | "deprecationReason": null
830 | },
831 | {
832 | "name": "defaultValue",
833 | "description": "A GraphQL-formatted string representing the default value for this input value.",
834 | "args": [],
835 | "type": {
836 | "kind": "SCALAR",
837 | "name": "String",
838 | "ofType": null
839 | },
840 | "isDeprecated": false,
841 | "deprecationReason": null
842 | }
843 | ],
844 | "inputFields": null,
845 | "interfaces": [],
846 | "enumValues": null,
847 | "possibleTypes": null
848 | },
849 | {
850 | "kind": "OBJECT",
851 | "name": "__EnumValue",
852 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.",
853 | "fields": [
854 | {
855 | "name": "name",
856 | "description": null,
857 | "args": [],
858 | "type": {
859 | "kind": "NON_NULL",
860 | "name": null,
861 | "ofType": {
862 | "kind": "SCALAR",
863 | "name": "String",
864 | "ofType": null
865 | }
866 | },
867 | "isDeprecated": false,
868 | "deprecationReason": null
869 | },
870 | {
871 | "name": "description",
872 | "description": null,
873 | "args": [],
874 | "type": {
875 | "kind": "SCALAR",
876 | "name": "String",
877 | "ofType": null
878 | },
879 | "isDeprecated": false,
880 | "deprecationReason": null
881 | },
882 | {
883 | "name": "isDeprecated",
884 | "description": null,
885 | "args": [],
886 | "type": {
887 | "kind": "NON_NULL",
888 | "name": null,
889 | "ofType": {
890 | "kind": "SCALAR",
891 | "name": "Boolean",
892 | "ofType": null
893 | }
894 | },
895 | "isDeprecated": false,
896 | "deprecationReason": null
897 | },
898 | {
899 | "name": "deprecationReason",
900 | "description": null,
901 | "args": [],
902 | "type": {
903 | "kind": "SCALAR",
904 | "name": "String",
905 | "ofType": null
906 | },
907 | "isDeprecated": false,
908 | "deprecationReason": null
909 | }
910 | ],
911 | "inputFields": null,
912 | "interfaces": [],
913 | "enumValues": null,
914 | "possibleTypes": null
915 | },
916 | {
917 | "kind": "OBJECT",
918 | "name": "__Directive",
919 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.",
920 | "fields": [
921 | {
922 | "name": "name",
923 | "description": null,
924 | "args": [],
925 | "type": {
926 | "kind": "NON_NULL",
927 | "name": null,
928 | "ofType": {
929 | "kind": "SCALAR",
930 | "name": "String",
931 | "ofType": null
932 | }
933 | },
934 | "isDeprecated": false,
935 | "deprecationReason": null
936 | },
937 | {
938 | "name": "description",
939 | "description": null,
940 | "args": [],
941 | "type": {
942 | "kind": "SCALAR",
943 | "name": "String",
944 | "ofType": null
945 | },
946 | "isDeprecated": false,
947 | "deprecationReason": null
948 | },
949 | {
950 | "name": "locations",
951 | "description": null,
952 | "args": [],
953 | "type": {
954 | "kind": "NON_NULL",
955 | "name": null,
956 | "ofType": {
957 | "kind": "LIST",
958 | "name": null,
959 | "ofType": {
960 | "kind": "NON_NULL",
961 | "name": null,
962 | "ofType": {
963 | "kind": "ENUM",
964 | "name": "__DirectiveLocation",
965 | "ofType": null
966 | }
967 | }
968 | }
969 | },
970 | "isDeprecated": false,
971 | "deprecationReason": null
972 | },
973 | {
974 | "name": "args",
975 | "description": null,
976 | "args": [],
977 | "type": {
978 | "kind": "NON_NULL",
979 | "name": null,
980 | "ofType": {
981 | "kind": "LIST",
982 | "name": null,
983 | "ofType": {
984 | "kind": "NON_NULL",
985 | "name": null,
986 | "ofType": {
987 | "kind": "OBJECT",
988 | "name": "__InputValue",
989 | "ofType": null
990 | }
991 | }
992 | }
993 | },
994 | "isDeprecated": false,
995 | "deprecationReason": null
996 | }
997 | ],
998 | "inputFields": null,
999 | "interfaces": [],
1000 | "enumValues": null,
1001 | "possibleTypes": null
1002 | },
1003 | {
1004 | "kind": "ENUM",
1005 | "name": "__DirectiveLocation",
1006 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.",
1007 | "fields": null,
1008 | "inputFields": null,
1009 | "interfaces": null,
1010 | "enumValues": [
1011 | {
1012 | "name": "QUERY",
1013 | "description": "Location adjacent to a query operation.",
1014 | "isDeprecated": false,
1015 | "deprecationReason": null
1016 | },
1017 | {
1018 | "name": "MUTATION",
1019 | "description": "Location adjacent to a mutation operation.",
1020 | "isDeprecated": false,
1021 | "deprecationReason": null
1022 | },
1023 | {
1024 | "name": "SUBSCRIPTION",
1025 | "description": "Location adjacent to a subscription operation.",
1026 | "isDeprecated": false,
1027 | "deprecationReason": null
1028 | },
1029 | {
1030 | "name": "FIELD",
1031 | "description": "Location adjacent to a field.",
1032 | "isDeprecated": false,
1033 | "deprecationReason": null
1034 | },
1035 | {
1036 | "name": "FRAGMENT_DEFINITION",
1037 | "description": "Location adjacent to a fragment definition.",
1038 | "isDeprecated": false,
1039 | "deprecationReason": null
1040 | },
1041 | {
1042 | "name": "FRAGMENT_SPREAD",
1043 | "description": "Location adjacent to a fragment spread.",
1044 | "isDeprecated": false,
1045 | "deprecationReason": null
1046 | },
1047 | {
1048 | "name": "INLINE_FRAGMENT",
1049 | "description": "Location adjacent to an inline fragment.",
1050 | "isDeprecated": false,
1051 | "deprecationReason": null
1052 | },
1053 | {
1054 | "name": "VARIABLE_DEFINITION",
1055 | "description": "Location adjacent to a variable definition.",
1056 | "isDeprecated": false,
1057 | "deprecationReason": null
1058 | },
1059 | {
1060 | "name": "SCHEMA",
1061 | "description": "Location adjacent to a schema definition.",
1062 | "isDeprecated": false,
1063 | "deprecationReason": null
1064 | },
1065 | {
1066 | "name": "SCALAR",
1067 | "description": "Location adjacent to a scalar definition.",
1068 | "isDeprecated": false,
1069 | "deprecationReason": null
1070 | },
1071 | {
1072 | "name": "OBJECT",
1073 | "description": "Location adjacent to an object type definition.",
1074 | "isDeprecated": false,
1075 | "deprecationReason": null
1076 | },
1077 | {
1078 | "name": "FIELD_DEFINITION",
1079 | "description": "Location adjacent to a field definition.",
1080 | "isDeprecated": false,
1081 | "deprecationReason": null
1082 | },
1083 | {
1084 | "name": "ARGUMENT_DEFINITION",
1085 | "description": "Location adjacent to an argument definition.",
1086 | "isDeprecated": false,
1087 | "deprecationReason": null
1088 | },
1089 | {
1090 | "name": "INTERFACE",
1091 | "description": "Location adjacent to an interface definition.",
1092 | "isDeprecated": false,
1093 | "deprecationReason": null
1094 | },
1095 | {
1096 | "name": "UNION",
1097 | "description": "Location adjacent to a union definition.",
1098 | "isDeprecated": false,
1099 | "deprecationReason": null
1100 | },
1101 | {
1102 | "name": "ENUM",
1103 | "description": "Location adjacent to an enum definition.",
1104 | "isDeprecated": false,
1105 | "deprecationReason": null
1106 | },
1107 | {
1108 | "name": "ENUM_VALUE",
1109 | "description": "Location adjacent to an enum value definition.",
1110 | "isDeprecated": false,
1111 | "deprecationReason": null
1112 | },
1113 | {
1114 | "name": "INPUT_OBJECT",
1115 | "description": "Location adjacent to an input object type definition.",
1116 | "isDeprecated": false,
1117 | "deprecationReason": null
1118 | },
1119 | {
1120 | "name": "INPUT_FIELD_DEFINITION",
1121 | "description": "Location adjacent to an input object field definition.",
1122 | "isDeprecated": false,
1123 | "deprecationReason": null
1124 | }
1125 | ],
1126 | "possibleTypes": null
1127 | }
1128 | ],
1129 | "directives": [
1130 | {
1131 | "name": "include",
1132 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.",
1133 | "locations": [
1134 | "FIELD",
1135 | "FRAGMENT_SPREAD",
1136 | "INLINE_FRAGMENT"
1137 | ],
1138 | "args": [
1139 | {
1140 | "name": "if",
1141 | "description": "Included when true.",
1142 | "type": {
1143 | "kind": "NON_NULL",
1144 | "name": null,
1145 | "ofType": {
1146 | "kind": "SCALAR",
1147 | "name": "Boolean",
1148 | "ofType": null
1149 | }
1150 | },
1151 | "defaultValue": null
1152 | }
1153 | ]
1154 | },
1155 | {
1156 | "name": "skip",
1157 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.",
1158 | "locations": [
1159 | "FIELD",
1160 | "FRAGMENT_SPREAD",
1161 | "INLINE_FRAGMENT"
1162 | ],
1163 | "args": [
1164 | {
1165 | "name": "if",
1166 | "description": "Skipped when true.",
1167 | "type": {
1168 | "kind": "NON_NULL",
1169 | "name": null,
1170 | "ofType": {
1171 | "kind": "SCALAR",
1172 | "name": "Boolean",
1173 | "ofType": null
1174 | }
1175 | },
1176 | "defaultValue": null
1177 | }
1178 | ]
1179 | },
1180 | {
1181 | "name": "deprecated",
1182 | "description": "Marks an element of a GraphQL schema as no longer supported.",
1183 | "locations": [
1184 | "FIELD_DEFINITION",
1185 | "ENUM_VALUE"
1186 | ],
1187 | "args": [
1188 | {
1189 | "name": "reason",
1190 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/).",
1191 | "type": {
1192 | "kind": "SCALAR",
1193 | "name": "String",
1194 | "ofType": null
1195 | },
1196 | "defaultValue": "\"No longer supported\""
1197 | }
1198 | ]
1199 | }
1200 | ]
1201 | }
1202 | }
1203 |
--------------------------------------------------------------------------------
/core/core_network/src/main/kotlin/com/redmadrobot/core_network/ApiFactory.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_network
2 |
3 | import com.apollographql.apollo.ApolloClient
4 | import com.redmadrobot.core_network.exception.NetworkExceptionHandler
5 | import okhttp3.OkHttpClient
6 | import java.util.concurrent.TimeUnit
7 |
8 | object ApiFactory {
9 |
10 | private const val DEFAULT_SERVER = "https://mad-showcase-fake-server.herokuapp.com/graphql"
11 |
12 | fun provideApi(networkExceptionHandler: NetworkExceptionHandler): ApolloApi {
13 | val okhttpClient = provideOkHttpClient()
14 | val apolloClient = provideApolloClient(okhttpClient)
15 | return ApolloApi(apolloClient, networkExceptionHandler)
16 | }
17 |
18 | private fun provideApolloClient(
19 | okHttpClient: OkHttpClient,
20 | ): ApolloClient {
21 | return ApolloClient.builder()
22 | .serverUrl(DEFAULT_SERVER)
23 | .okHttpClient(okHttpClient)
24 | .build()
25 | }
26 |
27 | private fun provideOkHttpClient(): OkHttpClient {
28 | return OkHttpClient.Builder()
29 | .connectTimeout(NetworkConstants.CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
30 | .readTimeout(NetworkConstants.READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
31 | .writeTimeout(NetworkConstants.WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
32 | .build()
33 | }
34 | }
--------------------------------------------------------------------------------
/core/core_network/src/main/kotlin/com/redmadrobot/core_network/ApolloApi.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_network
2 |
3 | import com.apollographql.apollo.ApolloClient
4 | import com.apollographql.apollo.api.Mutation
5 | import com.apollographql.apollo.api.Operation
6 | import com.apollographql.apollo.api.Operation.Variables
7 | import com.apollographql.apollo.api.Query
8 | import com.apollographql.apollo.api.Response
9 | import com.apollographql.apollo.coroutines.await
10 | import com.redmadrobot.core_network.exception.NetworkExceptionHandler
11 | import com.redmadrobot.core_network.exception.ServerException
12 | import com.redmadrobot.core_network.exception.ServerExceptionFactory
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.withContext
15 |
16 | open class ApolloApi(
17 | private val apolloClient: ApolloClient,
18 | private val networkErrorHandler: NetworkExceptionHandler
19 | ) {
20 |
21 | suspend fun query(query: Query): T {
22 | val response = queryForRawResponse(query)
23 | return response.data ?: throw getServerException(response)
24 | }
25 |
26 | suspend fun mutate(mutation: Mutation): T {
27 | val response = mutateForRawResponse(mutation)
28 | return response.data ?: throw getServerException(response)
29 | }
30 |
31 | suspend fun mutateForRawResponse(mutation: Mutation): Response {
32 | return withContext(Dispatchers.IO) {
33 | val response: Response?
34 | try {
35 | response = apolloClient.mutate(mutation).await()
36 | } catch (exception: Throwable) {
37 | throw networkErrorHandler.handle(exception)
38 | }
39 | response
40 | }
41 | }
42 |
43 | suspend fun queryForRawResponse(query: Query): Response {
44 | return withContext(Dispatchers.IO) {
45 | val response: Response?
46 | try {
47 | response = apolloClient.query(query).await()
48 | } catch (exception: Throwable) {
49 | throw networkErrorHandler.handle(exception)
50 | }
51 | response
52 | }
53 | }
54 |
55 | private fun getServerException(response: Response): ServerException {
56 | return ServerExceptionFactory.createException(checkNotNull(response.errors))
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/core/core_network/src/main/kotlin/com/redmadrobot/core_network/NetworkConstants.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_network
2 |
3 | internal object NetworkConstants {
4 | const val CONNECT_TIMEOUT_SECONDS = 5L
5 | const val READ_TIMEOUT_SECONDS = 30L
6 | const val WRITE_TIMEOUT_SECONDS = 10L
7 | }
--------------------------------------------------------------------------------
/core/core_network/src/main/kotlin/com/redmadrobot/core_network/exception/NetworkException.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_network.exception
2 |
3 | open class NetworkException(message: String) : Exception(message)
4 |
--------------------------------------------------------------------------------
/core/core_network/src/main/kotlin/com/redmadrobot/core_network/exception/NetworkExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_network.exception
2 |
3 | import android.content.Context
4 | import com.apollographql.apollo.exception.ApolloNetworkException
5 | import com.redmadrobot.core_network.R
6 | import java.net.*
7 |
8 | class NetworkExceptionHandler constructor(private val context: Context) {
9 |
10 | fun handle(exception: Throwable): NetworkException {
11 | val message: String = if (exception is ApolloNetworkException &&
12 | exception.cause.isNetworkException()
13 | ) {
14 | context.getString(R.string.error_no_internet)
15 | } else context.getString(R.string.error_network_unknown)
16 |
17 | return NetworkException(message)
18 | }
19 |
20 | private fun Throwable?.isNetworkException(): Boolean {
21 | return when (this) {
22 | is ConnectException,
23 | is SocketException,
24 | is SocketTimeoutException,
25 | is UnknownHostException,
26 | is ProtocolException -> true
27 | else -> false
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/core/core_network/src/main/kotlin/com/redmadrobot/core_network/exception/ServerException.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_network.exception
2 |
3 | open class ServerException(message: String) : Exception(message)
4 |
--------------------------------------------------------------------------------
/core/core_network/src/main/kotlin/com/redmadrobot/core_network/exception/ServerExceptionFactory.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_network.exception
2 |
3 | import com.apollographql.apollo.api.Error
4 |
5 | object ServerExceptionFactory {
6 |
7 | private const val EXTENSIONS = "extensions"
8 | private const val TYPE = "type"
9 |
10 | fun createException(errors: List): ServerException {
11 | val error = errors.first()
12 | val message = error.message
13 | val extensions = error.customAttributes[EXTENSIONS] as? HashMap<*, *>
14 | val errorType = extensions?.let { it[TYPE] as? String }.orEmpty()
15 | return getException(message, errorType)
16 | }
17 |
18 | @Suppress("UseIfInsteadOfWhen")
19 | private fun getException(message: String, errorType: String): ServerException {
20 | return when (errorType) {
21 | "" -> UnknownTypeServerException(message)
22 | else -> ServerException(message)
23 | }
24 | }
25 | }
26 |
27 | class UnknownTypeServerException(message: String) : ServerException(message)
28 |
--------------------------------------------------------------------------------
/core/core_network/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Something went wrong
4 | No internet connection.\nTry again
5 |
--------------------------------------------------------------------------------
/core/core_presentation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/core_presentation/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIBRARY)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | }
5 |
6 | dependencies {
7 | api(PresentationDependency.APPCOMPAT)
8 | api(PresentationDependency.COMPOSE_UI)
9 | api(PresentationDependency.COMPOSE_ANIMATION)
10 | api(PresentationDependency.COMPOSE_COMPILER)
11 | api(PresentationDependency.COMPOSE_TOOLING)
12 | api(PresentationDependency.COMPOSE_TOOLING_PREVIEW)
13 | api(PresentationDependency.COMPOSE_FOUNDATION)
14 | api(PresentationDependency.COMPOSE_MATERIAL)
15 | api(PresentationDependency.COMPOSE_ACTIVITY)
16 | api(PresentationDependency.COMPOSE_LIFECYCLE)
17 | }
18 |
--------------------------------------------------------------------------------
/core/core_presentation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/core_presentation/src/main/kotlin/com/redmadrobot/core_presentation/extensions/StateFlowExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_presentation.extensions
2 |
3 | import kotlinx.coroutines.flow.MutableStateFlow
4 |
5 | fun MutableStateFlow.requireValue(
6 | messageIfNull: String = "required value was null and not set"
7 | ): T {
8 | return this.value ?: error(messageIfNull)
9 | }
10 |
11 | inline fun MutableStateFlow.update(action: T.() -> T) {
12 | value = action.invoke(requireValue())
13 | }
14 |
--------------------------------------------------------------------------------
/core/core_presentation/src/main/kotlin/com/redmadrobot/core_presentation/model/State.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.core_presentation.model
2 |
3 | sealed class State
4 |
5 | data class Content(val content: T) : State()
6 | class Loading : State()
7 | class Stub(val error: Throwable) : State()
8 |
--------------------------------------------------------------------------------
/fake_server/.gitignore:
--------------------------------------------------------------------------------
1 | # editor
2 |
3 | .vscode
4 |
5 | # packages
6 |
7 | node_modules
8 |
9 | # misc
10 |
11 | .DS_Store
12 | .DS_Store?
13 | ._*
14 | .Spotlight-V100
15 | .Trashes
16 | ehthumbs.db
17 | *[Tt]humbs.db
18 | *.Trashes
19 |
--------------------------------------------------------------------------------
/fake_server/README.md:
--------------------------------------------------------------------------------
1 | # Fake server with GgaphQL
2 |
3 | **Installation:**
4 |
5 | ```bash
6 | npm i && npm run start
7 | ```
8 |
9 | **Tutorial:**
10 |
11 | Inspired by https://www.apollographql.com/blog/setting-up-authentication-and-authorization-with-apollo-federation 👩💻
12 |
--------------------------------------------------------------------------------
/fake_server/accounts/index.js:
--------------------------------------------------------------------------------
1 | const { ApolloServer, gql } = require("apollo-server");
2 | const { applyMiddleware } = require("graphql-middleware");
3 | const { buildFederatedSchema } = require("@apollo/federation");
4 | const jwt = require("jsonwebtoken");
5 |
6 | const { accounts } = require("../data");
7 | const { permissions } = require("./permissions");
8 |
9 | const { accounts_port } = require("../config")
10 |
11 | const typeDefs = gql`
12 | type Account @key(fields: "id") {
13 | id: ID!
14 | name: String
15 | }
16 |
17 | extend type Query {
18 | account(id: ID!): Account
19 | accounts: [Account]
20 | viewer: Account!
21 | }
22 |
23 | extend type Mutation {
24 | login(email: String!, password: String!): String
25 | }
26 | `;
27 |
28 | const resolvers = {
29 | Account: {
30 | __resolveReference(object) {
31 | return accounts.find(account => account.id === object.id);
32 | }
33 | },
34 | Query: {
35 | account(parent, { id }) {
36 | return accounts.find(account => account.id === id);
37 | },
38 | accounts() {
39 | return accounts;
40 | },
41 | viewer(parent, args, { user }) {
42 | return accounts.find(account => account.id === user.sub);
43 | }
44 | },
45 | Mutation: {
46 | login(parent, { email, password }) {
47 | const { id, permissions, roles } = accounts.find(
48 | account => account.email === email && account.password === password
49 | );
50 | return jwt.sign(
51 | { "https://awesomeapi.com/graphql": { roles, permissions } },
52 | "f1BtnWgD3VKY",
53 | { algorithm: "HS256", subject: id, expiresIn: "1d" }
54 | );
55 | }
56 | }
57 | };
58 |
59 | const server = new ApolloServer({
60 | schema: applyMiddleware(
61 | buildFederatedSchema([{ typeDefs, resolvers }]),
62 | permissions
63 | ),
64 | context: ({ req }) => {
65 | const user = req.headers.user ? JSON.parse(req.headers.user) : null;
66 | return { user };
67 | }
68 | });
69 |
70 | server.listen({ port: accounts_port }).then(({ url }) => {
71 | console.log(`Accounts service ready at ${url}`);
72 | });
73 |
--------------------------------------------------------------------------------
/fake_server/accounts/permissions.js:
--------------------------------------------------------------------------------
1 | const { and, or, rule, shield } = require("graphql-shield");
2 |
3 | function getPermissions(user) {
4 | if (user && user["https://awesomeapi.com/graphql"]) {
5 | return user["https://awesomeapi.com/graphql"].permissions;
6 | }
7 | return [];
8 | }
9 |
10 | const isAuthenticated = rule()((parent, args, { user }) => {
11 | return user !== null;
12 | });
13 |
14 | const canReadAnyAccount = rule()((parent, args, { user }) => {
15 | const userPermissions = getPermissions(user);
16 | return userPermissions.includes("read:any_account");
17 | });
18 |
19 | const canReadOwnAccount = rule()((parent, args, { user }) => {
20 | const userPermissions = getPermissions(user);
21 | return userPermissions.includes("read:own_account");
22 | });
23 |
24 | const isReadingOwnAccount = rule()((parent, { id }, { user }) => {
25 | return user && user.sub === id;
26 | });
27 |
28 | const permissions = shield({
29 | Query: {
30 | account: or(and(canReadOwnAccount, isReadingOwnAccount), canReadAnyAccount),
31 | accounts: canReadAnyAccount,
32 | viewer: isAuthenticated
33 | }
34 | });
35 |
36 | module.exports = { permissions };
37 |
--------------------------------------------------------------------------------
/fake_server/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | port: process.env.PORT || 6000,
3 | accounts_port: process.env.ACCOUNTS_PORT || 6001
4 | }
5 |
--------------------------------------------------------------------------------
/fake_server/data.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | accounts: [
3 | {
4 | id: "12345",
5 | name: "QA",
6 | email: "robot@redmadrobot.com",
7 | password: "Qq!11111",
8 | roles: ["admin"],
9 | permissions: ["read:any_account", "read:own_account"]
10 | },
11 | {
12 | id: "67890",
13 | name: "qa",
14 | email: "qa@redmadrobot.com",
15 | password: "Qq!11111",
16 | roles: ["subscriber"],
17 | permissions: ["read:own_account"]
18 | }
19 | ]
20 | };
21 |
--------------------------------------------------------------------------------
/fake_server/index.js:
--------------------------------------------------------------------------------
1 | const { ApolloGateway, RemoteGraphQLDataSource } = require("@apollo/gateway");
2 | const { ApolloServer } = require("apollo-server-express");
3 | const { port, accounts_port } = require("./config")
4 |
5 | const express = require("express");
6 | const expressJwt = require("express-jwt");
7 |
8 | const app = express();
9 |
10 | app.use(
11 | expressJwt({
12 | secret: "f1BtnWgD3VKY",
13 | algorithms: ["HS256"],
14 | credentialsRequired: false
15 | })
16 | );
17 |
18 | const gateway = new ApolloGateway({
19 | serviceList: [{ name: "accounts", url: `http://0.0.0.0:${accounts_port}`}],
20 | buildService({ name, url }) {
21 | return new RemoteGraphQLDataSource({
22 | url,
23 | willSendRequest({ request, context }) {
24 | request.http.headers.set(
25 | "user",
26 | context.user ? JSON.stringify(context.user) : null
27 | );
28 | }
29 | });
30 | }
31 | });
32 |
33 | const server = new ApolloServer({
34 | gateway,
35 | subscriptions: false,
36 | context: ({ req }) => {
37 | const user = req.user || null;
38 | return { user };
39 | },
40 | introspection: true,
41 | });
42 |
43 | server.applyMiddleware({ app });
44 |
45 | app.listen({ port }, () =>
46 | console.log(`Listening on ${port}${server.graphqlPath}`)
47 | );
48 |
--------------------------------------------------------------------------------
/fake_server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "apollo-federation-auth-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "concurrently -k npm:start:*",
8 | "start:accounts": "nodemon ./accounts/index.js",
9 | "start:gateway": "wait-on tcp:5001 && nodemon ./index.js"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "@apollo/federation": "^0.13.2",
16 | "@apollo/gateway": "^0.13.2",
17 | "apollo-server": "^2.11.0",
18 | "apollo-server-express": "^2.11.0",
19 | "concurrently": "^5.1.0",
20 | "express": "^4.17.1",
21 | "express-jwt": "^5.3.3",
22 | "graphql-middleware": "^4.0.2",
23 | "graphql-shield": "^7.2.3",
24 | "jsonwebtoken": "^8.5.1",
25 | "nodemon": "^2.0.3",
26 | "wait-on": "^4.0.2"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/features/auth/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/features/auth/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIBRARY)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | kotlin(GradlePluginId.KAPT)
5 | id(GradlePluginId.HILT)
6 | }
7 |
8 | dependencies {
9 | implementation(project(":core:core"))
10 |
11 | implementation(CoreDependency.HILT)
12 | kapt(CoreDependency.HILT_COMPILER)
13 | }
14 |
--------------------------------------------------------------------------------
/features/auth/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/features/auth/src/main/kotlin/com/redmadrobot/auth/presentation/AuthScreen.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.auth.presentation
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material.Button
8 | import androidx.compose.material.CircularProgressIndicator
9 | import androidx.compose.material.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.unit.dp
17 | import androidx.hilt.navigation.compose.hiltViewModel
18 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
19 | import com.redmadrobot.auth.R
20 |
21 | @Composable
22 | fun AuthRoute(viewModel: AuthViewModel = hiltViewModel()) {
23 | val isLoading = viewModel.viewState.collectAsStateWithLifecycle().value.isLoading
24 | AuthScreen(
25 | isLoading,
26 | viewModel::onLoginClicked
27 | )
28 | }
29 |
30 | @Composable
31 | fun AuthScreen(
32 | isLoading: Boolean,
33 | onLoginClicked: () -> Unit,
34 | ) {
35 | Column(
36 | modifier = Modifier.fillMaxSize(),
37 | verticalArrangement = Arrangement.Center,
38 | horizontalAlignment = Alignment.CenterHorizontally
39 | ) {
40 | Text(
41 | text = stringResource(id = R.string.login_title),
42 | color = Color.White,
43 | modifier = Modifier.padding(bottom = 4.dp),
44 | )
45 | if (isLoading) {
46 | CircularProgressIndicator()
47 | } else {
48 | Button(onClick = onLoginClicked) {
49 | Text(stringResource(id = R.string.login_button_title))
50 | }
51 | }
52 | }
53 | }
54 |
55 | @Preview(
56 | showBackground = true,
57 | backgroundColor = 0x434A74
58 | )
59 | @Composable
60 | fun PreviewAuthScreen() {
61 | return AuthScreen(isLoading = false) {}
62 | }
--------------------------------------------------------------------------------
/features/auth/src/main/kotlin/com/redmadrobot/auth/presentation/AuthState.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.auth.presentation
2 |
3 | data class AuthState(
4 | val isLoading: Boolean
5 | )
--------------------------------------------------------------------------------
/features/auth/src/main/kotlin/com/redmadrobot/auth/presentation/AuthViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.auth.presentation
2 |
3 | import android.app.Application
4 | import android.widget.Toast
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.redmadrobot.core.extensions.safeLaunch
8 | import com.redmadrobot.core_navigation.navigation.Router
9 | import com.redmadrobot.core_navigation.navigation.screens.Routes
10 | import com.redmadrobot.core_network.ApolloApi
11 | import com.redmadrobot.core_network.LoginMutation
12 | import com.redmadrobot.core_presentation.extensions.update
13 | import dagger.hilt.android.lifecycle.HiltViewModel
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.flow.StateFlow
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class AuthViewModel @Inject constructor(
20 | private val api: ApolloApi,
21 | private val router: Router,
22 | private val context: Application,
23 | ) : ViewModel() {
24 |
25 | private val _viewState = MutableStateFlow(AuthState(isLoading = false))
26 | val viewState: StateFlow = _viewState
27 |
28 | fun onLoginClicked() {
29 | _viewState.update { copy(isLoading = true) }
30 | viewModelScope.safeLaunch(
31 | {
32 | api.mutate(LoginMutation("robot@redmadrobot.com", "Qq!11111"))
33 | router.navigate(Routes.Home)
34 | },
35 | onError = { throwable ->
36 | Toast.makeText(context, throwable.localizedMessage, Toast.LENGTH_LONG).show()
37 | }
38 | )
39 | }
40 | }
--------------------------------------------------------------------------------
/features/auth/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Please Login
3 | Login
4 |
--------------------------------------------------------------------------------
/features/details/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIBRARY)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | kotlin(GradlePluginId.KAPT)
5 | id(GradlePluginId.HILT)
6 | }
7 |
8 | dependencies {
9 | implementation(project(":core:core"))
10 | implementation(project(":base:base_cards"))
11 |
12 | implementation(CoreDependency.HILT)
13 | kapt(CoreDependency.HILT_COMPILER)
14 | }
--------------------------------------------------------------------------------
/features/details/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/features/details/src/main/kotlin/com/redmadrobot/details/presentation/DetailsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.details.presentation
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.Button
5 | import androidx.compose.material.CircularProgressIndicator
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.ExperimentalComposeUiApi
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import androidx.compose.ui.unit.dp
14 | import androidx.hilt.navigation.compose.hiltViewModel
15 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
16 | import com.redmadrobot.base_cards.toCardViewState
17 | import com.redmadrobot.core_network.CardDetailsQuery.Card
18 | import com.redmadrobot.core_presentation.model.Content
19 | import com.redmadrobot.core_presentation.model.Loading
20 | import com.redmadrobot.core_presentation.model.Stub
21 | import com.redmadrobot.details.presentation.view.CreateCustomCard
22 |
23 | @ExperimentalComposeUiApi
24 | @Composable
25 | fun DetailsRoute(viewModel: DetailsViewModel = hiltViewModel()) {
26 | val state = viewModel.viewState.collectAsStateWithLifecycle().value
27 | DetailsScreen(
28 | state, viewModel::onRetryClicked
29 | )
30 | }
31 |
32 | @ExperimentalComposeUiApi
33 | @Composable
34 | private fun DetailsScreen(
35 | state: DetailsViewState,
36 | onRetryClicked: () -> Unit,
37 | ) {
38 | when (state) {
39 | is Loading -> {
40 | Box(Modifier.fillMaxSize()) { CircularProgressIndicator(Modifier.align(Alignment.Center)) }
41 | }
42 | is Content -> {
43 | CreateCustomCard(state.content)
44 | }
45 | is Stub -> {
46 | Column(
47 | modifier = Modifier.fillMaxSize(),
48 | horizontalAlignment = Alignment.CenterHorizontally,
49 | verticalArrangement = Arrangement.Center
50 | ) {
51 | Text(
52 | text = "error: ${state.error}",
53 | color = Color.White,
54 | modifier = Modifier.padding(bottom = 14.dp)
55 | )
56 | Button(onClick = onRetryClicked) {
57 | Text("Try again", color = Color.White)
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
64 | @Preview(
65 | showBackground = true, backgroundColor = 0x434A74
66 | )
67 | @ExperimentalComposeUiApi
68 | @Composable
69 | fun PreviewDetailsScreenWithContent() {
70 | return DetailsScreen(
71 | Content(
72 | Card(
73 | id = "33", number = "3123", type = "MASTER_CARD", color = "green"
74 | ).toCardViewState()
75 | )
76 | ) {}
77 | }
78 |
79 | @Preview(
80 | showBackground = true, backgroundColor = 0x434A74
81 | )
82 | @ExperimentalComposeUiApi
83 | @Composable
84 | fun PreviewDetailsScreenWithError() {
85 | return DetailsScreen(
86 | Stub(RuntimeException())
87 | ) {}
88 | }
89 |
90 |
91 |
--------------------------------------------------------------------------------
/features/details/src/main/kotlin/com/redmadrobot/details/presentation/DetailsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.details.presentation
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.redmadrobot.base_cards.toCardViewState
7 | import com.redmadrobot.core.extensions.safeLaunch
8 | import com.redmadrobot.core_network.ApolloApi
9 | import com.redmadrobot.core_network.CardDetailsQuery
10 | import com.redmadrobot.core_presentation.extensions.update
11 | import com.redmadrobot.core_presentation.model.Content
12 | import com.redmadrobot.core_presentation.model.Loading
13 | import com.redmadrobot.core_presentation.model.Stub
14 | import dagger.hilt.android.lifecycle.HiltViewModel
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import kotlinx.coroutines.flow.StateFlow
17 | import javax.inject.Inject
18 |
19 | @HiltViewModel
20 | class DetailsViewModel @Inject constructor(
21 | private val api: ApolloApi,
22 | savedStateHandle: SavedStateHandle
23 | ) : ViewModel() {
24 |
25 | private val _viewState = MutableStateFlow(DetailsViewState(card = Loading()))
26 | val viewState: StateFlow = _viewState
27 | private val id = savedStateHandle.get("id")!!
28 |
29 | init {
30 | loadDetails()
31 | }
32 |
33 | fun onRetryClicked() = loadDetails()
34 |
35 | private fun loadDetails() {
36 | _viewState.update { copy(card = Loading()) }
37 | viewModelScope.safeLaunch(
38 | {
39 | val card = api.query(CardDetailsQuery(id = id)).card
40 | _viewState.update { copy(card = Content(card.toCardViewState())) }
41 | },
42 | onError = { throwable ->
43 | _viewState.update { copy(card = Stub(throwable)) }
44 | }
45 | )
46 | }
47 | }
--------------------------------------------------------------------------------
/features/details/src/main/kotlin/com/redmadrobot/details/presentation/DetailsViewState.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.details.presentation
2 |
3 | import com.redmadrobot.base_cards.model.CardViewState
4 | import com.redmadrobot.core_presentation.model.State
5 |
6 | typealias DetailsViewState = State
--------------------------------------------------------------------------------
/features/details/src/main/kotlin/com/redmadrobot/details/presentation/model/CardDetailsParams.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.details.presentation.model
2 |
3 | import android.graphics.PointF
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.graphics.Path
7 |
8 | var cardPoints = emptyList()
9 | var additionalPoints = emptyList()
10 | var scaleListPoints = emptyList()
11 | var downY = mutableStateOf(0f)
12 | val cardPath = Path()
13 | var axisYStep = 0f
14 | var circleCoordinateX = 0f
15 | var scaleVerticalMargin = 0f
16 | var scaleBottomMargin = 0f
17 | var scaleRightMargin = 0f
18 | var radius = 0f
19 | const val countParts = 10
20 | var height = mutableStateOf(0f)
21 | var width = 0f
22 | var scaleHeight = 0f
23 | var border = 0f
24 | const val maxSum = 600
25 | const val minSum = 0
26 | const val max = maxSum - (maxSum - minSum) / countParts
27 |
28 | var position = 0
29 | fun currentSum(position: Int) = maxSum - (maxSum - minSum) / countParts * (position + 1)
--------------------------------------------------------------------------------
/features/details/src/main/kotlin/com/redmadrobot/details/presentation/view/CardDetailsView.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.details.presentation.view
2 |
3 | import android.annotation.SuppressLint
4 | import android.graphics.*
5 | import android.text.TextPaint
6 | import android.view.MotionEvent
7 | import androidx.compose.foundation.Canvas
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.MutableState
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.ExperimentalComposeUiApi
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.geometry.Offset
17 | import androidx.compose.ui.graphics.*
18 | import androidx.compose.ui.graphics.Canvas
19 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
20 | import androidx.compose.ui.input.pointer.pointerInteropFilter
21 | import androidx.compose.ui.layout.onSizeChanged
22 | import androidx.compose.ui.platform.LocalContext
23 | import androidx.compose.ui.platform.LocalDensity
24 | import androidx.compose.ui.unit.dp
25 | import androidx.compose.ui.unit.sp
26 | import androidx.core.graphics.drawable.toBitmap
27 | import com.redmadrobot.base_cards.model.CardViewState
28 | import com.redmadrobot.details.R
29 | import com.redmadrobot.details.presentation.model.*
30 | import androidx.compose.ui.graphics.Color
31 | import androidx.compose.ui.graphics.Paint
32 | import androidx.compose.ui.graphics.Path
33 | import androidx.compose.ui.graphics.PathEffect
34 |
35 | @ExperimentalComposeUiApi
36 | @SuppressLint("UseCompatLoadingForDrawables")
37 | @Composable
38 | fun CreateCustomCard(
39 | card: CardViewState
40 | ) {
41 | val imageSize = with(LocalDensity.current) { 150.dp.toPx() }.toInt()
42 | val cardIconWidth = with(LocalDensity.current) { 70.dp.toPx() }.toInt()
43 | val cardIconHeight = with(LocalDensity.current) { 40.dp.toPx() }.toInt()
44 | val circleIconWidth = with(LocalDensity.current) { 10.dp.toPx() }.toInt()
45 | val circleIconHeight = with(LocalDensity.current) { 24.dp.toPx() }.toInt()
46 | val innerMaskImageHeight = with(LocalDensity.current) { 90.dp.toPx() }.toInt()
47 | val innerMaskImageWidth = with(LocalDensity.current) { 80.dp.toPx() }.toInt()
48 | val leftMargin = with(LocalDensity.current) { 20.dp.toPx() }
49 | val imageRadius = with(LocalDensity.current) { 50.dp.toPx() }
50 | val imageCxCoordinate = with(LocalDensity.current) { 70.dp.toPx() }
51 | val imageCyCoordinate = with(LocalDensity.current) { 90.dp.toPx() }
52 | val textTopMargin = with(LocalDensity.current) { 200.dp.toPx() }
53 | val iconTopMargin = with(LocalDensity.current) { 460.dp.toPx() }
54 | val numberTextSize = with(LocalDensity.current) { 50.sp.toPx() }
55 | val scaleTextSize = with(LocalDensity.current) { 12.sp.toPx() }
56 |
57 | circleCoordinateX = with(LocalDensity.current) { 288.dp.toPx() }
58 | scaleVerticalMargin = with(LocalDensity.current) { 124.dp.toPx() }
59 | scaleBottomMargin = with(LocalDensity.current) { 62.dp.toPx() }
60 | scaleRightMargin = with(LocalDensity.current) { 10.dp.toPx() }
61 | radius = with(LocalDensity.current) { 30.dp.toPx() }
62 | border = with(LocalDensity.current) { 2.dp.toPx() }
63 |
64 | val circlePoint: MutableState = remember {
65 | mutableStateOf(Offset(0f, 0f))
66 | }
67 |
68 | val choosePart: MutableState = remember {
69 | mutableStateOf(4)
70 | }
71 |
72 | val scalePaint = Paint().apply {
73 | color = Color.Gray
74 | strokeWidth = 3f
75 | }
76 | val cardShader = LinearGradientShader(
77 | from = Offset(20f, 0f),
78 | to = Offset(20f, height.value),
79 | colors = card.cardBorderColors,
80 | )
81 |
82 | val innerCardShader = LinearGradientShader(
83 | from = Offset(20f, border),
84 | to = Offset(circleCoordinateX, height.value),
85 | colors = card.cardColors,
86 | )
87 |
88 | val innerCircleShader = LinearGradientShader(
89 | from = Offset(circlePoint.value.x - radius + border, circlePoint.value.y - radius + border),
90 | to = Offset(circlePoint.value.x + radius - border, circlePoint.value.y + radius - border),
91 | colors = card.cardColors,
92 | )
93 |
94 | val circleShader = LinearGradientShader(
95 | from = Offset(circlePoint.value.x - radius, circlePoint.value.y - radius),
96 | to = Offset(circlePoint.value.x + radius, circlePoint.value.y + radius),
97 | colors = card.cardBorderColors,
98 | )
99 |
100 | val cardPaint = Paint().apply {
101 | style = PaintingStyle.Stroke
102 | strokeWidth = border
103 | pathEffect = PathEffect.cornerPathEffect(20f)
104 | }
105 | val innerCardPaint = Paint().apply {
106 | style = PaintingStyle.Fill
107 | pathEffect = PathEffect.cornerPathEffect(20f)
108 | }
109 |
110 | val circlePaint = Paint().apply {
111 | style = PaintingStyle.Stroke
112 | strokeWidth = border
113 | }
114 | val innerCirclePaint = Paint().apply {
115 | style = PaintingStyle.Fill
116 | }
117 |
118 | val maskPaint = Paint().asFrameworkPaint().apply {
119 | isAntiAlias = true
120 | style = android.graphics.Paint.Style.FILL
121 | color = android.graphics.Color.WHITE
122 | }
123 |
124 | val typeIconPaint = Paint().asFrameworkPaint().apply {
125 | isAntiAlias = true
126 | style = android.graphics.Paint.Style.FILL
127 | }
128 |
129 | val textPaint = TextPaint().apply {
130 | isAntiAlias = true
131 | textSize = numberTextSize
132 | color = android.graphics.Color.WHITE
133 | }
134 |
135 | val textScalePaint = TextPaint().apply {
136 | isAntiAlias = true
137 | textSize = scaleTextSize
138 | textAlign = android.graphics.Paint.Align.RIGHT
139 | color = android.graphics.Color.GRAY
140 | }
141 |
142 | val sourceBitmap = LocalContext.current.getDrawable(R.drawable.ic_iron_man)
143 | ?.toBitmap(innerMaskImageWidth, innerMaskImageHeight, Bitmap.Config.ARGB_8888)
144 | val maskBitmap = Bitmap.createBitmap(imageSize, imageSize, Bitmap.Config.ARGB_8888)
145 | val resultBitmap = maskBitmap.copy(Bitmap.Config.ARGB_8888, true)
146 | val maskCanvas = android.graphics.Canvas(maskBitmap)
147 | maskCanvas.drawCircle(imageCxCoordinate, imageCyCoordinate, imageRadius, maskPaint)
148 | maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
149 | val resultCanvas = android.graphics.Canvas(resultBitmap)
150 | resultCanvas.drawBitmap(maskBitmap, 0f, 0f, null)
151 | sourceBitmap?.let { resultCanvas.drawBitmap(it, 90f, 130f, null) }
152 |
153 | val iconSourceBitmap = LocalContext.current.getDrawable(R.drawable.ic_master)
154 | ?.toBitmap(cardIconWidth, cardIconHeight, Bitmap.Config.ARGB_8888)
155 | typeIconPaint.colorFilter =
156 | PorterDuffColorFilter(card.iconColor.toArgb(), PorterDuff.Mode.SRC_IN)
157 |
158 | val circleIconBitmap = LocalContext.current.getDrawable(R.drawable.ic_circle_arrow)
159 | ?.toBitmap(circleIconWidth, circleIconHeight, Bitmap.Config.ARGB_8888)
160 |
161 | Canvas(modifier = Modifier
162 | .fillMaxWidth()
163 | .height(524.dp)
164 | .onSizeChanged {
165 | height.value = it.height.toFloat()
166 | width = it.width.toFloat()
167 | scaleHeight = height.value - scaleVerticalMargin
168 | axisYStep = scaleHeight / (countParts - 1)
169 | circlePoint.value = calculateCircleCoordinates(choosePart.value)
170 | cardPoints = calculateCardListPoints(circlePoint)
171 | additionalPoints = additionalCirclePoints()
172 | scaleListPoints = calculateScalePoints()
173 | buildCardPath(
174 | path = cardPath,
175 | )
176 | }
177 | .pointerInteropFilter { event ->
178 | when (event.action) {
179 | MotionEvent.ACTION_DOWN -> {
180 | downY.value = event.y
181 | }
182 | MotionEvent.ACTION_MOVE -> {
183 | val x = event.x
184 | val y = event.y
185 | val deltaY = downY.value - event.y
186 | if (x in (circlePoint.value.x - radius..circlePoint.value.x + radius)) {
187 | if (deltaY >= 0 && y < (circlePoint.value.y - radius) && choosePart.value >= 1) {
188 | downY.value = circlePoint.value.y
189 | val pos = choosePart.value - 1
190 | updateCoordinates(circlePoint, choosePart, pos)
191 | } else if (deltaY < 0 && y > (circlePoint.value.y + radius) && choosePart.value <= 8) {
192 | downY.value = circlePoint.value.y
193 | val pos = choosePart.value + 1
194 | updateCoordinates(circlePoint, choosePart, pos)
195 | }
196 | }
197 | }
198 | MotionEvent.ACTION_UP -> {
199 | }
200 | }
201 | true
202 | }
203 | ) {
204 |
205 | cardPaint.shader = cardShader
206 | innerCardPaint.shader = innerCardShader
207 | circlePaint.shader = circleShader
208 | innerCirclePaint.shader = innerCircleShader
209 |
210 | drawIntoCanvas { canvas ->
211 | drawCircle(
212 | canvas = canvas,
213 | outerPaint = circlePaint,
214 | innerPaint = innerCirclePaint,
215 | center = circlePoint.value,
216 | bitmap = circleIconBitmap,
217 | bitmapCenter = Offset(circleIconWidth.toFloat(), circleIconHeight.toFloat())
218 | )
219 | drawScale(
220 | canvas = canvas,
221 | scalePoints = scaleListPoints,
222 | paint = scalePaint,
223 | textPaint = textScalePaint,
224 | )
225 | drawCard(
226 | canvas = canvas,
227 | innerPaint = innerCardPaint,
228 | outerPaint = cardPaint,
229 | path = cardPath
230 | )
231 | drawImage(
232 | canvas = canvas.nativeCanvas,
233 | bitmap = resultBitmap
234 | )
235 | drawBudget(
236 | canvas = canvas.nativeCanvas,
237 | number = "$${currentSum(position)}",
238 | coordinates = Offset(leftMargin, textTopMargin),
239 | paint = textPaint
240 | )
241 | drawCardType(
242 | canvas = canvas.nativeCanvas,
243 | bitmap = iconSourceBitmap,
244 | coordinates = Offset(leftMargin, iconTopMargin),
245 | paint = typeIconPaint
246 | )
247 | }
248 | }
249 | }
250 |
251 | private fun updateCoordinates(
252 | circlePoint: MutableState,
253 | choosePart: MutableState,
254 | pos: Int
255 | ) {
256 | position = pos
257 | choosePart.value = pos
258 | circlePoint.value = calculateCircleCoordinates(pos)
259 | cardPoints = calculateCardListPoints(circlePoint)
260 | additionalPoints = additionalCirclePoints()
261 | scaleListPoints = calculateScalePoints()
262 | buildCardPath(cardPath)
263 | }
264 |
265 | private fun drawImage(
266 | canvas: android.graphics.Canvas,
267 | bitmap: Bitmap
268 | ) {
269 | canvas.drawBitmap(bitmap, 0f, 0f, null)
270 | }
271 |
272 | private fun drawCardType(
273 | canvas: android.graphics.Canvas,
274 | bitmap: Bitmap?,
275 | coordinates: Offset,
276 | paint: android.graphics.Paint
277 | ) {
278 | bitmap?.let { canvas.drawBitmap(it, coordinates.x, coordinates.y, paint) }
279 | }
280 |
281 | private fun drawBudget(
282 | canvas: android.graphics.Canvas,
283 | number: String,
284 | coordinates: Offset,
285 | paint: TextPaint
286 | ) {
287 | canvas.drawText(number, coordinates.x, coordinates.y, paint)
288 | }
289 |
290 | private fun drawCircle(
291 | canvas: Canvas,
292 | innerPaint: Paint,
293 | outerPaint: Paint,
294 | center: Offset,
295 | bitmap: Bitmap?,
296 | bitmapCenter: Offset
297 | ) {
298 | canvas.drawCircle(
299 | center = center,
300 | radius = radius - border,
301 | paint = innerPaint
302 | )
303 | canvas.drawCircle(
304 | center = center,
305 | radius = radius,
306 | paint = outerPaint
307 | )
308 | bitmap?.let {
309 | canvas.nativeCanvas.drawBitmap(
310 | it,
311 | center.x - bitmapCenter.x / 2,
312 | center.y - bitmapCenter.y / 2,
313 | null
314 | )
315 | }
316 | }
317 |
318 | private fun drawScale(canvas: Canvas, scalePoints: List, paint: Paint, textPaint: TextPaint) {
319 | val lastIndex = scalePoints.lastIndex
320 | scalePoints.forEachIndexed { index, point ->
321 | when (index) {
322 | 0 -> canvas.nativeCanvas.drawText("$"+ minSum, point.x, point.y, textPaint)
323 | lastIndex -> canvas.nativeCanvas.drawText("$"+ max.toString(), point.x, point.y, textPaint)
324 | else -> canvas.drawLine(Offset(point.x, point.y), Offset(point.x - 20f, point.y), paint)
325 | }
326 | }
327 | }
328 |
329 | private fun drawCard(canvas: Canvas, innerPaint: Paint, outerPaint: Paint, path: Path) {
330 | canvas.drawPath(path, innerPaint)
331 | canvas.drawPath(path, outerPaint)
332 | }
333 |
334 | private fun buildCardPath(path: Path) {
335 | path.reset()
336 | cardPoints.forEachIndexed { index, point ->
337 | when (index) {
338 | 0 -> path.moveTo(point.x, point.y)
339 | 3 -> {
340 | path.cubicTo(
341 | additionalPoints[0].x, additionalPoints[0].y,
342 | additionalPoints[1].x, additionalPoints[1].y,
343 | point.x, point.y,
344 | )
345 | }
346 | 4 -> {
347 | path.cubicTo(
348 | additionalPoints[2].x, additionalPoints[2].y,
349 | additionalPoints[3].x, additionalPoints[3].y,
350 | point.x, point.y + radius / 3
351 | )
352 | }
353 | else -> path.lineTo(point.x, point.y)
354 | }
355 | }
356 | path.close()
357 | }
358 |
359 | private fun calculateScalePoints(): List {
360 | val points = mutableListOf()
361 | val scaleHeight = height.value - scaleBottomMargin
362 | val coordinateX = width - scaleRightMargin
363 | for (i in 0 until countParts) {
364 | val step = scaleHeight - axisYStep * i
365 | points.add(PointF(coordinateX, step))
366 | }
367 | return points
368 | }
369 |
370 | private fun additionalCirclePoints(): List {
371 | val topCirclePoint = cardPoints[2]
372 | val middleCirclePoint = cardPoints[3]
373 | val bottomCirclePoint = cardPoints[4]
374 |
375 | val additionFirstTopCirclePoint = PointF(topCirclePoint.x, topCirclePoint.y + radius / 3)
376 | val additionSecondTopCirclePoint =
377 | PointF(middleCirclePoint.x, middleCirclePoint.y - 4 * radius / 3)
378 | val additionFirstBottomCirclePoint =
379 | PointF(middleCirclePoint.x, middleCirclePoint.y + 4 * radius / 3)
380 | val additionSecondBottomCirclePoint =
381 | PointF(bottomCirclePoint.x, bottomCirclePoint.y - radius / 3)
382 |
383 | return listOf(
384 | additionFirstTopCirclePoint,
385 | additionSecondTopCirclePoint,
386 | additionFirstBottomCirclePoint,
387 | additionSecondBottomCirclePoint
388 | )
389 | }
390 |
391 | private fun calculateCardListPoints(
392 | circlePoint: MutableState
393 | ): List {
394 | val topCirclePoint = Offset(circlePoint.value.x, circlePoint.value.y - axisYStep)
395 | val bottomCirclePoint = Offset(circlePoint.value.x, circlePoint.value.y + axisYStep)
396 | val middleCirclePoint = Offset(circlePoint.value.x - 1.3f * radius, circlePoint.value.y)
397 |
398 | return listOf(
399 | Offset(-20f, border),
400 | Offset(circlePoint.value.x, border),
401 | topCirclePoint,
402 | middleCirclePoint,
403 | bottomCirclePoint,
404 | Offset(circlePoint.value.x, height.value - border),
405 | Offset(-20f, height.value - border),
406 | )
407 | }
408 |
409 |
410 | private fun calculateCircleCoordinates(
411 | choosePart: Int,
412 | ): Offset {
413 | val y = choosePart * axisYStep + scaleBottomMargin
414 | return Offset(circleCoordinateX, y)
415 | }
416 |
--------------------------------------------------------------------------------
/features/details/src/main/res/drawable/ic_circle_arrow.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
27 |
28 |
--------------------------------------------------------------------------------
/features/details/src/main/res/drawable/ic_iron_man.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/features/home/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/features/home/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(GradlePluginId.ANDROID_LIBRARY)
3 | id(GradlePluginId.ANDROID_COMMON_CONFIG)
4 | kotlin(GradlePluginId.KAPT)
5 | id(GradlePluginId.HILT)
6 | }
7 |
8 | dependencies {
9 | implementation(project(":core:core"))
10 | implementation(project(":base:base_cards"))
11 |
12 | implementation(CoreDependency.HILT)
13 | kapt(CoreDependency.HILT_COMPILER)
14 | }
15 |
--------------------------------------------------------------------------------
/features/home/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/features/home/src/main/kotlin/com/redmadrobot/home/presentation/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.home.presentation
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.lazy.LazyColumn
6 | import androidx.compose.foundation.lazy.rememberLazyListState
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material.Button
9 | import androidx.compose.material.CircularProgressIndicator
10 | import androidx.compose.material.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.draw.clip
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.unit.dp
18 | import androidx.hilt.navigation.compose.hiltViewModel
19 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
20 | import com.redmadrobot.base_cards.model.CardViewState
21 | import com.redmadrobot.core_presentation.model.Content
22 | import com.redmadrobot.core_presentation.model.Loading
23 | import com.redmadrobot.core_presentation.model.Stub
24 | import com.redmadrobot.home.presentation.view.CardView
25 |
26 | @Composable
27 | fun HomeRoute() {
28 | val viewModel: HomeViewModel = hiltViewModel()
29 | HomeScreen(viewModel)
30 | }
31 |
32 | @Composable
33 | fun HomeScreen(viewModel: HomeViewModel) {
34 | when (val state = viewModel.viewState.collectAsStateWithLifecycle().value) {
35 | is Loading -> {
36 | Box(Modifier.fillMaxSize()) { CircularProgressIndicator(Modifier.align(Alignment.Center)) }
37 | }
38 | is Content -> {
39 | val cards: List = state.content
40 | val listState = rememberLazyListState()
41 | LazyColumn(
42 | modifier = Modifier
43 | .fillMaxSize()
44 | .padding(horizontal = 14.dp)
45 | .background(Color(0xFF434A74))
46 | .clip(RoundedCornerShape(10.dp)),
47 | reverseLayout = true,
48 | verticalArrangement = Arrangement.spacedBy((-90).dp),
49 | state = listState,
50 | ) {
51 | items(count = cards.size) { cardIndex ->
52 | CardView(
53 | cardViewState = cards[cardIndex], onCardClicked = viewModel::onCardClicked
54 | )
55 | }
56 | }
57 | LaunchedEffect(viewModel.scrollToLastPosition) {
58 | listState.scrollToItem(cards.size - 1)
59 | viewModel.onLastPositionScrolled()
60 | }
61 | }
62 | is Stub -> {
63 | Column {
64 | Text(text = "error: ${state.error}", color = Color.White)
65 | Button(onClick = viewModel::onRetryClicked) {
66 | Text("Try again", color = Color.White)
67 | }
68 | }
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/features/home/src/main/kotlin/com/redmadrobot/home/presentation/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.home.presentation
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.redmadrobot.base_cards.toCardViewState
7 | import com.redmadrobot.core.extensions.safeLaunch
8 | import com.redmadrobot.core_navigation.navigation.Router
9 | import com.redmadrobot.core_navigation.navigation.screens.Routes
10 | import com.redmadrobot.core_network.ApolloApi
11 | import com.redmadrobot.core_network.CardsListQuery
12 | import com.redmadrobot.core_presentation.extensions.update
13 | import com.redmadrobot.core_presentation.model.Content
14 | import com.redmadrobot.core_presentation.model.Loading
15 | import com.redmadrobot.core_presentation.model.Stub
16 | import com.redmadrobot.home.presentation.state.HomeViewState
17 | import dagger.hilt.android.lifecycle.HiltViewModel
18 | import kotlinx.coroutines.flow.MutableStateFlow
19 | import kotlinx.coroutines.flow.StateFlow
20 | import javax.inject.Inject
21 |
22 | @HiltViewModel
23 | class HomeViewModel @Inject constructor(
24 | private val api: ApolloApi,
25 | private val router: Router,
26 | private val savedStateHandle: SavedStateHandle
27 | ) : ViewModel() {
28 |
29 | companion object {
30 | private const val SCROLL_TO_LAST_POSITION_KEY = "SCROLL_TO_LAST_POSITION_KEY"
31 | }
32 |
33 | private val _viewState = MutableStateFlow(Loading())
34 | val viewState: StateFlow = _viewState
35 |
36 | // Fix negative arrangement: in initial state the list have a wrong position
37 | var scrollToLastPosition = savedStateHandle.get(SCROLL_TO_LAST_POSITION_KEY) ?: true
38 | private set
39 |
40 | init {
41 | loadCards()
42 | }
43 |
44 | fun onRetryClicked() = loadCards()
45 |
46 | fun onLastPositionScrolled() {
47 | savedStateHandle.set(SCROLL_TO_LAST_POSITION_KEY, false)
48 | scrollToLastPosition = false
49 | }
50 |
51 | private fun loadCards() {
52 | _viewState.update { Loading() }
53 | viewModelScope.safeLaunch(
54 | {
55 | val cards = api.query(CardsListQuery()).cards
56 | _viewState.update { Content(cards.map(CardsListQuery.Card::toCardViewState)) }
57 | },
58 | onError = { throwable ->
59 | _viewState.update { Stub(throwable) }
60 | }
61 | )
62 | }
63 |
64 | fun onCardClicked(id: String) {
65 | router.navigate(Routes.toDetails(id))
66 | }
67 | }
--------------------------------------------------------------------------------
/features/home/src/main/kotlin/com/redmadrobot/home/presentation/state/HomeViewState.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.home.presentation.state
2 |
3 | import com.redmadrobot.core_presentation.model.State
4 | import com.redmadrobot.base_cards.model.CardViewState
5 |
6 | typealias HomeViewState = State>
--------------------------------------------------------------------------------
/features/home/src/main/kotlin/com/redmadrobot/home/presentation/view/CardView.kt:
--------------------------------------------------------------------------------
1 | package com.redmadrobot.home.presentation.view
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.foundation.BorderStroke
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.border
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.Icon
11 | import androidx.compose.material.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.draw.clip
16 | import androidx.compose.ui.draw.drawBehind
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.graphics.Paint
19 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
20 | import androidx.compose.ui.graphics.nativeCanvas
21 | import androidx.compose.ui.res.painterResource
22 | import androidx.compose.ui.unit.Dp
23 | import androidx.compose.ui.unit.dp
24 | import com.redmadrobot.base_cards.model.CardViewState
25 | import com.redmadrobot.home.R
26 |
27 | @SuppressLint("UnrememberedMutableState")
28 | @Composable
29 | fun CardView(cardViewState: CardViewState, onCardClicked: (String) -> Unit) {
30 | val height = 200.dp
31 | Column(
32 | Modifier
33 | .padding(horizontal = 10.dp, vertical = 10.dp)
34 | .drawColoredShadow(
35 | Color.White,
36 | darkColor = Color.Black,
37 | borderRadius = 10.dp,
38 | offsetY = 0.dp,
39 | offsetX = 0.dp,
40 | offsetDarkY = 10.dp
41 | )
42 | .clickable {
43 | onCardClicked(cardViewState.id)
44 | }
45 | .fillMaxWidth()
46 | .height(height)
47 | .clip(RoundedCornerShape(10.dp))
48 | .background(cardViewState.cardGradient)
49 | .border(BorderStroke(1.dp, cardViewState.borderGradient), RoundedCornerShape(10.dp))
50 | ) {
51 | Icon(
52 | modifier = Modifier
53 | .padding(30.dp),
54 | painter = painterResource(id = R.drawable.ic_subtract),
55 | contentDescription = "substract",
56 | tint = cardViewState.iconColor,
57 | )
58 | Spacer(modifier = Modifier.height(40.dp))
59 | Row(
60 | Modifier
61 | .padding(horizontal = 30.dp)
62 | .fillMaxWidth(),
63 | horizontalArrangement = Arrangement.SpaceBetween
64 | ) {
65 | Text(
66 | modifier = Modifier
67 | .align(Alignment.Bottom)
68 | .drawTextColoredShadow(
69 | shadowColor = Color.White,
70 | textColor = cardViewState.iconColor,
71 | text = cardViewState.number
72 | ), color = Color.Black, text = ""
73 | )
74 | Icon(
75 | modifier = Modifier.align(Alignment.Bottom),
76 | painter = painterResource(id = cardViewState.iconRes),
77 | contentDescription = "type",
78 | tint = cardViewState.iconColor
79 | )
80 | }
81 | }
82 | }
83 |
84 | private fun Modifier.drawTextColoredShadow(
85 | shadowColor: Color,
86 | textColor: Color,
87 | alpha: Float = 0.8f,
88 | shadowRadius: Float = 5f,
89 | offsetY: Float = 3f,
90 | offsetX: Float = 3f,
91 | text: String
92 | ) = this.drawBehind {
93 | val shadowColorArgb =
94 | android.graphics.Color.toArgb(shadowColor.copy(alpha = alpha).value.toLong())
95 | val textColorArgb = android.graphics.Color.toArgb(textColor.value.toLong())
96 |
97 | drawIntoCanvas {
98 | val paint = Paint()
99 | val frameworkPaint = paint.asFrameworkPaint()
100 | frameworkPaint.isAntiAlias = true
101 | frameworkPaint.color = textColorArgb
102 | frameworkPaint.textSize = 50f
103 | frameworkPaint.setShadowLayer(
104 | shadowRadius,
105 | offsetX,
106 | offsetY,
107 | shadowColorArgb
108 | )
109 | it.nativeCanvas.drawText(text, 0f, this.size.height, frameworkPaint)
110 | }
111 | }
112 |
113 | private fun Modifier.drawColoredShadow(
114 | color: Color,
115 | darkColor: Color,
116 | alpha: Float = 0.2f,
117 | borderRadius: Dp = 20.dp,
118 | shadowRadius: Dp = 10.dp,
119 | offsetY: Dp = 0.dp,
120 | offsetX: Dp = 0.dp,
121 | offsetDarkY: Dp = 0.dp,
122 | offsetDarkX: Dp = 0.dp
123 | ) = this.drawBehind {
124 | val transparentColor = android.graphics.Color.toArgb(color.copy(alpha = 0.0f).value.toLong())
125 |
126 | val shadowColor = android.graphics.Color.toArgb(color.copy(alpha = alpha).value.toLong())
127 | val shadowDarkColor =
128 | android.graphics.Color.toArgb(darkColor.copy(alpha = alpha).value.toLong())
129 | this.drawIntoCanvas {
130 | val paint = Paint()
131 | val frameworkPaint = paint.asFrameworkPaint()
132 | frameworkPaint.color = transparentColor
133 | frameworkPaint.setShadowLayer(
134 | shadowRadius.toPx(),
135 | offsetX.toPx(),
136 | offsetY.toPx(),
137 | shadowColor
138 | )
139 | it.drawRoundRect(
140 | 0f,
141 | 0f,
142 | this.size.width - 2 * shadowRadius.value,
143 | this.size.height - 2 * shadowRadius.value,
144 | borderRadius.toPx(),
145 | borderRadius.toPx(),
146 | paint
147 | )
148 | val darkPaint = Paint()
149 | val frameworkDarkPaint = darkPaint.asFrameworkPaint()
150 | frameworkDarkPaint.setShadowLayer(
151 | shadowRadius.toPx(),
152 | offsetDarkX.toPx(),
153 | offsetDarkY.toPx(),
154 | shadowDarkColor
155 | )
156 | it.drawRoundRect(
157 | 2 * shadowRadius.value,
158 | 0f,
159 | this.size.width - 2 * shadowRadius.value,
160 | this.size.height,
161 | borderRadius.toPx(),
162 | borderRadius.toPx(),
163 | darkPaint
164 | )
165 | }
166 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # -------Gradle--------
2 | org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
3 | org.gradle.configureondemand=true
4 | org.gradle.daemon=true
5 | org.gradle.parallel=true
6 | org.gradle.caching=true
7 |
8 | # -------Android-------
9 | android.useAndroidX=true
10 |
11 | # -------Kotlin--------
12 | kotlin.code.style=official
13 |
14 | # -------Kapt----------
15 | kapt.incremental.apt=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/android-mad-showcase/9e90bf0432eb9170ce7e73e0fa779ea4e7f8cd6b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Aug 04 11:21:41 SAMT 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = ("android-mad-showcase")
2 |
3 | include(":app")
4 |
5 | include(
6 | ":core:core",
7 | ":core:core_navigation",
8 | ":core:core_network",
9 | ":core:core_presentation"
10 | )
11 |
12 | include(
13 | "base:base_cards"
14 | )
15 |
16 | include(
17 | ":features:auth",
18 | ":features:home",
19 | ":features:details"
20 | )
21 |
--------------------------------------------------------------------------------