├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
└── misc.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── christopherelias
│ │ └── blockchain
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── christopherelias
│ │ │ └── blockchain
│ │ │ ├── BlockChainApp.kt
│ │ │ ├── di
│ │ │ ├── CoroutinesModule.kt
│ │ │ ├── DispatcherQualifier.kt
│ │ │ ├── MiddlewareModule.kt
│ │ │ ├── RetrofitModule.kt
│ │ │ └── UtilsModule.kt
│ │ │ ├── features
│ │ │ └── home
│ │ │ │ ├── data
│ │ │ │ ├── data_source
│ │ │ │ │ └── HomeRemoteDataSource.kt
│ │ │ │ └── repository
│ │ │ │ │ └── HomeRepositoryImpl.kt
│ │ │ │ ├── data_source
│ │ │ │ ├── model
│ │ │ │ │ └── TransactionPerSecondResponse.kt
│ │ │ │ └── remote
│ │ │ │ │ ├── HomeRemoteDataSourceImpl.kt
│ │ │ │ │ └── HomeService.kt
│ │ │ │ ├── di
│ │ │ │ ├── HomeFeatureModule.kt
│ │ │ │ └── HomeNetworkModule.kt
│ │ │ │ ├── domain
│ │ │ │ └── repository
│ │ │ │ │ └── HomeRepository.kt
│ │ │ │ └── mapper
│ │ │ │ ├── TransactionMapper.kt
│ │ │ │ └── TransactionMapperImpl.kt
│ │ │ ├── middlewares
│ │ │ ├── ConnectivityMiddleware.kt
│ │ │ └── MiddlewareProviderImpl.kt
│ │ │ ├── ui
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── components
│ │ │ │ ├── LinearChart.kt
│ │ │ │ └── StatsCards.kt
│ │ │ ├── models
│ │ │ │ ├── Stats.kt
│ │ │ │ └── TransactionRate.kt
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── utils_impl
│ │ │ ├── connectivity
│ │ │ └── ConnectivityUtilsImpl.kt
│ │ │ └── resource_provider
│ │ │ └── ResourceProviderImpl.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ ├── java
│ └── com
│ │ └── christopherelias
│ │ └── blockchain
│ │ ├── HomeRepositoryUnitTest.kt
│ │ ├── TransactionMapperUnitTest.kt
│ │ ├── TransactionRetrofitServiceUnitTest.kt
│ │ └── utils
│ │ ├── DefaultTestNetworkMiddleware.kt
│ │ ├── EitherTestException.kt
│ │ ├── EitherTestExtensions.kt
│ │ ├── FileReaderUtil.kt
│ │ └── TransactionsData.kt
│ └── resources
│ └── transactions_response.json
├── art
├── blockchain_app_architecture_diagram.png
└── screenshot.jpg
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── com
│ └── christopherelias
│ └── blockchain
│ └── buildsrc
│ └── dependencies.kt
├── common-android-library.gradle
├── common-kotlin-library.gradle
├── core
├── functional-programming
│ ├── build.gradle
│ └── src
│ │ ├── main
│ │ └── java
│ │ │ └── com
│ │ │ └── christopherelias
│ │ │ └── blockchain
│ │ │ └── functional_programming
│ │ │ ├── Either.kt
│ │ │ ├── Failure.kt
│ │ │ └── utils
│ │ │ └── Extensions.kt
│ │ └── test
│ │ └── java
│ │ └── com
│ │ └── christopherelias
│ │ └── blockchain
│ │ └── functional_programming
│ │ └── EitherUnitTest.kt
└── network
│ ├── .gitignore
│ ├── build.gradle
│ └── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── christopherelias
│ │ └── blockchain
│ │ └── core
│ │ └── network
│ │ ├── middleware
│ │ ├── NetworkMiddleware.kt
│ │ ├── NetworkMiddlewareFailure.kt
│ │ └── provider
│ │ │ └── MiddlewareProvider.kt
│ │ ├── models
│ │ ├── ResponseError.kt
│ │ └── ResponseList.kt
│ │ └── utils
│ │ └── Extensions.kt
│ └── test
│ └── java
│ └── com
│ └── christopherelias
│ └── blockchain
│ └── core
│ └── network
│ ├── DumbMiddleware.kt
│ └── NetworkCallExtensionUnitTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── utils
├── build.gradle
└── src
├── androidTest
└── java
│ └── com
│ └── christopherelias
│ └── blockchain
│ └── utils
│ └── ExampleInstrumentedTest.kt
├── main
├── AndroidManifest.xml
└── java
│ └── com
│ └── christopherelias
│ └── blockchain
│ └── utils
│ ├── OneTimeEvent.kt
│ ├── connectivity
│ └── ConnectivityUtils.kt
│ └── resource_provider
│ └── ResourceProvider.kt
└── test
└── java
└── com
└── christopherelias
└── blockchain
└── utils
└── ExampleUnitTest.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | # Android Studio 3 in .gitignore file.
48 | .idea/caches
49 | .idea/modules.xml
50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
51 | .idea/navEditor.xml
52 |
53 | # Keystore files
54 | # Uncomment the following lines if you do not want to check your keystore files in.
55 | #*.jks
56 | #*.keystore
57 |
58 | # External native build folder generated in Android Studio 2.2 and later
59 | .externalNativeBuild
60 | .cxx/
61 |
62 | # Google Services (e.g. APIs or Firebase)
63 | # google-services.json
64 |
65 | # Freeline
66 | freeline.py
67 | freeline/
68 | freeline_project_description.json
69 |
70 | # fastlane
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots
74 | fastlane/test_output
75 | fastlane/readme.md
76 |
77 | # Version control
78 | vcs.xml
79 |
80 | # lint
81 | lint/intermediates/
82 | lint/generated/
83 | lint/outputs/
84 | lint/tmp/
85 | # lint/reports/
86 |
87 | # Android Profiling
88 | *.hprof
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blockchain App
2 |
3 | This is an application that consumes the API of [Blockchain](https://www.blockchain.com/api/charts_api) and display a linear chart of the bitcoin transactions per second on a given range & other stats.
4 | The graph is made with [whimsical](https://whimsical.com).
5 |
6 | 
7 |
8 | ## Tech Stack
9 |
10 | This project is monolithic(for now) and has only one screen. However, all inner packages are well organized in order to be scalable and modularized in the following days. It uses MVVM as Software Design Patter and is using Jetpack Compose for the UI.
11 |
12 | - :core:functional-programming contains the Either sealed class & some helper methods. Either is the "wrapper" for handle Either an Error or a Success structure.
13 | - :core:network contains some middlewares & extensions for execute safe retrofit calls (I write 3 articles about it -> Create a safe retrofit calls extension part [I](https://christopher-elias.medium.com/safe-retrofit-calls-extension-with-kotlin-coroutines-for-android-in-2021-part-i-d47e9e2962ad), [II](https://christopher-elias.medium.com/safe-retrofit-calls-extension-with-kotlin-coroutines-for-android-in-2021-part-ii-fd55842951cf), & [III](https://christopher-elias.medium.com/safe-retrofit-calls-extension-with-kotlin-coroutines-for-android-in-2021-part-iii-583249b0e86b))
14 | - :core:network/middlewares middlewares are going to act as a firewall before executing any retrofit call. Only if all the middlewares are supplied then the retrofit call is allowed to be executed.
15 | - :utils has utilities interfaces like a ResourceProvider & ConnectivityUtils, who's implementations will be in the app module.
16 | - :features:home Home is our only feature (for now), it has it's data sources, mappers & repositories interfaces for prepare the data and send it to the HomeViewModel.
17 | - :ui:components: contains our composables.
18 | - :tests unit tests for repository, datasources, mappers, etc. I also write articles about this. [Unit Tests](https://proandroiddev.com/understanding-unit-tests-for-android-in-2021-71984f370240) & [Instrumented Tests](https://proandroiddev.com/easy-instrumented-tests-ui-tests-for-android-in-2021-2e28134ff309)
19 | - :app module contains our HiltApplication in charge to create the app DI graph and the interface implementations from our libraries.
20 |
21 | ## App Screenshot v1.0.0
22 | 
23 |
24 | ## Development setup
25 |
26 | You require at least [Android Studio Arctic Fox](https://developer.android.com/studio/releases#arctic-fox) | 2020.3.1 Build #AI-203.7717.56.2031.7583922, built on July 26, 2021 for run this project. No API Keys required.
27 |
28 | ## Libraries
29 |
30 | - Application entirely written in [Kotlin](https://kotlinlang.org)
31 | - Asynchronous processing using [Coroutines](https://kotlin.github.io/kotlinx.coroutines/)
32 | - Uses [Dagger-Hilt](https://developer.android.com/training/dependency-injection/hilt-android) for dependency Injection
33 | - Uses [Jetpack Compose](https://developer.android.com/jetpack/compose) for latest declarative UI features
34 | - Uses [mockk](https://github.com/mockk/mockk) for mocking objects, interfaces & more.
35 | - Uses [JUnit4](https://junit.org/junit4/) for unit tests assertions & more.
36 |
37 |
38 | ## 📃 License
39 |
40 | ```
41 | Copyright 2021 Christopher Elias
42 |
43 | Licensed under the Apache License, Version 2.0 (the "License");
44 | you may not use this file except in compliance with the License.
45 | You may obtain a copy of the License at
46 |
47 | http://www.apache.org/licenses/LICENSE-2.0
48 |
49 | Unless required by applicable law or agreed to in writing, software
50 | distributed under the License is distributed on an "AS IS" BASIS,
51 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
52 | See the License for the specific language governing permissions and
53 | limitations under the License.
54 | ```
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | import com.christopherelias.blockchain.buildsrc.Libs
2 | import com.christopherelias.blockchain.buildsrc.DefaultConfig
3 | import com.christopherelias.blockchain.buildsrc.Releases
4 | import com.christopherelias.blockchain.buildsrc.Modules
5 | import com.christopherelias.blockchain.buildsrc.Core
6 |
7 | plugins {
8 | id 'com.android.application'
9 | id 'kotlin-android'
10 | id 'kotlin-kapt'
11 | id 'dagger.hilt.android.plugin'
12 | }
13 |
14 | android {
15 | compileSdk DefaultConfig.compileSdk
16 | buildToolsVersion(DefaultConfig.buildToolsVersion)
17 |
18 | defaultConfig {
19 | applicationId DefaultConfig.appId
20 | minSdk DefaultConfig.minSdk
21 | targetSdk DefaultConfig.targetSdk
22 | versionCode Releases.versionCode
23 | versionName Releases.versionName
24 |
25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
26 | vectorDrawables {
27 | useSupportLibrary true
28 | }
29 | }
30 |
31 | buildTypes {
32 | release {
33 | minifyEnabled false
34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
35 | }
36 | }
37 | compileOptions {
38 | sourceCompatibility JavaVersion.VERSION_1_8
39 | targetCompatibility JavaVersion.VERSION_1_8
40 | }
41 | kotlinOptions {
42 | jvmTarget = '1.8'
43 | useIR = true
44 | }
45 | buildFeatures {
46 | compose true
47 | }
48 | composeOptions {
49 | kotlinCompilerExtensionVersion Libs.AndroidX.Compose.version
50 | }
51 | }
52 |
53 | dependencies {
54 |
55 | implementation project(Core.functionalProgramming)
56 | implementation project(Core.network)
57 | implementation project(Modules.utils)
58 |
59 | implementation Libs.AndroidX.coreKtx
60 | implementation Libs.AndroidX.appCompat
61 | implementation Libs.googleMaterial
62 |
63 | // Lifecycle
64 | implementation Libs.AndroidX.Lifecycle.viewModelKtx
65 | implementation Libs.AndroidX.Lifecycle.runtimeKtx
66 |
67 | // Compose
68 | implementation Libs.AndroidX.Compose.ui
69 | implementation Libs.AndroidX.Compose.tooling
70 | implementation Libs.AndroidX.Compose.material
71 | implementation Libs.AndroidX.Compose.runtimeLivedata
72 | implementation Libs.AndroidX.Compose.activity
73 |
74 | // Kotlin Coroutines
75 | implementation Libs.Coroutines.core
76 | implementation Libs.Coroutines.android
77 | testImplementation Libs.Coroutines.test
78 |
79 | // Timber
80 | implementation Libs.timber
81 |
82 | // Retrofit
83 | implementation Libs.Square.retrofit
84 | implementation Libs.Square.moshi
85 | implementation Libs.Square.loggingInterceptor
86 | testImplementation Libs.Square.Test.mockWerbServer
87 |
88 | // DaggerHilt
89 | implementation Libs.Hilt.android
90 | kapt Libs.Hilt.kapt
91 |
92 | // Mockk
93 | testImplementation Libs.mockkTesting
94 | testImplementation Libs.jUnit4Testing
95 | androidTestImplementation Libs.AndroidX.Test.Ext.junit
96 | androidTestImplementation Libs.AndroidX.Test.espressoCore
97 | androidTestImplementation Libs.AndroidX.Compose.Test.junit
98 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/christopherelias/blockchain/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.christopherelias.blockchain", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/BlockChainApp.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 | import timber.log.Timber
6 |
7 | /*
8 | * Created by Christopher Elias on 9/06/2021
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Lima, Peru.
12 | */
13 |
14 | @HiltAndroidApp
15 | class BlockChainApp : Application() {
16 |
17 | override fun onCreate() {
18 | super.onCreate()
19 | if (BuildConfig.DEBUG) {
20 | Timber.plant(Timber.DebugTree())
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/di/CoroutinesModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.Dispatchers
9 |
10 | /*
11 | * Created by Christopher Elias on 9/06/2021
12 | * christopher.mike.96@gmail.com
13 | *
14 | * Lima, Peru.
15 | */
16 |
17 | @Module
18 | @InstallIn(SingletonComponent::class)
19 | object CoroutinesModule {
20 |
21 | @DefaultDispatcher
22 | @Provides
23 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
24 |
25 | @IoDispatcher
26 | @Provides
27 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
28 |
29 | @MainDispatcher
30 | @Provides
31 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/di/DispatcherQualifier.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | /*
6 | * Created by Christopher Elias on 9/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 |
13 | @Retention(AnnotationRetention.BINARY)
14 | @Qualifier
15 | annotation class DefaultDispatcher
16 |
17 | @Retention(AnnotationRetention.BINARY)
18 | @Qualifier
19 | annotation class IoDispatcher
20 |
21 | @Retention(AnnotationRetention.BINARY)
22 | @Qualifier
23 | annotation class MainDispatcher
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/di/MiddlewareModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.di
2 |
3 | import com.christopherelias.blockchain.core.network.middleware.provider.MiddlewareProvider
4 | import com.christopherelias.blockchain.middlewares.MiddlewareProviderImpl
5 | import com.christopherelias.blockchain.middlewares.ConnectivityMiddleware
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 | import javax.inject.Singleton
11 |
12 | /*
13 | * Created by Christopher Elias on 9/06/2021
14 | * christopher.mike.96@gmail.com
15 | *
16 | * Lima, Peru.
17 | */
18 |
19 | @Module
20 | @InstallIn(SingletonComponent::class)
21 | object MiddlewareModule {
22 |
23 | @Provides
24 | @Singleton
25 | fun provideMiddlewares(
26 | connectivityMiddleware: ConnectivityMiddleware
27 | ): MiddlewareProvider {
28 | return MiddlewareProviderImpl.Builder()
29 | .add(connectivityMiddleware)
30 | .build()
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/di/RetrofitModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.di
2 |
3 | import com.christopherelias.blockchain.BuildConfig
4 | import com.christopherelias.blockchain.core.network.models.ResponseError
5 | import com.squareup.moshi.JsonAdapter
6 | import com.squareup.moshi.Moshi
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import okhttp3.OkHttpClient
12 | import okhttp3.logging.HttpLoggingInterceptor
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.moshi.MoshiConverterFactory
15 | import java.util.concurrent.TimeUnit
16 | import javax.inject.Inject
17 | import javax.inject.Singleton
18 |
19 | /*
20 | * Created by Christopher Elias on 9/06/2021
21 | * christopher.mike.96@gmail.com
22 | *
23 | * Lima, Peru.
24 | */
25 |
26 | @Module
27 | @InstallIn(SingletonComponent::class)
28 | object RetrofitModule {
29 |
30 | @Provides
31 | @Singleton
32 | internal fun provideOkHttpBuilder(
33 | httpClientFactory: HttpClientFactory
34 | ): OkHttpClient.Builder {
35 | return httpClientFactory.create()
36 | }
37 |
38 | @Provides
39 | @Singleton
40 | internal fun provideClient(
41 | clientBuilder: OkHttpClient.Builder
42 | ): OkHttpClient {
43 | clientBuilder
44 | .connectTimeout(30, TimeUnit.SECONDS)
45 | .readTimeout(30, TimeUnit.SECONDS)
46 | if (BuildConfig.DEBUG) {
47 | val loggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT)
48 | loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
49 | clientBuilder.addInterceptor(loggingInterceptor)
50 | }
51 |
52 | return clientBuilder.build()
53 | }
54 |
55 |
56 | // TODO : Add BASE_URL in BuildConfig and change this...
57 | @Provides
58 | @Singleton
59 | fun provideRetrofitBuilder(
60 | okHttpClient: OkHttpClient
61 | ): Retrofit {
62 | return Retrofit.Builder()
63 | .baseUrl("https://api.blockchain.info/")
64 | .client(okHttpClient)
65 | .addConverterFactory(MoshiConverterFactory.create())
66 | .build()
67 | }
68 |
69 | @Provides
70 | @Singleton
71 | internal fun provideMoshi(): Moshi {
72 | return Moshi.Builder().build()
73 | }
74 |
75 | @Provides
76 | fun provideJsonErrorAdapter(moshi: Moshi): JsonAdapter {
77 | return moshi.adapter(ResponseError::class.java)
78 | }
79 | }
80 |
81 | internal class HttpClientFactory @Inject constructor() {
82 |
83 | private val httpClient by lazy { OkHttpClient() }
84 |
85 | fun create(): OkHttpClient.Builder {
86 | return httpClient.newBuilder()
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/di/UtilsModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.di
2 |
3 | import com.christopherelias.blockchain.utils.connectivity.ConnectivityUtils
4 | import com.christopherelias.blockchain.utils.resource_provider.ResourceProvider
5 | import com.christopherelias.blockchain.utils_impl.connectivity.ConnectivityUtilsImpl
6 | import com.christopherelias.blockchain.utils_impl.resource_provider.ResourceProviderImpl
7 | import dagger.Binds
8 | import dagger.Module
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import javax.inject.Singleton
12 |
13 | /*
14 | * Created by Christopher Elias on 9/06/2021
15 | * christopher.mike.96@gmail.com
16 | *
17 | * Lima, Peru.
18 | */
19 |
20 | @Module
21 | @InstallIn(SingletonComponent::class)
22 | abstract class UtilsModule {
23 |
24 | @Binds
25 | @Singleton
26 | abstract fun provideConnectivityUtils(
27 | connectivityUtilsImpl: ConnectivityUtilsImpl
28 | ): ConnectivityUtils
29 |
30 | @Binds
31 | @Singleton
32 | abstract fun provideResourceProviderUtils(
33 | resourceProviderImpl: ResourceProviderImpl
34 | ): ResourceProvider
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/data/data_source/HomeRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.data.data_source
2 |
3 | import com.christopherelias.blockchain.functional_programming.Either
4 | import com.christopherelias.blockchain.functional_programming.Failure
5 | import com.christopherelias.blockchain.features.home.data_source.model.TransactionPerSecondResponse
6 |
7 | /*
8 | * Created by Christopher Elias on 9/06/2021
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Lima, Peru.
12 | */
13 |
14 | interface HomeRemoteDataSource {
15 |
16 | suspend fun getTransactionsPerSecond(): Either>
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/data/repository/HomeRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.data.repository
2 |
3 | import com.christopherelias.blockchain.functional_programming.Either
4 | import com.christopherelias.blockchain.functional_programming.Failure
5 | import com.christopherelias.blockchain.features.home.data.data_source.HomeRemoteDataSource
6 | import com.christopherelias.blockchain.features.home.domain.repository.HomeRepository
7 | import com.christopherelias.blockchain.features.home.mapper.TransactionMapper
8 | import com.christopherelias.blockchain.ui.models.TransactionsPerSecond
9 | import javax.inject.Inject
10 |
11 | /*
12 | * Created by Christopher Elias on 9/06/2021
13 | * christopher.mike.96@gmail.com
14 | *
15 | * Lima, Peru.
16 | */
17 |
18 | class HomeRepositoryImpl @Inject constructor(
19 | private val mapper: TransactionMapper,
20 | private val homeRemoteDataSource: HomeRemoteDataSource
21 | ) : HomeRepository {
22 |
23 | override suspend fun getTransactionsPerSecond(): Either {
24 | return homeRemoteDataSource.getTransactionsPerSecond()
25 | .coMapSuccess { response -> mapper.mapRemoteTransactionsToUi(response) }
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/data_source/model/TransactionPerSecondResponse.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.data_source.model
2 |
3 | import com.squareup.moshi.Json
4 |
5 | /*
6 | * Created by Christopher Elias on 9/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | data class TransactionPerSecondResponse(
13 | @field:Json(name = "x") val timeStamp: Long,
14 | @field:Json(name = "y") val transactionsPerSecondValue: Double
15 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/data_source/remote/HomeRemoteDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.data_source.remote
2 |
3 | import com.christopherelias.blockchain.functional_programming.Either
4 | import com.christopherelias.blockchain.functional_programming.Failure
5 | import com.christopherelias.blockchain.core.network.middleware.provider.MiddlewareProvider
6 | import com.christopherelias.blockchain.core.network.models.ResponseError
7 | import com.christopherelias.blockchain.core.network.utils.call
8 | import com.christopherelias.blockchain.di.IoDispatcher
9 | import com.christopherelias.blockchain.features.home.data.data_source.HomeRemoteDataSource
10 | import com.christopherelias.blockchain.features.home.data_source.model.TransactionPerSecondResponse
11 | import com.squareup.moshi.JsonAdapter
12 | import kotlinx.coroutines.CoroutineDispatcher
13 | import javax.inject.Inject
14 |
15 | /*
16 | * Created by Christopher Elias on 9/06/2021
17 | * christopher.mike.96@gmail.com
18 | *
19 | * Lima, Peru.
20 | */
21 |
22 | class HomeRemoteDataSourceImpl @Inject constructor(
23 | private val middlewareProvider: MiddlewareProvider,
24 | @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
25 | private val errorAdapter: JsonAdapter,
26 | private val homeService: HomeService
27 | ) : HomeRemoteDataSource {
28 |
29 | override suspend fun getTransactionsPerSecond(): Either> {
30 | return call(
31 | middleWares = middlewareProvider.getAll(),
32 | ioDispatcher = ioDispatcher,
33 | adapter = errorAdapter,
34 | retrofitCall = {
35 | homeService.getTransactionsPerSecond(
36 | chartName = "transactions-per-second",
37 | timeSpan = "5weeks",
38 | rollingAverage = "8hours"
39 | )
40 | }
41 | ).let { response -> response.mapSuccess { it.items } }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/data_source/remote/HomeService.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.data_source.remote
2 |
3 | import com.christopherelias.blockchain.core.network.models.ResponseList
4 | import com.christopherelias.blockchain.features.home.data_source.model.TransactionPerSecondResponse
5 | import retrofit2.http.GET
6 | import retrofit2.http.Path
7 | import retrofit2.http.Query
8 |
9 | /*
10 | * Created by Christopher Elias on 9/06/2021
11 | * christopher.mike.96@gmail.com
12 | *
13 | * Lima, Peru.
14 | */
15 |
16 | interface HomeService {
17 |
18 | @GET("charts/{chartName}")
19 | suspend fun getTransactionsPerSecond(
20 | @Path("chartName") chartName: String,
21 | @Query("timespan") timeSpan: String,
22 | @Query("rollingAverage") rollingAverage: String
23 | ): ResponseList
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/di/HomeFeatureModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.di
2 |
3 | import com.christopherelias.blockchain.features.home.data.data_source.HomeRemoteDataSource
4 | import com.christopherelias.blockchain.features.home.data.repository.HomeRepositoryImpl
5 | import com.christopherelias.blockchain.features.home.data_source.remote.HomeRemoteDataSourceImpl
6 | import com.christopherelias.blockchain.features.home.domain.repository.HomeRepository
7 | import com.christopherelias.blockchain.features.home.mapper.TransactionMapper
8 | import com.christopherelias.blockchain.features.home.mapper.TransactionMapperImpl
9 | import dagger.Binds
10 | import dagger.Module
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.android.components.ViewModelComponent
13 |
14 | /*
15 | * Created by Christopher Elias on 9/06/2021
16 | * christopher.mike.96@gmail.com
17 | *
18 | * Lima, Peru.
19 | */
20 |
21 | @Module(includes = [HomeNetworkModule::class])
22 | @InstallIn(ViewModelComponent::class)
23 | abstract class HomeFeatureModule {
24 |
25 | @Binds
26 | internal abstract fun provideHomeRemoteDataSource(
27 | homeRemoteDataSourceImpl: HomeRemoteDataSourceImpl
28 | ): HomeRemoteDataSource
29 |
30 | @Binds
31 | internal abstract fun provideTransactionMapper(
32 | transactionMapperImpl: TransactionMapperImpl
33 | ): TransactionMapper
34 |
35 | @Binds
36 | internal abstract fun provideHomeRepository(
37 | homeRepositoryImpl: HomeRepositoryImpl
38 | ): HomeRepository
39 |
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/di/HomeNetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.di
2 |
3 | import com.christopherelias.blockchain.features.home.data_source.remote.HomeService
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.components.ViewModelComponent
8 | import retrofit2.Retrofit
9 |
10 | /*
11 | * Created by Christopher Elias on 9/06/2021
12 | * christopher.mike.96@gmail.com
13 | *
14 | * Lima, Peru.
15 | */
16 |
17 | @Module
18 | @InstallIn(ViewModelComponent::class)
19 | internal class HomeNetworkModule {
20 |
21 | @Provides
22 | internal fun provideTransactionService(
23 | retrofit: Retrofit
24 | ): HomeService = retrofit.create(HomeService::class.java)
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/domain/repository/HomeRepository.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.domain.repository
2 |
3 | import com.christopherelias.blockchain.functional_programming.Either
4 | import com.christopherelias.blockchain.functional_programming.Failure
5 | import com.christopherelias.blockchain.ui.models.TransactionsPerSecond
6 |
7 | /*
8 | * Created by Christopher Elias on 9/06/2021
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Lima, Peru.
12 | */
13 |
14 | interface HomeRepository {
15 | suspend fun getTransactionsPerSecond(): Either
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/mapper/TransactionMapper.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.mapper
2 |
3 | import com.christopherelias.blockchain.features.home.data_source.model.TransactionPerSecondResponse
4 | import com.christopherelias.blockchain.ui.models.TransactionsPerSecond
5 |
6 |
7 | /*
8 | * Created by Christopher Elias on 9/06/2021
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Lima, Peru.
12 | */
13 |
14 | interface TransactionMapper {
15 |
16 | suspend fun mapRemoteTransactionsToUi(
17 | transactionsRemote: List
18 | ): TransactionsPerSecond
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/features/home/mapper/TransactionMapperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.features.home.mapper
2 |
3 | import com.christopherelias.blockchain.di.DefaultDispatcher
4 | import com.christopherelias.blockchain.features.home.data_source.model.TransactionPerSecondResponse
5 | import com.christopherelias.blockchain.ui.models.TransactionRate
6 | import com.christopherelias.blockchain.ui.models.TransactionsPerSecond
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.withContext
9 | import javax.inject.Inject
10 |
11 | /*
12 | * Created by Christopher Elias on 9/06/2021
13 | * christopher.mike.96@gmail.com
14 | *
15 | * Lima, Peru.
16 | */
17 |
18 | class TransactionMapperImpl @Inject constructor(
19 | @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher
20 | ) : TransactionMapper {
21 |
22 | override suspend fun mapRemoteTransactionsToUi(
23 | transactionsRemote: List
24 | ): TransactionsPerSecond {
25 | return withContext(defaultDispatcher) {
26 |
27 | var maxRateValue = 0.0
28 |
29 | val uiTransactions = transactionsRemote.mapIndexed { index, remoteTransaction ->
30 | // Find maximum rate value
31 | if (transactionsRemote[index].transactionsPerSecondValue >= maxRateValue) {
32 | maxRateValue = transactionsRemote[index].transactionsPerSecondValue
33 | }
34 |
35 | // Map items
36 | TransactionRate(
37 | timeStamp = remoteTransaction.timeStamp,
38 | transactionsPerSecondValue = remoteTransaction.transactionsPerSecondValue
39 | )
40 | }
41 |
42 | return@withContext TransactionsPerSecond(maxRateValue, uiTransactions)
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/middlewares/ConnectivityMiddleware.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.middlewares
2 |
3 | import com.christopherelias.blockchain.R
4 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddleware
5 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddlewareFailure
6 | import com.christopherelias.blockchain.utils.connectivity.ConnectivityUtils
7 | import com.christopherelias.blockchain.utils.resource_provider.ResourceProvider
8 | import javax.inject.Inject
9 |
10 | /*
11 | * Created by Christopher Elias on 9/06/2021
12 | * christopher.mike.96@gmail.com
13 | *
14 | * Lima, Peru.
15 | */
16 |
17 |
18 | class ConnectivityMiddleware @Inject constructor(
19 | private val connectivityUtils: ConnectivityUtils,
20 | private val resourceProvider: ResourceProvider
21 | ) : NetworkMiddleware() {
22 |
23 | override val failure: NetworkMiddlewareFailure
24 | get() = NetworkMiddlewareFailure(
25 | middleWareExceptionMessage = resourceProvider.getString(R.string.error_no_network_detected)
26 | )
27 |
28 | override fun isValid(): Boolean = connectivityUtils.isNetworkAvailable()
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/middlewares/MiddlewareProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.middlewares
2 |
3 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddleware
4 | import com.christopherelias.blockchain.core.network.middleware.provider.MiddlewareProvider
5 |
6 | /*
7 | * Created by Christopher Elias on 9/06/2021
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Lima, Peru.
11 | */
12 |
13 | class MiddlewareProviderImpl private constructor(
14 | private val middlewareList: List = listOf()
15 | ) : MiddlewareProvider {
16 |
17 | class Builder(
18 | private val middlewareList: MutableList = mutableListOf()
19 | ) {
20 |
21 | fun add(middleware: NetworkMiddleware) = apply {
22 | this.middlewareList.add(middleware)
23 | }
24 |
25 | fun build() = MiddlewareProviderImpl(
26 | middlewareList = middlewareList
27 | )
28 | }
29 |
30 |
31 | override fun getAll(): List = middlewareList
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.viewModels
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.lazy.LazyColumn
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.material.Surface
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.livedata.observeAsState
14 | import androidx.compose.ui.Modifier
15 | import com.christopherelias.blockchain.ui.components.CurrencyStatistics
16 | import com.christopherelias.blockchain.ui.components.StatsHorizontalList
17 | import com.christopherelias.blockchain.ui.models.Stats
18 | import com.christopherelias.blockchain.ui.theme.BlockchainTheme
19 | import dagger.hilt.android.AndroidEntryPoint
20 |
21 | @AndroidEntryPoint
22 | class MainActivity : ComponentActivity() {
23 |
24 | private val viewModel: MainViewModel by viewModels()
25 |
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 | setContent {
29 | BlockchainTheme {
30 | Content(viewModel = viewModel)
31 | }
32 | }
33 | }
34 | }
35 |
36 |
37 | /*
38 | * TODO:
39 | * Load Cards in Repository
40 | * Add HomeRepositoryUnitTests
41 | * Display list of StatsCards Composable in a 'GridView'
42 | * Make the whole screen Scrollable
43 | * Implement & Display Failure Screen if some failure is thrown
44 | */
45 | @Composable
46 | fun Content(viewModel: MainViewModel) {
47 | // A surface container using the 'background' color from the theme
48 | Surface(color = MaterialTheme.colors.background) {
49 | val homeState: HomeState by viewModel.homeState.observeAsState(HomeState())
50 | LazyColumn(
51 | modifier = Modifier
52 | .fillMaxSize()
53 | ) {
54 | with(homeState) {
55 | // Header
56 | item {
57 | StatsHorizontalList(
58 | stats = listOf(
59 | Stats(
60 | "Market Price",
61 | "$36,980.20",
62 | "The avarage USD market price acorss major bitcoin exchanges"
63 | ),
64 | Stats(
65 | "Market Price",
66 | "$36,980.20",
67 | "The avarage USD market price acorss major bitcoin exchanges"
68 | )
69 | )
70 | )
71 |
72 | }
73 | // Body Stats
74 | item {
75 | CurrencyStatistics(transactionsPerSecond = transactionsPerSecond)
76 | }
77 | }
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.christopherelias.blockchain.functional_programming.Failure
8 | import com.christopherelias.blockchain.features.home.domain.repository.HomeRepository
9 | import com.christopherelias.blockchain.ui.models.TransactionsPerSecond
10 | import com.christopherelias.blockchain.utils.OneTimeEvent
11 | import com.christopherelias.blockchain.utils.toOneTimeEvent
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 | /*
17 | * Created by Christopher Elias on 9/06/2021
18 | * christopher.mike.96@gmail.com
19 | *
20 | * Lima, Peru.
21 | */
22 |
23 | @HiltViewModel
24 | class MainViewModel @Inject constructor(
25 | private val repository: HomeRepository
26 | ) : ViewModel() {
27 |
28 | private val _homeState = MutableLiveData(HomeState())
29 | val homeState: LiveData = _homeState
30 |
31 | init {
32 | getHomeContent()
33 | }
34 |
35 | private fun getHomeContent() {
36 | // Initial State
37 | _homeState.value = _homeState.value?.copy(isLoading = true)
38 |
39 | // Execute
40 | viewModelScope.launch {
41 | repository.getTransactionsPerSecond()
42 | .either(::handleHomeContentFailure, ::handleHomeContentSuccess)
43 | }
44 | }
45 |
46 | private fun handleHomeContentFailure(
47 | failure: Failure
48 | ) {
49 | _homeState.value = _homeState.value?.copy(
50 | isLoading = false,
51 | failure = failure.toOneTimeEvent()
52 | )
53 | }
54 |
55 | private fun handleHomeContentSuccess(
56 | transactionsPerSecond: TransactionsPerSecond
57 | ) {
58 | _homeState.value = _homeState.value?.copy(
59 | isLoading = false,
60 | transactionsPerSecond = transactionsPerSecond,
61 | failure = null
62 | )
63 | }
64 | }
65 |
66 | data class HomeState(
67 | val isLoading: Boolean = false,
68 | val transactionsPerSecond: TransactionsPerSecond = TransactionsPerSecond.idle(),
69 | val failure: OneTimeEvent? = null
70 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/components/LinearChart.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui.components
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material.Card
10 | import androidx.compose.material.MaterialTheme
11 | import androidx.compose.material.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.geometry.Offset
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.graphics.drawscope.Stroke
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.unit.dp
19 | import com.christopherelias.blockchain.ui.models.TransactionsPerSecond
20 |
21 | /*
22 | * Created by Christopher Elias on 9/06/2021
23 | * christopher.mike.96@gmail.com
24 | *
25 | * Lima, Peru.
26 | */
27 |
28 | @Composable
29 | fun CurrencyStatistics(
30 | transactionsPerSecond: TransactionsPerSecond
31 | ) {
32 | Column {
33 | Text(
34 | text = "Currency Statistics",
35 | style = MaterialTheme.typography.h4,
36 | textAlign = TextAlign.Start,
37 | modifier = Modifier
38 | .fillMaxWidth()
39 | .padding(12.dp)
40 | )
41 | TransactionsPerSecondComposable(transactionsPerSecond = transactionsPerSecond)
42 | }
43 | }
44 |
45 | @Composable
46 | fun TransactionsPerSecondComposable(
47 | transactionsPerSecond: TransactionsPerSecond,
48 | ) {
49 | Card(
50 | shape = RoundedCornerShape(4.dp),
51 | elevation = 12.dp,
52 | modifier = Modifier
53 | .padding(12.dp)
54 | .fillMaxWidth()
55 | ) {
56 | Column {
57 | Text(
58 | text = "Transactions Per Second",
59 | style = MaterialTheme.typography.h6,
60 | textAlign = TextAlign.Center,
61 | modifier = Modifier
62 | .fillMaxWidth()
63 | .padding(12.dp)
64 | )
65 | LinearTransactionsChart(
66 | modifier = Modifier
67 | .height(250.dp)
68 | .fillMaxWidth()
69 | .padding(12.dp),
70 | transactionsPerSecond = transactionsPerSecond
71 | )
72 | }
73 | }
74 | }
75 |
76 | @Composable
77 | fun LinearTransactionsChart(
78 | modifier: Modifier = Modifier,
79 | transactionsPerSecond: TransactionsPerSecond
80 | ) {
81 | if (transactionsPerSecond.transactions.isEmpty()) return
82 |
83 | Canvas(modifier = modifier) {
84 | // Total number of transactions.
85 | val totalRecords = transactionsPerSecond.transactions.size
86 |
87 | // Maximum distance between dots (transactions)
88 | val lineDistance = size.width / (totalRecords + 1)
89 |
90 | // Canvas height
91 | val cHeight = size.height
92 |
93 | // Add some kind of a "Padding" for the initial point where the line starts.
94 | var currentLineDistance = 0F + lineDistance
95 |
96 | transactionsPerSecond.transactions.forEachIndexed { index, transactionRate ->
97 | if (totalRecords >= index + 2) {
98 | drawLine(
99 | start = Offset(
100 | x = currentLineDistance,
101 | y = calculateYCoordinate(
102 | higherTransactionRateValue = transactionsPerSecond.maxTransaction,
103 | currentTransactionRate = transactionRate.transactionsPerSecondValue,
104 | canvasHeight = cHeight
105 | )
106 | ),
107 | end = Offset(
108 | x = currentLineDistance + lineDistance,
109 | y = calculateYCoordinate(
110 | higherTransactionRateValue = transactionsPerSecond.maxTransaction,
111 | currentTransactionRate = transactionsPerSecond.transactions[index + 1].transactionsPerSecondValue,
112 | canvasHeight = cHeight
113 | )
114 | ),
115 | color = Color(40, 193, 218),
116 | strokeWidth = Stroke.DefaultMiter
117 | )
118 | }
119 | currentLineDistance += lineDistance
120 | }
121 | }
122 | }
123 |
124 | /**
125 | * Calculates the Y pixel coordinate for a given transaction rate.
126 | *
127 | * @param higherTransactionRateValue the highest rate value in the whole list of transactions.
128 | * @param currentTransactionRate the current transaction RATE while iterating the list of transactions.
129 | * @param canvasHeight the canvas HEIGHT for draw the linear chart.
130 | *
131 | * @return [Float] Y coordinate for a transaction rate.
132 | */
133 | private fun calculateYCoordinate(
134 | higherTransactionRateValue: Double,
135 | currentTransactionRate: Double,
136 | canvasHeight: Float
137 | ): Float {
138 | val maxAndCurrentValueDifference = (higherTransactionRateValue - currentTransactionRate)
139 | .toFloat()
140 | val relativePercentageOfScreen = (canvasHeight / higherTransactionRateValue)
141 | .toFloat()
142 | return maxAndCurrentValueDifference * relativePercentageOfScreen
143 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/components/StatsCards.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.foundation.lazy.LazyRow
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.Card
11 | import androidx.compose.material.MaterialTheme
12 | import androidx.compose.material.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.text.style.TextAlign
20 | import androidx.compose.ui.text.style.TextOverflow
21 | import com.christopherelias.blockchain.ui.models.Stats
22 |
23 | /*
24 | * Created by Christopher Elias on 9/06/2021
25 | * christopher.mike.96@gmail.com
26 | *
27 | * Lima, Peru.
28 | */
29 |
30 | @Composable
31 | fun StatsHorizontalList(
32 | stats: List
33 | ) {
34 | Column {
35 | Text(
36 | text = "Popular Stats",
37 | style = MaterialTheme.typography.h4,
38 | textAlign = TextAlign.Start,
39 | modifier = Modifier
40 | .fillMaxWidth()
41 | .padding(12.dp)
42 | )
43 | LazyRow {
44 | items(stats) { statItem ->
45 | StatsCardItem(
46 | title = statItem.title,
47 | content = statItem.content,
48 | description = statItem.description,
49 | )
50 | }
51 | }
52 | }
53 | }
54 |
55 | @Composable
56 | fun StatsCardItem(
57 | title: String,
58 | content: String,
59 | description: String
60 | ) {
61 | Card(
62 | shape = RoundedCornerShape(4.dp),
63 | elevation = 12.dp,
64 | modifier = Modifier
65 | .padding(8.dp)
66 | .width(200.dp)
67 | ) {
68 | Column(
69 | horizontalAlignment = Alignment.CenterHorizontally
70 | ) {
71 | Text(
72 | text = title,
73 | modifier = Modifier.padding(6.dp)
74 | )
75 | Text(
76 | text = content,
77 | style = MaterialTheme.typography.h6,
78 | maxLines = 1,
79 | overflow = TextOverflow.Ellipsis,
80 | color = Color(40, 193, 218),
81 | modifier = Modifier.padding(6.dp)
82 | )
83 | Text(
84 | text = description,
85 | style = MaterialTheme.typography.caption,
86 | modifier = Modifier.padding(6.dp),
87 | textAlign = TextAlign.Center
88 | )
89 |
90 | }
91 | }
92 | }
93 |
94 | @Preview(showBackground = true)
95 | @Composable
96 | fun StatCardPreview() {
97 | StatsHorizontalList(
98 | stats = listOf(
99 | Stats(
100 | "Market Price",
101 | "$36,980.2000000000000000",
102 | "The avarage USD market price acorss major bitcoin exchanges"
103 | ),
104 | Stats(
105 | "Market Price",
106 | "$36,980.20",
107 | "The avarage USD market price acorss major bitcoin exchanges"
108 | )
109 | )
110 | )
111 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/models/Stats.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui.models
2 |
3 | /*
4 | * Created by Christopher Elias on 9/06/2021
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Lima, Peru.
8 | */
9 |
10 | data class Stats(
11 | val title: String,
12 | val content: String,
13 | val description: String
14 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/models/TransactionRate.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui.models
2 |
3 | /*
4 | * Created by Christopher Elias on 9/06/2021
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Lima, Peru.
8 | */
9 |
10 | /**
11 | * Represents a transaction per day object.
12 | * @param timeStamp the time stamp of the transaction. TODO: We have to convert this to date at some point.
13 | * @param transactionsPerSecondValue the quantity of transactions made per day.
14 | */
15 | data class TransactionRate(
16 | val timeStamp: Long,
17 | val transactionsPerSecondValue: Double
18 | )
19 |
20 | /**
21 | * Represents a list of Transaction Rate Per Second
22 | * The number of transactions added to the mempool per second.
23 | */
24 | data class TransactionsPerSecond(
25 | val maxTransaction: Double,
26 | val transactions: List
27 | ) {
28 | companion object {
29 | fun idle(): TransactionsPerSecond {
30 | return TransactionsPerSecond(0.0, emptyList())
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun BlockchainTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
32 | val colors = if (darkTheme) {
33 | DarkColorPalette
34 | } else {
35 | LightColorPalette
36 | }
37 |
38 | MaterialTheme(
39 | colors = colors,
40 | typography = Typography,
41 | shapes = Shapes,
42 | content = content
43 | )
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/utils_impl/connectivity/ConnectivityUtilsImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils_impl.connectivity
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import android.os.Build
7 | import com.christopherelias.blockchain.utils.connectivity.ConnectivityUtils
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import javax.inject.Inject
10 |
11 | /*
12 | * Created by Christopher Elias on 9/06/2021
13 | * christopher.mike.96@gmail.com
14 | *
15 | * Lima, Peru.
16 | */
17 |
18 | // This can be an internal class if we move the module to
19 | class ConnectivityUtilsImpl @Inject constructor(
20 | @ApplicationContext private val applicationContext: Context
21 | ) : ConnectivityUtils {
22 | override fun isNetworkAvailable(): Boolean {
23 | try {
24 | val connectivityManager =
25 | applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
26 | val nw = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
27 | connectivityManager.activeNetwork ?: return false
28 | } else {
29 | return true
30 | }
31 | val actNw = connectivityManager.getNetworkCapabilities(nw) ?: return false
32 | return when {
33 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
34 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
35 | //for other device how are able to connect with Ethernet
36 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
37 | else -> false
38 | }
39 | } catch (e: Exception) {
40 | return false
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/christopherelias/blockchain/utils_impl/resource_provider/ResourceProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils_impl.resource_provider
2 |
3 | import android.content.Context
4 | import com.christopherelias.blockchain.utils.resource_provider.ResourceProvider
5 | import dagger.hilt.android.qualifiers.ApplicationContext
6 | import javax.inject.Inject
7 |
8 | /*
9 | * Created by Christopher Elias on 9/06/2021
10 | * christopher.mike.96@gmail.com
11 | *
12 | * Lima, Peru.
13 | */
14 |
15 | class ResourceProviderImpl @Inject constructor(
16 | @ApplicationContext private val context: Context
17 | ) : ResourceProvider {
18 |
19 | override fun getString(resourceId: Int): String = context.getString(resourceId)
20 |
21 | override fun getString(
22 | resourceId: Int,
23 | vararg args: Any
24 | ): String {
25 | return if (args.isNotEmpty()) {
26 | context.resources.getString(resourceId, *args)
27 | } else {
28 | context.resources.getString(resourceId)
29 | }
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/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.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Blockchain
3 | No internet connection found. Check that you are connected to a WIFI network or have your data turned on.
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/com/christopherelias/blockchain/HomeRepositoryUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain
2 |
3 | import com.christopherelias.blockchain.core.network.middleware.provider.MiddlewareProvider
4 | import com.christopherelias.blockchain.core.network.models.ResponseError
5 | import com.christopherelias.blockchain.core.network.models.ResponseList
6 | import com.christopherelias.blockchain.core.network.utils.ServiceBodyFailure
7 | import com.christopherelias.blockchain.features.home.data.data_source.HomeRemoteDataSource
8 | import com.christopherelias.blockchain.features.home.data.repository.HomeRepositoryImpl
9 | import com.christopherelias.blockchain.features.home.data_source.model.TransactionPerSecondResponse
10 | import com.christopherelias.blockchain.features.home.data_source.remote.HomeRemoteDataSourceImpl
11 | import com.christopherelias.blockchain.features.home.data_source.remote.HomeService
12 | import com.christopherelias.blockchain.features.home.domain.repository.HomeRepository
13 | import com.christopherelias.blockchain.features.home.mapper.TransactionMapper
14 | import com.christopherelias.blockchain.features.home.mapper.TransactionMapperImpl
15 | import com.christopherelias.blockchain.utils.DefaultTestNetworkMiddleware
16 | import com.christopherelias.blockchain.utils.TransactionsData
17 | import com.christopherelias.blockchain.utils.getDataWhenResultIsFailureOrThrowException
18 | import com.christopherelias.blockchain.utils.getDataWhenResultIsSuccessOrThrowException
19 | import com.squareup.moshi.JsonAdapter
20 | import com.squareup.moshi.Moshi
21 | import io.mockk.coEvery
22 | import io.mockk.every
23 | import io.mockk.mockk
24 | import kotlinx.coroutines.ExperimentalCoroutinesApi
25 | import kotlinx.coroutines.test.TestCoroutineDispatcher
26 | import kotlinx.coroutines.test.runBlockingTest
27 | import kotlinx.coroutines.withContext
28 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
29 | import okhttp3.ResponseBody.Companion.toResponseBody
30 | import org.junit.Assert.assertEquals
31 | import org.junit.Test
32 | import retrofit2.HttpException
33 | import retrofit2.Response
34 |
35 | /*
36 | * Created by Christopher Elias on 9/06/2021
37 | * christopher.mike.96@gmail.com
38 | *
39 | * Lima, Peru.
40 | */
41 |
42 | @ExperimentalCoroutinesApi
43 | class HomeRepositoryUnitTest {
44 |
45 | private val moshi = Moshi.Builder().build()
46 |
47 | private val errorAdapter: JsonAdapter =
48 | moshi.adapter(ResponseError::class.java)
49 |
50 | private val testCoroutineDispatcher = TestCoroutineDispatcher()
51 |
52 | private val homeService = mockk()
53 |
54 | private val middlewareProvider = mockk()
55 |
56 | private val remoteDataSource: HomeRemoteDataSource = HomeRemoteDataSourceImpl(
57 | middlewareProvider = middlewareProvider,
58 | ioDispatcher = testCoroutineDispatcher,
59 | errorAdapter = errorAdapter,
60 | homeService = homeService
61 | )
62 |
63 | private val mapper: TransactionMapper = TransactionMapperImpl(testCoroutineDispatcher)
64 |
65 | private val homeRepository: HomeRepository = HomeRepositoryImpl(
66 | mapper = mapper,
67 | homeRemoteDataSource = remoteDataSource
68 | )
69 |
70 |
71 | @Test
72 | fun `Assert repository return transaction rate values when remote service works as expected`() {
73 | // Load data from resources. This is not so "fake",
74 | // it's the actual response from the Blockchain API.
75 | val remoteTransactions: List =
76 | TransactionsData.provideRemoteTransactionsFromAssets()
77 |
78 | // Bypass all middlewares
79 | every { middlewareProvider.getAll() } returns listOf(
80 | DefaultTestNetworkMiddleware(
81 | isMiddlewareValid = true
82 | )
83 | )
84 |
85 | // Mock homeService response
86 | coEvery {
87 | homeService.getTransactionsPerSecond(
88 | chartName = any(),
89 | timeSpan = any(),
90 | rollingAverage = any()
91 | )
92 | } returns ResponseList(remoteTransactions)
93 |
94 | runBlockingTest {
95 |
96 | // TODO: Use async here for find the max rate value from remote while getting the repository data.
97 |
98 | val remoteMaxTransactionRateValue: Double = remoteTransactions.findMaxRateValue()
99 |
100 | homeRepository.getTransactionsPerSecond()
101 | .getDataWhenResultIsSuccessOrThrowException { transactionUi ->
102 | assertEquals(
103 | "Remote transactions list size doesn't match with the ones returned by the repository.",
104 | remoteTransactions.size,
105 | transactionUi.transactions.size
106 | )
107 | assertEquals(
108 | "Max transaction rate value from remote does not match the one returned by the repository",
109 | remoteMaxTransactionRateValue,
110 | transactionUi.maxTransaction,
111 | 0.001
112 | )
113 | }
114 | }
115 | }
116 |
117 | @Test
118 | fun `Assert repository return network service call exception properly`() {
119 | val errorBody = "{\"status\": \"Invalid Request\",\"error\": \"Error\"}"
120 | .toResponseBody("application/json".toMediaTypeOrNull())
121 |
122 | every { middlewareProvider.getAll() } returns listOf(
123 | DefaultTestNetworkMiddleware(
124 | isMiddlewareValid = true
125 | )
126 | )
127 |
128 | coEvery {
129 | homeService.getTransactionsPerSecond(any(), any(), any())
130 | } throws HttpException(Response.error(400, errorBody))
131 |
132 | runBlockingTest {
133 | homeRepository.getTransactionsPerSecond()
134 | .getDataWhenResultIsFailureOrThrowException { failure ->
135 | assertEquals(
136 | ServiceBodyFailure(
137 | internalStatus = "Invalid Request",
138 | internalMessage = "Error"
139 | ),
140 | failure
141 | )
142 | }
143 | }
144 | }
145 |
146 | private suspend fun List.findMaxRateValue(): Double {
147 | return withContext(testCoroutineDispatcher) {
148 | return@withContext maxByOrNull { it.transactionsPerSecondValue }
149 | ?.transactionsPerSecondValue ?: 0.0
150 | }
151 | }
152 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/christopherelias/blockchain/TransactionMapperUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain
2 |
3 | import com.christopherelias.blockchain.features.home.data_source.model.TransactionPerSecondResponse
4 | import com.christopherelias.blockchain.features.home.mapper.TransactionMapper
5 | import com.christopherelias.blockchain.features.home.mapper.TransactionMapperImpl
6 | import com.christopherelias.blockchain.ui.models.TransactionsPerSecond
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.test.TestCoroutineDispatcher
9 | import kotlinx.coroutines.test.runBlockingTest
10 | import org.junit.Assert.assertEquals
11 | import org.junit.Test
12 |
13 | /*
14 | * Created by Christopher Elias on 10/06/2021
15 | * christopher.mike.96@gmail.com
16 | *
17 | * Lima, Peru.
18 | */
19 |
20 | @ExperimentalCoroutinesApi
21 | class TransactionMapperUnitTest {
22 |
23 | companion object {
24 | private const val MAX_TRANSACTION_VALUE = 10.0
25 | }
26 |
27 | private val testCoroutineDispatcher = TestCoroutineDispatcher()
28 | private val mapper: TransactionMapper =
29 | TransactionMapperImpl(testCoroutineDispatcher)
30 |
31 | @Test
32 | fun `assert REMOTE transactions per second are mapped correctly for UI`() = runBlockingTest {
33 |
34 | // Some fake data
35 | val dummyFakeRemoteTransactions = listOf(
36 | TransactionPerSecondResponse(
37 | timeStamp = 1L,
38 | transactionsPerSecondValue = (MAX_TRANSACTION_VALUE - 1.0)
39 | ),
40 | TransactionPerSecondResponse(
41 | timeStamp = 3L,
42 | transactionsPerSecondValue = MAX_TRANSACTION_VALUE
43 | ),
44 | TransactionPerSecondResponse(
45 | timeStamp = 2L,
46 | transactionsPerSecondValue = (MAX_TRANSACTION_VALUE - 1.5)
47 | ),
48 | TransactionPerSecondResponse(
49 | timeStamp = 3L,
50 | transactionsPerSecondValue = (MAX_TRANSACTION_VALUE - 2.0)
51 | )
52 | )
53 |
54 | val uiTransactionObject: TransactionsPerSecond =
55 | mapper.mapRemoteTransactionsToUi(dummyFakeRemoteTransactions)
56 |
57 | assertEquals(
58 | "Max transaction value is not mapped correctly",
59 | MAX_TRANSACTION_VALUE,
60 | uiTransactionObject.maxTransaction,
61 | 0.001
62 | )
63 |
64 | assertEquals(
65 | "UI Transactions mapped does not have the same size of the remote list",
66 | dummyFakeRemoteTransactions.size,
67 | uiTransactionObject.transactions.size
68 | )
69 |
70 | val thirdRemoteItem = dummyFakeRemoteTransactions[2]
71 | val thirdUiItem = uiTransactionObject.transactions[2]
72 |
73 | assertEquals(
74 | "UI transaction mapped does not have the same timeStamp mark from its remote pair",
75 | thirdRemoteItem.timeStamp,
76 | thirdUiItem.timeStamp
77 | )
78 |
79 | assertEquals(
80 | "UI transaction mapped does not have the same transaction rate value from its remote pair",
81 | thirdRemoteItem.transactionsPerSecondValue,
82 | thirdUiItem.transactionsPerSecondValue,
83 | 0.001
84 | )
85 |
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/christopherelias/blockchain/TransactionRetrofitServiceUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain
2 |
3 | import com.christopherelias.blockchain.features.home.data_source.remote.HomeService
4 | import com.christopherelias.blockchain.utils.FileReaderUtil
5 | import com.christopherelias.blockchain.utils.TransactionsData
6 | import kotlinx.coroutines.runBlocking
7 | import okhttp3.mockwebserver.Dispatcher
8 | import okhttp3.mockwebserver.MockResponse
9 | import okhttp3.mockwebserver.MockWebServer
10 | import okhttp3.mockwebserver.RecordedRequest
11 | import org.junit.After
12 | import org.junit.Assert.*
13 | import org.junit.Before
14 | import org.junit.Test
15 | import retrofit2.Retrofit
16 | import retrofit2.converter.moshi.MoshiConverterFactory
17 |
18 | /*
19 | * Created by Christopher Elias on 9/06/2021
20 | * christopher.mike.96@gmail.com
21 | *
22 | * Lima, Peru.
23 | */
24 |
25 | //This will test if our data classes are well mapped with the expected response.
26 | class TransactionRetrofitServiceUnitTest {
27 |
28 | private val chartName = "transactions-per-second"
29 | private val timeSpan = "5weeks"
30 | private val rollingAverage = "8hours"
31 |
32 | // Mock web server
33 | private val mockWebServer = MockWebServer()
34 | private lateinit var transactionService: HomeService
35 |
36 | @Before
37 | fun setUp() {
38 | mockWebServer.start()
39 | mockWebServer.dispatcher = setUpMockWebServerDispatcher()
40 | setUpTransactionRetrofitService()
41 | }
42 |
43 | @After
44 | fun tearDown() {
45 | mockWebServer.shutdown()
46 | }
47 |
48 | @Test
49 | fun `Assert get transactions per second remote response structure match JSON Server response`() =
50 | runBlocking {
51 | // This shouldn't have to throw an error if the Transactions Response
52 | // is well mapped with the server response mocked in [setUpMockWebServerDispatcher]
53 | val remoteTransactions = transactionService.getTransactionsPerSecond(
54 | chartName = chartName,
55 | timeSpan = timeSpan,
56 | rollingAverage = rollingAverage
57 | )
58 |
59 | assertEquals(
60 | "Transactions size does not match the one provided in resources.",
61 | TransactionsData.provideRemoteTransactionsFromAssets().size,
62 | remoteTransactions.items.size
63 | )
64 | }
65 |
66 | private fun setUpTransactionRetrofitService() {
67 | transactionService = Retrofit.Builder()
68 | .addConverterFactory(MoshiConverterFactory.create())
69 | .baseUrl(mockWebServer.url("/"))
70 | .build()
71 | .create(HomeService::class.java)
72 | }
73 |
74 | private fun setUpMockWebServerDispatcher(): Dispatcher = object : Dispatcher() {
75 | override fun dispatch(request: RecordedRequest): MockResponse {
76 | println("BASE_URL${request.path}")
77 | return when (request.path) {
78 | "/charts/$chartName?timespan=$timeSpan&rollingAverage=$rollingAverage" -> {
79 | MockResponse()
80 | .setResponseCode(200)
81 | .setBody(FileReaderUtil.kotlinReadFileWithNewLineFromResources("transactions_response.json"))
82 | }
83 | else -> MockResponse().setResponseCode(404)
84 | }
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/christopherelias/blockchain/utils/DefaultTestNetworkMiddleware.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils
2 |
3 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddleware
4 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddlewareFailure
5 |
6 | /*
7 | * Created by Christopher Elias on 10/06/2021
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Lima, Peru.
11 | */
12 |
13 | class DefaultTestNetworkMiddleware(
14 | private val isMiddlewareValid: Boolean,
15 | private val failureMessage: String = ""
16 | ) : NetworkMiddleware() {
17 |
18 | override val failure: NetworkMiddlewareFailure
19 | get() = NetworkMiddlewareFailure(middleWareExceptionMessage = failureMessage)
20 |
21 | override fun isValid(): Boolean = isMiddlewareValid
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/christopherelias/blockchain/utils/EitherTestException.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils
2 |
3 | import java.lang.Exception
4 |
5 | /*
6 | * Created by Christopher Elias on 10/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | class EitherTestException(failureMessage: String = "") : Exception(failureMessage)
--------------------------------------------------------------------------------
/app/src/test/java/com/christopherelias/blockchain/utils/EitherTestExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils
2 |
3 | import com.christopherelias.blockchain.functional_programming.Either
4 |
5 | /*
6 | * Created by Christopher Elias on 10/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | /**
13 | * @param block the block to ve invoked if the result of the [Either] instance is a [Either.Success]
14 | * @throws [EitherTestException] if the result of the [Either] instance is [Either.Error]
15 | */
16 | inline fun Either.getDataWhenResultIsSuccessOrThrowException(
17 | block: (someResult: R) -> Unit
18 | ) {
19 | if (isSuccess) {
20 | block((this as Either.Success).success)
21 | } else {
22 | throw EitherTestException("The result is a Failure. ${(this as Either.Error).error}")
23 | }
24 | }
25 |
26 | /**
27 | * @param block the block to ve invoked if the result of the [Either] instance is a [Either.Error]
28 | * @throws [EitherTestException] if the result of the [Either] instance is [Either.Success]
29 | */
30 | inline fun Either.getDataWhenResultIsFailureOrThrowException(
31 | block: (someError: L) -> Unit
32 | ) {
33 | if (isError) {
34 | block((this as Either.Error).error)
35 | } else {
36 | throw EitherTestException("The result is Success. ${(this as Either.Success).success}")
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/christopherelias/blockchain/utils/FileReaderUtil.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils
2 |
3 | import java.io.*
4 |
5 | /*
6 | * Created by Christopher Elias on 9/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | /**
13 | * @see [https://medium.com/mobile-app-development-publication/android-reading-a-text-file-during-test-2815671e8b3b] (read on private mode)
14 | * @see [https://github.com/elye/demo_android_mock_web_service/blob/master/app/src/test/java/com/example/mockserverexperiment/ChatTest.kt]
15 | */
16 | object FileReaderUtil {
17 |
18 | @Throws(IOException::class)
19 | fun readFileWithoutNewLineFromResources(fileName: String): String {
20 | var inputStream: InputStream? = null
21 | try {
22 | inputStream = getInputStreamFromResource(fileName)
23 | val builder = StringBuilder()
24 | val reader = BufferedReader(InputStreamReader(inputStream))
25 |
26 | var str: String? = reader.readLine()
27 | while (str != null) {
28 | builder.append(str)
29 | str = reader.readLine()
30 | }
31 | return builder.toString()
32 | } finally {
33 | inputStream?.close()
34 | }
35 | }
36 |
37 | @Throws(IOException::class)
38 | fun readFileWithNewLineFromResources(fileName: String): String {
39 | var inputStream: InputStream? = null
40 | try {
41 | inputStream = getInputStreamFromResource(fileName)
42 | val builder = StringBuilder()
43 | val reader = BufferedReader(InputStreamReader(inputStream))
44 |
45 | var theCharNum = reader.read()
46 | while (theCharNum != -1) {
47 | builder.append(theCharNum.toChar())
48 | theCharNum = reader.read()
49 | }
50 |
51 | return builder.toString()
52 | } finally {
53 | inputStream?.close()
54 | }
55 | }
56 |
57 | fun kotlinReadFileWithNewLineFromResources(fileName: String): String {
58 | return getInputStreamFromResource(fileName)?.bufferedReader()
59 | .use { bufferReader -> bufferReader?.readText() } ?: ""
60 | }
61 |
62 | @Throws(IOException::class)
63 | fun readBinaryFileFromResources(fileName: String): ByteArray {
64 | var inputStream: InputStream? = null
65 | val byteStream = ByteArrayOutputStream()
66 | try {
67 | inputStream = getInputStreamFromResource(fileName)
68 |
69 | var nextValue = inputStream?.read() ?: -1
70 |
71 | while (nextValue != -1) {
72 | byteStream.write(nextValue)
73 | nextValue = inputStream?.read() ?: -1
74 | }
75 | return byteStream.toByteArray()
76 |
77 | } finally {
78 | inputStream?.close()
79 | byteStream.close()
80 | }
81 | }
82 |
83 | fun kotlinReadBinaryFileFromResources(fileName: String): ByteArray {
84 | ByteArrayOutputStream().use { byteStream ->
85 | getInputStreamFromResource(fileName)?.copyTo(byteStream)
86 | return byteStream.toByteArray()
87 | }
88 | }
89 |
90 | private fun getInputStreamFromResource(fileName: String) =
91 | javaClass.classLoader?.getResourceAsStream(fileName)
92 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/christopherelias/blockchain/utils/TransactionsData.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils
2 |
3 | import com.christopherelias.blockchain.core.network.models.ResponseList
4 | import com.christopherelias.blockchain.features.home.data_source.model.TransactionPerSecondResponse
5 | import com.squareup.moshi.JsonAdapter
6 | import com.squareup.moshi.Moshi
7 | import com.squareup.moshi.Types
8 | import java.lang.reflect.Type
9 |
10 | /*
11 | * Created by Christopher Elias on 9/06/2021
12 | * christopher.mike.96@gmail.com
13 | *
14 | * Lima, Peru.
15 | */
16 |
17 | object TransactionsData {
18 |
19 | private val moshi = Moshi.Builder().build()
20 | private val transactionsResponseGenericType: Type = Types.newParameterizedType(
21 | ResponseList::class.java,
22 | TransactionPerSecondResponse::class.java
23 | )
24 | private val remoteTransactionsAdapter: JsonAdapter> =
25 | moshi.adapter(transactionsResponseGenericType)
26 |
27 | fun provideRemoteTransactionsFromAssets(): List {
28 | return remoteTransactionsAdapter.fromJson(
29 | FileReaderUtil.kotlinReadFileWithNewLineFromResources(
30 | fileName = "transactions_response.json"
31 | )
32 | )?.items ?: listOf()
33 | }
34 | }
--------------------------------------------------------------------------------
/art/blockchain_app_architecture_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/art/blockchain_app_architecture_diagram.png
--------------------------------------------------------------------------------
/art/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/art/screenshot.jpg
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | dependencies {
8 | classpath(com.christopherelias.blockchain.buildsrc.Libs.androidToolsBuildGradle)
9 | classpath(com.christopherelias.blockchain.buildsrc.Libs.Kotlin.gradlePlugin)
10 | classpath(com.christopherelias.blockchain.buildsrc.Libs.Hilt.gradlePlugin)
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | tasks.register("clean", Delete::class) {
18 | delete(rootProject.buildDir)
19 | }
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | repositories {
2 | jcenter()
3 | }
4 |
5 | plugins {
6 | `kotlin-dsl`
7 | }
8 |
--------------------------------------------------------------------------------
/buildSrc/src/main/java/com/christopherelias/blockchain/buildsrc/dependencies.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.buildsrc
2 |
3 | object Releases {
4 | const val versionCode = 1
5 | const val versionName = "0.0.1"
6 | }
7 |
8 | object DefaultConfig {
9 | const val buildToolsVersion = "30.0.3"
10 | const val appId = "com.christopher_elias.blockchain"
11 | const val minSdk = 21
12 | const val targetSdk = 30
13 | const val compileSdk = 30
14 | }
15 |
16 | object Core {
17 | const val network = ":core:network"
18 | const val functionalProgramming = ":core:functional-programming"
19 | }
20 |
21 | object Modules {
22 | const val utils = ":utils"
23 | }
24 |
25 | object Libs {
26 |
27 | const val androidToolsBuildGradle = "com.android.tools.build:gradle:7.0.0"
28 |
29 | const val googleMaterial = "com.google.android.material:material:1.4.0"
30 |
31 | const val timber = "com.jakewharton.timber:timber:4.7.1"
32 |
33 | //--- Unit Test ---
34 | const val mockkTesting = "io.mockk:mockk:1.11.0"
35 | const val jUnit4Testing = "junit:junit:4.13.2"
36 | //---
37 |
38 | object Kotlin {
39 | private const val version = "1.5.10"
40 | const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib:$version"
41 | const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$version"
42 | }
43 |
44 | object Coroutines {
45 | private const val version = "1.5.1"
46 | const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
47 | const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"
48 | // Unit Test
49 | const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version"
50 | }
51 |
52 | object AndroidX {
53 | const val coreKtx = "androidx.core:core-ktx:1.6.0"
54 | const val appCompat = "androidx.appcompat:appcompat:1.3.1"
55 |
56 | object Lifecycle {
57 | private const val version = "2.3.1"
58 | const val viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$version"
59 | const val runtimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:$version"
60 | }
61 |
62 | object Compose {
63 | const val version = "1.0.0"
64 | const val ui = "androidx.compose.ui:ui:$version"
65 | const val tooling = "androidx.compose.ui:ui-tooling:$version"
66 | const val material = "androidx.compose.material:material:$version"
67 | const val runtimeLivedata = "androidx.compose.runtime:runtime-livedata:$version"
68 | const val activity = "androidx.activity:activity-compose:$version"
69 | object Test {
70 | //androidTestImplementation
71 | const val junit = "androidx.compose.ui:ui-test-junit4:$version"
72 | }
73 | }
74 |
75 | object Test {
76 | object Ext {
77 | //androidTestImplementation
78 | const val junit = "androidx.test.ext:junit:1.1.2"
79 | }
80 | //androidTestImplementation
81 | const val espressoCore = "androidx.test.espresso:espresso-core:3.3.0"
82 | }
83 | }
84 |
85 | object Square {
86 | private const val retrofitVersion = "2.9.0"
87 | const val retrofit = "com.squareup.retrofit2:retrofit:$retrofitVersion"
88 | const val moshi = "com.squareup.retrofit2:converter-moshi:$retrofitVersion"
89 | const val loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:4.9.0"
90 | object Test {
91 | const val mockWerbServer = "com.squareup.okhttp3:mockwebserver:4.9.0"
92 | }
93 |
94 | }
95 |
96 | object Hilt {
97 | private const val version = "2.36"
98 | const val gradlePlugin = "com.google.dagger:hilt-android-gradle-plugin:$version"
99 | const val android = "com.google.dagger:hilt-android:$version"
100 | const val kapt = "com.google.dagger:hilt-android-compiler:$version"
101 | }
102 | }
--------------------------------------------------------------------------------
/common-android-library.gradle:
--------------------------------------------------------------------------------
1 | import com.christopherelias.blockchain.buildsrc.DefaultConfig
2 | import com.christopherelias.blockchain.buildsrc.Releases
3 | import com.christopherelias.blockchain.buildsrc.Libs
4 |
5 | apply plugin: 'com.android.library'
6 | apply plugin: 'kotlin-android'
7 |
8 | android {
9 | compileSdkVersion DefaultConfig.compileSdk
10 |
11 | defaultConfig {
12 | minSdkVersion DefaultConfig.minSdk
13 | targetSdkVersion DefaultConfig.targetSdk
14 | versionCode Releases.versionCode
15 | versionName Releases.versionName
16 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
17 | }
18 |
19 | buildTypes {
20 | debug {
21 | debuggable true
22 | }
23 | }
24 |
25 | kotlinOptions.jvmTarget = "1.8"
26 |
27 | packagingOptions {
28 | exclude 'META-INF/DEPENDENCIES'
29 | exclude 'META-INF/LICENSE'
30 | exclude 'META-INF/LICENSE.txt'
31 | exclude 'META-INF/license.txt'
32 | exclude 'META-INF/NOTICE'
33 | exclude 'META-INF/NOTICE.txt'
34 | exclude 'META-INF/notice.txt'
35 | exclude 'META-INF/ASL2.0'
36 | exclude 'META-INF/LGPL2.1'
37 | exclude 'META-INF/AL2.0'
38 | exclude("META-INF/*.kotlin_module")
39 | }
40 | }
41 |
42 | dependencies {
43 | implementation Libs.Kotlin.stdlib
44 | }
--------------------------------------------------------------------------------
/common-kotlin-library.gradle:
--------------------------------------------------------------------------------
1 | import com.christopherelias.blockchain.buildsrc.Libs
2 |
3 | apply plugin: 'kotlin'
4 |
5 | dependencies {
6 | implementation Libs.Kotlin.stdlib
7 | }
8 |
--------------------------------------------------------------------------------
/core/functional-programming/build.gradle:
--------------------------------------------------------------------------------
1 | import com.christopherelias.blockchain.buildsrc.Libs
2 | apply from: "$rootDir/common-kotlin-library.gradle"
3 | apply plugin: 'kotlin-kapt'
4 |
5 | java {
6 | sourceCompatibility = JavaVersion.VERSION_1_8
7 | targetCompatibility = JavaVersion.VERSION_1_8
8 | }
9 |
10 | dependencies {
11 | api Libs.Coroutines.core
12 | testImplementation Libs.jUnit4Testing
13 | testImplementation Libs.Coroutines.test
14 | }
--------------------------------------------------------------------------------
/core/functional-programming/src/main/java/com/christopherelias/blockchain/functional_programming/Either.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.functional_programming
2 |
3 | import com.christopherelias.blockchain.functional_programming.utils.toSuccess
4 |
5 | /*
6 | * Created by Christopher Elias on 11/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | /**
13 | * Represents a value of one of two possible types (a disjoint union).
14 | * Instances of [Either] are either an instance of [Error] or [Success].
15 | * FP Convention dictates that [Error] is used for "failure"
16 | * and [Success] is used for "success".
17 | *
18 | * @see Error
19 | * @see Success
20 | *
21 | * https://danielwestheide.com/blog/the-neophytes-guide-to-scala-part-7-the-either-type/
22 | */
23 | sealed class Either {
24 |
25 | /** * Represents the left side of [Either] class which by convention is a "Failure". */
26 | data class Error(val error: L) : Either()
27 |
28 | /** * Represents the right side of [Either] class which by convention is a "Success". */
29 | data class Success(val success: R) : Either()
30 |
31 | val isSuccess get() = this is Success
32 | val isError get() = this is Error
33 |
34 | fun either(fnL: (L) -> Unit, fnR: (R) -> Unit): Any =
35 | when (this) {
36 | is Error -> fnL(error)
37 | is Success -> fnR(success)
38 | }
39 |
40 | /**
41 | * This method will send the Failure if is [Error].
42 | * But, if is [Success] then it will invoke the [transform]
43 | * suspend lambda that return the desired transformed object [T].
44 | *
45 | * @return [Either.Success] or the expected [Either.Error]
46 | * @see [https://github.com/arrow-kt/arrow-core/blob/master/arrow-core-data/src/main/kotlin/arrow/core/Either.kt]
47 | */
48 | suspend inline fun coMapSuccess(
49 | crossinline transform: suspend (R) -> T
50 | ): Either {
51 | return when (this) {
52 | is Success -> transform(this.success).toSuccess()
53 | is Error -> this
54 | }
55 | }
56 |
57 | /**
58 | * This method will send the Failure if is [Error].
59 | * But, if is [Success] then it will invoke the [transform] block and return the transformation result.
60 | *
61 | * @return [Either.Success] or the expected [Either.Error]
62 | * @see [https://github.com/arrow-kt/arrow-core/blob/master/arrow-core-data/src/main/kotlin/arrow/core/Either.kt]
63 | */
64 | inline fun mapSuccess(
65 | crossinline transform: (R) -> T
66 | ): Either {
67 | return when (this) {
68 | is Success -> transform(this.success).toSuccess()
69 | is Error -> this
70 | }
71 | }
72 |
73 | fun getSuccessOrNull(): R? = if (this is Success) {
74 | this.success
75 | } else {
76 | null
77 | }
78 |
79 | fun getFailureOrNull(): L? = if (this is Error) {
80 | this.error
81 | } else {
82 | null
83 | }
84 | }
--------------------------------------------------------------------------------
/core/functional-programming/src/main/java/com/christopherelias/blockchain/functional_programming/Failure.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.functional_programming
2 |
3 | /*
4 | * Created by Christopher Elias on 11/06/2021
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Lima, Peru.
8 | */
9 |
10 | sealed class Failure {
11 |
12 | /**
13 | * Extend this class in order to provide your own
14 | * custom failure.
15 | */
16 | open class CustomFailure : Failure()
17 |
18 | data class UnexpectedFailure(
19 | val message: String?
20 | ) : Failure()
21 |
22 | }
--------------------------------------------------------------------------------
/core/functional-programming/src/main/java/com/christopherelias/blockchain/functional_programming/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.functional_programming.utils
2 |
3 | import com.christopherelias.blockchain.functional_programming.Either
4 |
5 | /*
6 | * Created by Christopher Elias on 11/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | fun R.toSuccess(): Either.Success {
13 | return Either.Success(this)
14 | }
15 |
16 | fun L.toError(): Either.Error {
17 | return Either.Error(this)
18 | }
--------------------------------------------------------------------------------
/core/functional-programming/src/test/java/com/christopherelias/blockchain/functional_programming/EitherUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.functional_programming
2 |
3 | import com.christopherelias.blockchain.functional_programming.utils.toError
4 | import com.christopherelias.blockchain.functional_programming.utils.toSuccess
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.runBlockingTest
7 | import org.junit.Assert
8 | import org.junit.Assert.assertEquals
9 | import org.junit.Test
10 |
11 | /*
12 | * Created by Christopher Elias on 11/06/2021
13 | * christopher.mike.96@gmail.com
14 | *
15 | * Lima, Peru.
16 | */
17 |
18 | @ExperimentalCoroutinesApi
19 | class EitherUnitTest {
20 |
21 | @Test
22 | fun `assert coMapSuccess method will return a Success struct and not a Failure`() {
23 | runBlockingTest {
24 | val myAge = getAgeService().coMapSuccess { age -> age + 1 }
25 | Assert.assertTrue("My Age is not success.", myAge.isSuccess)
26 |
27 | myAge.coMapSuccess { resultAge ->
28 | Assert.assertEquals(26, resultAge)
29 | }
30 |
31 | }
32 | }
33 |
34 | @Test
35 | fun `assert coMapSuccess method will pass the failure if is not a Success`() {
36 | runBlockingTest {
37 | val someAge = someFailure().coMapSuccess { ageIfSuccess -> ageIfSuccess + 1 }
38 | Assert.assertTrue("My age is a success after all", someAge.isError)
39 | Assert.assertFalse(someAge.isSuccess)
40 |
41 | with(someAge as Either.Error) {
42 | with(this.error as Failure.UnexpectedFailure) {
43 | assertEquals(message, "ups!")
44 | }
45 | }
46 | }
47 | }
48 |
49 | private fun getAgeService(): Either =
50 | (2021.minus(1996)).toSuccess()
51 |
52 | private fun someFailure(): Either =
53 | Failure.UnexpectedFailure("ups!").toError()
54 | }
--------------------------------------------------------------------------------
/core/network/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/network/build.gradle:
--------------------------------------------------------------------------------
1 | import com.christopherelias.blockchain.buildsrc.Libs
2 | import com.christopherelias.blockchain.buildsrc.Core
3 |
4 | apply from: "$rootDir/common-kotlin-library.gradle"
5 | apply plugin: 'kotlin-kapt'
6 |
7 | java {
8 | sourceCompatibility = JavaVersion.VERSION_1_8
9 | targetCompatibility = JavaVersion.VERSION_1_8
10 | }
11 |
12 | dependencies {
13 | implementation project(Core.functionalProgramming)
14 |
15 | api Libs.Square.retrofit
16 | api Libs.Square.moshi
17 | api Libs.Square.loggingInterceptor
18 |
19 | testImplementation Libs.AndroidX.Test.Ext.junit
20 | testImplementation Libs.Coroutines.test
21 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/christopherelias/blockchain/core/network/middleware/NetworkMiddleware.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.core.network.middleware
2 |
3 | /*
4 | * Created by Christopher Elias on 11/06/2021
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Lima, Peru.
8 | */
9 |
10 | abstract class NetworkMiddleware {
11 |
12 | abstract val failure: NetworkMiddlewareFailure
13 |
14 | abstract fun isValid(): Boolean
15 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/christopherelias/blockchain/core/network/middleware/NetworkMiddlewareFailure.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.core.network.middleware
2 |
3 | import com.christopherelias.blockchain.functional_programming.Failure
4 |
5 | /*
6 | * Created by Christopher Elias on 11/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | class NetworkMiddlewareFailure(
13 | val middleWareExceptionMessage: String,
14 | ) : Failure.CustomFailure()
--------------------------------------------------------------------------------
/core/network/src/main/java/com/christopherelias/blockchain/core/network/middleware/provider/MiddlewareProvider.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.core.network.middleware.provider
2 |
3 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddleware
4 |
5 | /*
6 | * Created by Christopher Elias on 11/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | interface MiddlewareProvider {
13 | fun getAll(): List
14 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/christopherelias/blockchain/core/network/models/ResponseError.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.core.network.models
2 |
3 | import com.squareup.moshi.Json
4 |
5 | /*
6 | * Created by Christopher Elias on 11/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | data class ResponseError(
13 | @field:Json(name = "status") val status: String,
14 | @field:Json(name = "error") val error: String?
15 | )
--------------------------------------------------------------------------------
/core/network/src/main/java/com/christopherelias/blockchain/core/network/models/ResponseList.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.core.network.models
2 |
3 | import com.squareup.moshi.Json
4 |
5 | /*
6 | * Created by Christopher Elias on 11/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | data class ResponseList(
13 | @field:Json(name = "values") val items: List
14 | )
--------------------------------------------------------------------------------
/core/network/src/main/java/com/christopherelias/blockchain/core/network/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.core.network.utils
2 |
3 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddleware
4 | import com.christopherelias.blockchain.core.network.models.ResponseError
5 | import com.christopherelias.blockchain.functional_programming.Either
6 | import com.christopherelias.blockchain.functional_programming.Failure
7 | import com.christopherelias.blockchain.functional_programming.utils.toError
8 | import com.christopherelias.blockchain.functional_programming.utils.toSuccess
9 | import com.squareup.moshi.JsonAdapter
10 | import kotlinx.coroutines.CoroutineDispatcher
11 | import kotlinx.coroutines.withContext
12 | import okio.BufferedSource
13 | import retrofit2.HttpException
14 | import java.net.SocketTimeoutException
15 | import javax.net.ssl.SSLException
16 | import javax.net.ssl.SSLHandshakeException
17 |
18 | /*
19 | * Created by Christopher Elias on 11/06/2021
20 | * christopher.mike.96@gmail.com
21 | *
22 | * Lima, Peru.
23 | */
24 |
25 | /**
26 | * @param middleWares list of customizable [NetworkMiddleware] that would returns its error if one of them is not valid.
27 | * @param ioDispatcher the [CoroutineDispatcher] which is expected to be a Dispatcher.IO for make a safe call.
28 | * @param adapter the adapter to provide in order to parse the errors from the service.
29 | * @param retrofitCall the block to invoke the retrofit method.
30 | */
31 | suspend inline fun call(
32 | middleWares: List = emptyList(),
33 | ioDispatcher: CoroutineDispatcher,
34 | adapter: JsonAdapter,
35 | crossinline retrofitCall: suspend () -> T
36 | ): Either {
37 | return runMiddleWares(middleWares = middleWares)?.toError()
38 | ?: executeRetrofitCall(ioDispatcher, adapter, retrofitCall)
39 | }
40 |
41 | /**
42 | * Iterate ove all the [NetworkMiddleware] and return true if all of them are valid.
43 | * @return []
44 | */
45 | fun runMiddleWares(
46 | middleWares: List = emptyList(),
47 | ): Failure? {
48 | if (middleWares.isEmpty()) return null
49 | return middleWares.find { !it.isValid() }?.failure
50 | }
51 |
52 | /**
53 | * Executes a safe retrofit call without middlewares.
54 | * @param ioDispatcher the [CoroutineDispatcher] which is expected to be a Dispatcher.IO for make a safe call.
55 | * @param adapter the adapter to provide in order to parse the errors from the service.
56 | * @param retrofitCall the block to invoke the retrofit method.
57 | */
58 | suspend inline fun executeRetrofitCall(
59 | ioDispatcher: CoroutineDispatcher,
60 | adapter: JsonAdapter,
61 | crossinline retrofitCall: suspend () -> T
62 | ): Either {
63 | return withContext(ioDispatcher) {
64 | try {
65 | return@withContext retrofitCall().toSuccess()
66 | } catch (e: Exception) {
67 | return@withContext e.parseException(adapter).toError()
68 | }
69 | }
70 | }
71 |
72 | fun Throwable.parseException(
73 | adapter: JsonAdapter
74 | ): Failure {
75 | return when (this) {
76 | is SocketTimeoutException -> TimeOut
77 | is SSLException -> NetworkConnectionLostSuddenly
78 | is SSLHandshakeException -> SSLError
79 | is HttpException -> {
80 | val errorService = adapter.parseError(response()?.errorBody()?.source())
81 | if (errorService != null) {
82 | ServiceBodyFailure(
83 | internalStatus = errorService.status,
84 | internalMessage = errorService.error
85 | )
86 | } else {
87 | Failure.UnexpectedFailure(
88 | message = "Service ERROR BODY does not match."
89 | )
90 | }
91 | }
92 | else -> Failure.UnexpectedFailure(
93 | message = message ?: "Exception not handled caused an Unknown failure"
94 | )
95 | }
96 | }
97 |
98 | private fun JsonAdapter.parseError(
99 | json: BufferedSource?
100 | ): ResponseError? {
101 | return if (json != null) {
102 | fromJson(json)
103 | } else {
104 | null
105 | }
106 | }
107 |
108 | object TimeOut : Failure.CustomFailure()
109 |
110 | object NetworkConnectionLostSuddenly : Failure.CustomFailure()
111 |
112 | object SSLError : Failure.CustomFailure()
113 |
114 | /**
115 | * If your service return some custom error use this with the given attributes you expect.
116 | */
117 | data class ServiceBodyFailure(
118 | val internalStatus: String,
119 | val internalMessage: String?
120 | ) : Failure.CustomFailure()
121 |
--------------------------------------------------------------------------------
/core/network/src/test/java/com/christopherelias/blockchain/core/network/DumbMiddleware.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.core.network
2 |
3 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddleware
4 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddlewareFailure
5 |
6 | /*
7 | * Created by Christopher Elias on 11/06/2021
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Lima, Peru.
11 | */
12 |
13 | class DumbMiddleware(
14 | private val hardCodedValidation: Boolean,
15 | private val middlewareFailureMessage: String
16 | ) : NetworkMiddleware() {
17 |
18 | override val failure: NetworkMiddlewareFailure
19 | get() = NetworkMiddlewareFailure(middleWareExceptionMessage = middlewareFailureMessage)
20 |
21 | override fun isValid(): Boolean = hardCodedValidation
22 | }
23 |
24 | class AnotherDumbMiddleware() : NetworkMiddleware() {
25 | override val failure: NetworkMiddlewareFailure
26 | get() = NetworkMiddlewareFailure("")
27 |
28 | override fun isValid(): Boolean = true
29 |
30 | }
--------------------------------------------------------------------------------
/core/network/src/test/java/com/christopherelias/blockchain/core/network/NetworkCallExtensionUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.core.network
2 |
3 | import com.christopherelias.blockchain.core.network.middleware.NetworkMiddlewareFailure
4 | import com.christopherelias.blockchain.core.network.models.ResponseError
5 | import com.christopherelias.blockchain.core.network.utils.ServiceBodyFailure
6 | import com.christopherelias.blockchain.core.network.utils.call
7 | import com.christopherelias.blockchain.functional_programming.Either
8 | import com.christopherelias.blockchain.functional_programming.Failure
9 | import com.christopherelias.blockchain.functional_programming.utils.toError
10 | import com.christopherelias.blockchain.functional_programming.utils.toSuccess
11 | import com.squareup.moshi.JsonAdapter
12 | import com.squareup.moshi.Moshi
13 | import kotlinx.coroutines.ExperimentalCoroutinesApi
14 | import kotlinx.coroutines.test.TestCoroutineDispatcher
15 | import kotlinx.coroutines.test.runBlockingTest
16 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
17 | import okhttp3.ResponseBody.Companion.toResponseBody
18 | import org.junit.Assert.assertEquals
19 | import org.junit.Test
20 | import retrofit2.HttpException
21 | import retrofit2.Response
22 | import java.io.IOException
23 |
24 | /*
25 | * Created by Christopher Elias on 11/06/2021
26 | * christopher.mike.96@gmail.com
27 | *
28 | * Lima, Peru.
29 | */
30 |
31 | @ExperimentalCoroutinesApi
32 | class NetworkCallExtensionTest {
33 |
34 | private val dispatcher = TestCoroutineDispatcher()
35 | private val moshi = Moshi.Builder().build()
36 | private val adapter: JsonAdapter = moshi.adapter(ResponseError::class.java)
37 |
38 | @Test
39 | fun `when lambda returns successfully then it should emit the result as success`() {
40 | runBlockingTest {
41 |
42 | val lambdaResult = true
43 | val result = call(ioDispatcher = dispatcher, adapter = adapter) { lambdaResult }
44 |
45 | assertEquals(
46 | lambdaResult.toSuccess(),
47 | result
48 | )
49 | }
50 | }
51 |
52 | @Test
53 | fun `when lambda throws IOException then it should emit the result as UnknownFailure`() {
54 | runBlockingTest {
55 |
56 | val result = call(
57 | ioDispatcher = dispatcher,
58 | adapter = adapter
59 | ) { throw IOException("The fuck happened") }
60 |
61 | assertEquals(
62 | Failure.UnexpectedFailure(message = "The fuck happened").toError(),
63 | result
64 | )
65 | }
66 | }
67 |
68 | @Test
69 | fun `when lambda throws HttpException then it should emit the result as GenericError`() {
70 | val errorBody = "{\"status\": \"Invalid Request\",\"error\": \"Error\"}"
71 | .toResponseBody("application/json".toMediaTypeOrNull())
72 |
73 | runBlockingTest {
74 | val result = call(ioDispatcher = dispatcher, adapter = adapter) {
75 | throw HttpException(Response.error(400, errorBody))
76 | }
77 | assertEquals(
78 | ServiceBodyFailure(
79 | internalStatus = "Invalid Request",
80 | internalMessage = "Error"
81 | ).toError(),
82 | result
83 | )
84 | }
85 | }
86 |
87 | @Test
88 | fun `when lambda throws unknown exception then it should emit UnknownFailure`() {
89 | runBlockingTest {
90 | val result = call(ioDispatcher = dispatcher, adapter = adapter) {
91 | throw IllegalStateException("")
92 | }
93 | assertEquals(
94 | Failure.UnexpectedFailure("").toError(),
95 | result
96 | )
97 | }
98 | }
99 |
100 | @Test
101 | fun `when middleware is not valid return its failure`() {
102 | runBlockingTest {
103 | val dumbMiddleware = DumbMiddleware(
104 | hardCodedValidation = false,
105 | middlewareFailureMessage = "X"
106 | )
107 | val middlewares = listOf(
108 | AnotherDumbMiddleware(),
109 | AnotherDumbMiddleware(),
110 | dumbMiddleware,
111 | AnotherDumbMiddleware()
112 | )
113 | val result = call(
114 | middleWares = middlewares,
115 | ioDispatcher = dispatcher,
116 | adapter = adapter
117 | ) {
118 | 10
119 | }
120 | println("dumMiddleware: ${dumbMiddleware.failure.middleWareExceptionMessage}")
121 | println("result: $result")
122 |
123 | with(result as Either.Error) {
124 | assertEquals(
125 | dumbMiddleware.failure.middleWareExceptionMessage,
126 | this.error.middleWareExceptionMessage
127 | )
128 | }
129 | }
130 | }
131 |
132 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The Daemon is a long-lived process, so not only are we able to avoid the cost of JVM startup for every build,
9 | # but we are able to cache information about project structure, files, tasks, and more in memory
10 | # https://docs.gradle.org/current/userguide/gradle_daemon.html
11 | org.gradle.daemon=true
12 | # The setting is particularly useful for tweaking memory settings.
13 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | org.gradle.parallel=true
18 | # Caching
19 | org.gradle.caching=true
20 | # Experimental gradle config caching
21 | # https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:usage
22 | org.gradle.unsafe.configuration-cache=true
23 | org.gradle.unsafe.configuration-cache.max-problems=5
24 | # Setting configure on demand to true to only run configuration for all the modules that participate in the build task.
25 | org.gradle.configureondemand=true
26 | # AndroidX package structure to make it clearer which packages are bundled with the
27 | # Android operating system, and which are packaged with your app"s APK
28 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
29 | android.useAndroidX=true
30 | # Automatically convert third-party libraries to use AndroidX
31 | android.enableJetifier=true
32 | # Kotlin code style for this project: "official" or "obsolete":
33 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/compose-blockchain/8e7dd621de2cff6f8510b0df9eee0025ad3dc37d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jul 12 12:52:16 COT 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | rootProject.name = "Blockchain"
9 | include ':app'
10 | include ':core:functional-programming'
11 | include ':core:network'
12 | include ':utils'
13 |
--------------------------------------------------------------------------------
/utils/build.gradle:
--------------------------------------------------------------------------------
1 | apply from: "$rootDir/common-android-library.gradle"
2 | apply plugin: 'kotlin-kapt'
--------------------------------------------------------------------------------
/utils/src/androidTest/java/com/christopherelias/blockchain/utils/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.christopherelias.blockchain.utils.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/utils/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/utils/src/main/java/com/christopherelias/blockchain/utils/OneTimeEvent.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils
2 |
3 | import java.util.concurrent.atomic.AtomicBoolean
4 |
5 | /*
6 | * Created by Christopher Elias on 11/06/2021
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Lima, Peru.
10 | */
11 |
12 | /**
13 | * Used to represent a one-shot UI event within an [MviViewState], so that we don't have to
14 | * toggle [Boolean] values or use timers in Rx or anything too wild. [consumeOnce] allows you to
15 | * process the event only once.
16 | *
17 | * Can store whatever data you might want - most of the time this would be a [String] or
18 | * res ID [Int].
19 | */
20 | data class OneTimeEvent(val payload: T? = null) {
21 |
22 | private val isConsumed = AtomicBoolean(false)
23 |
24 | internal fun getValue(): T? =
25 | if (isConsumed.compareAndSet(false, true)) payload
26 | else null
27 | }
28 |
29 | fun T.toOneTimeEvent() =
30 | OneTimeEvent(this)
31 |
32 | /**
33 | * Allows you to consume the [OneTimeEvent.payload] of the event only once,
34 | * as it will be marked as consumed on access.
35 | */
36 |
37 | fun OneTimeEvent?.consumeOnce(block: (T) -> Unit) {
38 | this?.getValue()?.let { block(it) }
39 | }
--------------------------------------------------------------------------------
/utils/src/main/java/com/christopherelias/blockchain/utils/connectivity/ConnectivityUtils.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils.connectivity
2 |
3 | /*
4 | * Created by Christopher Elias on 11/06/2021
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Lima, Peru.
8 | */
9 |
10 | interface ConnectivityUtils {
11 | /**
12 | * @return TRUE if client is connected to Wife or Cell data.
13 | */
14 | fun isNetworkAvailable(): Boolean
15 | }
--------------------------------------------------------------------------------
/utils/src/main/java/com/christopherelias/blockchain/utils/resource_provider/ResourceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils.resource_provider
2 |
3 | /*
4 | * Created by Christopher Elias on 11/06/2021
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Lima, Peru.
8 | */
9 |
10 | interface ResourceProvider {
11 | fun getString(resourceId: Int): String
12 | fun getString(resourceId: Int, vararg args: Any): String
13 | }
--------------------------------------------------------------------------------
/utils/src/test/java/com/christopherelias/blockchain/utils/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopherelias.blockchain.utils
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------