├── .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 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 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 | ![GitHub Cards Preview](https://github.com/ChristopherME/Blockchain/blob/master/art/blockchain_app_architecture_diagram.png) 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 | ![GitHub Cards Preview](https://github.com/ChristopherME/Blockchain/blob/master/art/screenshot.jpg) 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 |