├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── markdown-navigator-enh.xml ├── markdown-navigator.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── enofeb │ │ └── coinbox │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── enofeb │ │ │ └── coinbox │ │ │ ├── CoinBoxApp.kt │ │ │ ├── home │ │ │ └── HomeActivity.kt │ │ │ └── splash │ │ │ └── SplashActivity.kt │ └── res │ │ ├── color │ │ └── bottom_navigation_selector.xml │ │ ├── drawable-v24 │ │ ├── ic_launcher_foreground.xml │ │ └── screens.png │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_z_cash.xml │ │ ├── layout │ │ └── activity_home.xml │ │ ├── menu │ │ └── bottom_nav_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── enofeb │ └── coinbox │ └── ExampleUnitTest.kt ├── base └── core │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── enofeb │ │ └── core │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── enofeb │ │ │ └── core │ │ │ ├── base │ │ │ ├── BaseFragment.kt │ │ │ └── BaseViewModel.kt │ │ │ ├── constants │ │ │ └── ApiConstants.kt │ │ │ ├── data │ │ │ ├── market │ │ │ │ ├── CurrencyType.kt │ │ │ │ ├── coin │ │ │ │ │ ├── Coin.kt │ │ │ │ │ ├── CoinDescription.kt │ │ │ │ │ ├── CoinDetail.kt │ │ │ │ │ └── CoinImage.kt │ │ │ │ ├── order │ │ │ │ │ └── HomeOrderType.kt │ │ │ │ └── popular │ │ │ │ │ ├── PopularCoin.kt │ │ │ │ │ ├── PopularCoinItem.kt │ │ │ │ │ └── PopularCoinResponse.kt │ │ │ └── price │ │ │ │ └── exchange │ │ │ │ ├── ExchangeRate.kt │ │ │ │ ├── ExchangeRateCollection.kt │ │ │ │ └── ExchangeRateResponse.kt │ │ │ ├── di │ │ │ ├── ApiModule.kt │ │ │ └── DataModule.kt │ │ │ ├── domain │ │ │ ├── Repository.kt │ │ │ ├── market │ │ │ │ ├── MarketRepository.kt │ │ │ │ └── MarketRepositoryImpl.kt │ │ │ └── price │ │ │ │ ├── PriceRepository.kt │ │ │ │ └── PriceRepositoryImpl.kt │ │ │ ├── extensions │ │ │ ├── DoubleExt.kt │ │ │ ├── LongExt.kt │ │ │ ├── RepositoryExt.kt │ │ │ ├── String.kt │ │ │ └── ViewModelExt.kt │ │ │ ├── service │ │ │ ├── market │ │ │ │ └── MarketService.kt │ │ │ └── price │ │ │ │ └── PriceService.kt │ │ │ ├── state │ │ │ └── State.kt │ │ │ └── ui │ │ │ ├── dimens │ │ │ └── Dimens.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ └── font │ │ ├── montserrat_light.ttf │ │ ├── montserrat_medium.ttf │ │ ├── montserrat_regular.ttf │ │ └── montserrat_semibold.ttf │ └── test │ └── java │ └── com │ └── enofeb │ └── core │ └── ExampleUnitTest.kt ├── build.gradle ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ ├── Dependencies.kt │ ├── Modules.kt │ ├── Plugins.kt │ └── Versions.kt ├── common-android-library.gradle ├── common.gradle ├── core-dependencies.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── presentation ├── dashboard │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── enofeb │ │ │ └── dashboard │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── enofeb │ │ │ │ └── dashboard │ │ │ │ └── home │ │ │ │ ├── HomeFragment.kt │ │ │ │ ├── HomeState.kt │ │ │ │ ├── HomeViewModel.kt │ │ │ │ └── detail │ │ │ │ ├── CoinDetailFragment.kt │ │ │ │ ├── CoinDetailState.kt │ │ │ │ └── CoinDetailViewModel.kt │ │ └── res │ │ │ ├── drawable │ │ │ ├── ic_calculation.xml │ │ │ ├── ic_home.xml │ │ │ └── ic_z_cash.xml │ │ │ ├── navigation │ │ │ └── nav_dashboard_graph.xml │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── enofeb │ │ └── dashboard │ │ └── ExampleUnitTest.kt └── price │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── enofeb │ │ └── price │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── enofeb │ │ │ └── price │ │ │ ├── PriceFragment.kt │ │ │ ├── PriceUiState.kt │ │ │ └── PriceViewModel.kt │ └── res │ │ ├── drawable │ │ └── ic_change.xml │ │ ├── navigation │ │ └── nav_price_graph.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── enofeb │ └── price │ └── ExampleUnitTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | 17 | # Project exclude paths 18 | /buildSrc/build/ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 25 | 26 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/markdown-navigator-enh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

CoinBox

2 | 3 |

4 | CoinBox is a multi module small currency tracker app which is developed with Jetpack Compose. This demo app used coingecko.com API to fetch data. You can track coins with different categories. 5 | Moreover you can check return value of bitcoin in different alt coin. It is possible to be informed with coins descriptions. 6 |

7 | 8 | ![Screens](app/src/main/res/drawable-v24/screens.png) 9 | 10 | # Technologies 11 | - Clean Architecture (Multi-module) 12 | - Kotlin 13 | - Architecture 14 | - MVVM Architecture 15 | - Repository pattern 16 | - Jetpack 17 | - Jetpack Compose 18 | - Navigation Component 19 | - ViewModel 20 | - Dagger Hilt 21 | - Coroutines 22 | - State Flow 23 | - Repository 24 | - Retrofit 25 | - Okhttp 26 | - [Accompanist](https://github.com/google/accompanist) 27 | 28 | # Contribution 29 | I will be appreciated if you open pull request. We can develop together <3 30 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: Plugins.androidApplication 2 | apply from: "$rootDir/common.gradle" 3 | apply from: "$rootDir/core-dependencies.gradle" 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion "30.0.3" 8 | 9 | buildFeatures { 10 | viewBinding = true 11 | } 12 | 13 | defaultConfig { 14 | applicationId "com.enofeb.coinbox" 15 | minSdkVersion 23 16 | targetSdkVersion 30 17 | versionCode 1 18 | versionName "1.0" 19 | vectorDrawables.useSupportLibrary = true 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | buildTypes.each { 30 | it.buildConfigField 'String', 'BASE_URL', BASE_URL 31 | } 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | dynamicFeatures = [] 40 | } 41 | 42 | dependencies { 43 | implementation project(Modules.dashboard) 44 | implementation project(Modules.price) 45 | implementation CoreLibraries.kotlin 46 | implementation Libraries.daggerHilt 47 | kapt Libraries.daggerCompiler 48 | } -------------------------------------------------------------------------------- /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/enofeb/coinbox/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.coinbox 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.enofeb.coinbox", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/enofeb/coinbox/CoinBoxApp.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.coinbox 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class CoinBoxApp : Application() { 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/enofeb/coinbox/home/HomeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.coinbox.home 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.navigation.NavController 6 | import androidx.navigation.fragment.NavHostFragment 7 | import androidx.navigation.ui.setupWithNavController 8 | import com.enofeb.coinbox.R 9 | import com.enofeb.coinbox.databinding.ActivityHomeBinding 10 | import com.google.android.material.bottomnavigation.BottomNavigationView 11 | import dagger.hilt.android.AndroidEntryPoint 12 | 13 | @AndroidEntryPoint 14 | class HomeActivity : AppCompatActivity() { 15 | 16 | private lateinit var navController: NavController 17 | lateinit var binding: ActivityHomeBinding 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | 22 | binding = ActivityHomeBinding.inflate(layoutInflater) 23 | setContentView(binding.root) 24 | 25 | val navHostFragment = supportFragmentManager.findFragmentById( 26 | R.id.nav_host_fragment 27 | ) as NavHostFragment 28 | navController = navHostFragment.navController 29 | 30 | setupBottomNavMenu(navController) 31 | } 32 | private fun setupBottomNavMenu(navController: NavController){ 33 | val bottomNav = findViewById(R.id.bottom_nav_view) 34 | bottomNav?.setupWithNavController(navController) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/enofeb/coinbox/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.coinbox.splash 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.animation.core.* 8 | import androidx.compose.foundation.Canvas 9 | import androidx.compose.foundation.Image 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.layout.* 12 | import androidx.compose.material.* 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.Phone 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.draw.rotate 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import com.enofeb.coinbox.R 27 | import com.enofeb.coinbox.home.HomeActivity 28 | import com.enofeb.core.ui.dimens.DefaultPadding 29 | import kotlinx.coroutines.* 30 | 31 | class SplashActivity : ComponentActivity() { 32 | 33 | private val splashScope = CoroutineScope(Dispatchers.Main) + SupervisorJob() 34 | 35 | override fun onCreate(savedInstanceState: Bundle?) { 36 | super.onCreate(savedInstanceState) 37 | setContent { 38 | MaterialTheme { 39 | InitialView() 40 | splashScope.launch { 41 | delay(1000) 42 | navigateToHome() 43 | } 44 | } 45 | } 46 | } 47 | 48 | override fun onPause() { 49 | splashScope.cancel() 50 | super.onPause() 51 | } 52 | 53 | private fun navigateToHome() { 54 | 55 | try { 56 | val intent = Intent( 57 | this, 58 | HomeActivity::class.java 59 | ) 60 | startActivity(intent) 61 | } catch (e: ClassNotFoundException) { 62 | e.printStackTrace() 63 | } 64 | } 65 | } 66 | 67 | @Composable 68 | fun InitialView() { 69 | 70 | val infiniteTransition = rememberInfiniteTransition() 71 | val infiniteAnimatedFloat = infiniteTransition.animateFloat( 72 | initialValue = 0f, 73 | targetValue = 180f, 74 | animationSpec = infiniteRepeatable( 75 | animation = tween(durationMillis = 1000), repeatMode = RepeatMode.Restart 76 | ) 77 | ) 78 | 79 | Column( 80 | modifier = Modifier 81 | .fillMaxSize() 82 | .background(Color(0xFFF0F8FF)) 83 | .padding(DefaultPadding), 84 | verticalArrangement = Arrangement.Center, 85 | horizontalAlignment = Alignment.CenterHorizontally 86 | ) { 87 | 88 | Image( 89 | painter = painterResource(id = R.drawable.ic_z_cash), 90 | contentDescription = "Desc", 91 | modifier = Modifier 92 | .size(100.dp) 93 | .rotate(infiniteAnimatedFloat.value) 94 | ) 95 | 96 | } 97 | } 98 | 99 | @Preview 100 | @Composable 101 | fun WatchPreview() { 102 | InitialView() 103 | } -------------------------------------------------------------------------------- /app/src/main/res/color/bottom_navigation_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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-v24/screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/drawable-v24/screens.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_z_cash.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_nav_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | 11 | #121212 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CoinBox 3 | core 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /app/src/test/java/com/enofeb/coinbox/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.coinbox 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 | } -------------------------------------------------------------------------------- /base/core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /base/core/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: "$rootDir/common-android-library.gradle" 2 | dependencies { 3 | 4 | //Support Libraries 5 | api SupportLibraries.appCompat 6 | api SupportLibraries.design 7 | api SupportLibraries.recyclerView 8 | api SupportLibraries.constraintLayout 9 | api SupportLibraries.androidXcore 10 | api SupportLibraries.androidXFragment 11 | api SupportLibraries.androidXActivityKtx 12 | api SupportLibraries.androidXFragmentKtx 13 | 14 | //Libraries 15 | api Libraries.viewModel 16 | //api Libraries.lifecycleExtensions 17 | api Libraries.lifecycleRuntime 18 | api Libraries.coroutinesCore 19 | api Libraries.coroutinesAndroid 20 | api Libraries.retrofit 21 | api Libraries.gson 22 | api Libraries.okHttp 23 | api Libraries.logInterceptor 24 | api Libraries.daggerHilt 25 | kapt Libraries.daggerCompiler 26 | 27 | } -------------------------------------------------------------------------------- /base/core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/base/core/consumer-rules.pro -------------------------------------------------------------------------------- /base/core/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 -------------------------------------------------------------------------------- /base/core/src/androidTest/java/com/enofeb/core/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core 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.enofeb.core.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /base/core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.base 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.material.Surface 5 | import androidx.compose.runtime.Composable 6 | import androidx.fragment.app.Fragment 7 | import com.enofeb.core.ui.theme.CoinBoxTheme 8 | 9 | abstract class BaseFragment: Fragment() { 10 | 11 | @Composable 12 | fun ComposeMagic(content: @Composable () -> Unit) { 13 | CoinBoxTheme(darkTheme = true) { 14 | Surface(color = MaterialTheme.colors.background) { 15 | content() 16 | } 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.enofeb.core.state.ErrorState 5 | import com.enofeb.core.state.LoadingState 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.StateFlow 8 | 9 | abstract class BaseViewModel : ViewModel() { 10 | 11 | protected val _loadingState = MutableStateFlow(LoadingState(false)) 12 | 13 | val loadingState: StateFlow = _loadingState 14 | 15 | protected val _errorState = MutableStateFlow(ErrorState()) 16 | 17 | val errorState: StateFlow = _errorState 18 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/constants/ApiConstants.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.constants 2 | 3 | class ApiConstants private constructor() { 4 | 5 | companion object { 6 | 7 | /* 8 | * Api version 9 | */ 10 | const val API_PREFIX = "/api" 11 | 12 | const val API_VERSION = "/v3" 13 | 14 | const val BASE_URL = "https://api.coingecko.com" 15 | const val API_KEY = "KM04dLO6He8BRlYi" 16 | 17 | const val TIMEOUT_MILLISECOND = 60000L 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/CurrencyType.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market 2 | 3 | enum class CurrencyType(val shortName: String) { 4 | USD("usd"), 5 | EURO("eur"), 6 | JPY("jpy"), 7 | TRY("try") 8 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/coin/Coin.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market.coin 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Coin( 6 | @SerializedName("id") val id: String, 7 | @SerializedName("symbol") val symbol: String, 8 | @SerializedName("name") val name: String, 9 | @SerializedName("image") val imageUrl: String, 10 | @SerializedName("current_price") val currentPrice: Double, 11 | @SerializedName("price_change_percentage_24h") val changePercentage: Double, 12 | @SerializedName("high_24h") val todayMaxPrice: Double, 13 | @SerializedName("total_volume") val volume: Long, 14 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/coin/CoinDescription.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market.coin 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class CoinDescription( 6 | @SerializedName("en") val detail: String 7 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/coin/CoinDetail.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market.coin 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class CoinDetail( 6 | @SerializedName("id") val id: String, 7 | @SerializedName("symbol") val symbol: String, 8 | @SerializedName("name") val name: String, 9 | @SerializedName("image") val image: CoinImage, 10 | @SerializedName("description") val description: CoinDescription 11 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/coin/CoinImage.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market.coin 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class CoinImage( 6 | @SerializedName("thumb") val thumb: String, 7 | @SerializedName("small") val small: String, 8 | @SerializedName("large") val large: String 9 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/order/HomeOrderType.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market.order 2 | 3 | enum class HomeOrderType( 4 | var title: String 5 | ) { 6 | HOT("Hot"), GAINER("Gainers"), LOSERS("Losers"), TODAYHIGH("24h") 7 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/popular/PopularCoin.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market.popular 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class PopularCoin( 6 | @SerializedName("item") val item: PopularCoinItem 7 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/popular/PopularCoinItem.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market.popular 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class PopularCoinItem( 6 | @SerializedName("id") val id: String, 7 | @SerializedName("symbol") val symbol: String, 8 | @SerializedName("name") val name: String, 9 | @SerializedName("large") val imageUrl: String, 10 | @SerializedName("price_btc") val priceBtc: Double 11 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/market/popular/PopularCoinResponse.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.market.popular 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class PopularCoinResponse( 6 | @SerializedName("coins") val coins: List 7 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/price/exchange/ExchangeRate.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.price.exchange 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ExchangeRate( 6 | @SerializedName("name") val name: String?, 7 | @SerializedName("unit") val unit: String, 8 | @SerializedName("value") val value: Double?, 9 | @SerializedName("type") val type: String? 10 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/price/exchange/ExchangeRateCollection.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.price.exchange 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ExchangeRateCollection( 6 | @SerializedName("btc") val bitcoin: ExchangeRate, 7 | @SerializedName("eth") val etherium: ExchangeRate, 8 | @SerializedName("xrp") val ripple: ExchangeRate, 9 | @SerializedName("ltc") val lite: ExchangeRate, 10 | ) { 11 | val exchangeList: MutableList 12 | get() = mutableListOf(bitcoin, etherium, ripple,lite) 13 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/data/price/exchange/ExchangeRateResponse.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.data.price.exchange 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ExchangeRateResponse( 6 | @SerializedName("rates") val rates: ExchangeRateCollection? 7 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/di/ApiModule.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.di 2 | 3 | import com.enofeb.core.constants.ApiConstants 4 | import com.enofeb.core.constants.ApiConstants.Companion.API_KEY 5 | import com.enofeb.core.constants.ApiConstants.Companion.BASE_URL 6 | import com.google.gson.Gson 7 | import com.google.gson.GsonBuilder 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import okhttp3.Interceptor 13 | import okhttp3.OkHttpClient 14 | import okhttp3.logging.HttpLoggingInterceptor 15 | import retrofit2.Retrofit 16 | import retrofit2.converter.gson.GsonConverterFactory 17 | import java.util.concurrent.TimeUnit 18 | import javax.inject.Singleton 19 | 20 | @Module 21 | @InstallIn(SingletonComponent::class) 22 | object ApiModule { 23 | 24 | @Provides 25 | @Singleton 26 | fun provideLoggingInterceptor(): HttpLoggingInterceptor = 27 | HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } 28 | 29 | @Provides 30 | @Singleton 31 | fun provideOkHttpClient( 32 | loggingInterceptor: HttpLoggingInterceptor 33 | ): OkHttpClient { 34 | val okHttpClientBuilder = OkHttpClient.Builder().apply { 35 | addInterceptor(loggingInterceptor) 36 | addInterceptor(Interceptor { chain -> 37 | val builder = chain.request().newBuilder() 38 | return@Interceptor chain.proceed(builder.build()) 39 | }) 40 | connectTimeout(ApiConstants.TIMEOUT_MILLISECOND, TimeUnit.MILLISECONDS) 41 | readTimeout(ApiConstants.TIMEOUT_MILLISECOND, TimeUnit.MILLISECONDS) 42 | } 43 | return okHttpClientBuilder.build() 44 | } 45 | 46 | @Provides 47 | @Singleton 48 | fun provideGson(): Gson { 49 | val gsonBuilder = GsonBuilder() 50 | return gsonBuilder.create() 51 | } 52 | 53 | @Provides 54 | @Singleton 55 | fun provideRetrofit(client: OkHttpClient, gson: Gson): Retrofit = 56 | with(Retrofit.Builder()) { 57 | baseUrl(BASE_URL) 58 | client(client) 59 | addConverterFactory(GsonConverterFactory.create(gson)) 60 | build() 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.di 2 | 3 | import com.enofeb.core.domain.market.MarketRepository 4 | import com.enofeb.core.domain.market.MarketRepositoryImpl 5 | import com.enofeb.core.domain.price.PriceRepository 6 | import com.enofeb.core.domain.price.PriceRepositoryImpl 7 | import com.enofeb.core.service.market.MarketService 8 | import com.enofeb.core.service.price.PriceService 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | import retrofit2.Retrofit 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object DataModule { 19 | 20 | @Provides 21 | @Singleton 22 | fun providePriceServices(retrofit: Retrofit): PriceService = 23 | retrofit.create(PriceService::class.java) 24 | 25 | @Provides 26 | @Singleton 27 | fun provideMarketServices(retrofit: Retrofit): MarketService = 28 | retrofit.create(MarketService::class.java) 29 | 30 | @Provides 31 | @Singleton 32 | fun providePriceRepository(priceService: PriceService): PriceRepository = 33 | PriceRepositoryImpl(priceService) 34 | 35 | @Provides 36 | @Singleton 37 | fun provideMarketRepository(marketService: MarketService): MarketRepository = 38 | MarketRepositoryImpl(marketService) 39 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/domain/Repository.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.domain 2 | 3 | interface Repository -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/domain/market/MarketRepository.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.domain.market 2 | 3 | import com.enofeb.core.data.market.coin.Coin 4 | import com.enofeb.core.data.market.coin.CoinDetail 5 | import com.enofeb.core.data.market.popular.PopularCoinResponse 6 | import com.enofeb.core.domain.Repository 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface MarketRepository : Repository { 10 | fun getCoinMarket(): Flow?> 11 | 12 | fun getPopularCoins(): Flow 13 | 14 | fun getCoinDetail(id: String): Flow 15 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/domain/market/MarketRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.domain.market 2 | 3 | import com.enofeb.core.data.market.coin.Coin 4 | import com.enofeb.core.data.market.coin.CoinDetail 5 | import com.enofeb.core.data.market.popular.PopularCoinResponse 6 | import com.enofeb.core.extensions.knock 7 | import com.enofeb.core.service.market.MarketService 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | class MarketRepositoryImpl(private val marketService: MarketService) : MarketRepository { 11 | 12 | override fun getCoinMarket(): Flow?> { 13 | return knock { 14 | marketService.getCoinMarket() 15 | } 16 | } 17 | 18 | override fun getPopularCoins(): Flow { 19 | return knock { 20 | marketService.getPopularCoin() 21 | } 22 | } 23 | 24 | override fun getCoinDetail(id: String): Flow { 25 | return knock { 26 | marketService.getCoinDetail(id) 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/domain/price/PriceRepository.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.domain.price 2 | 3 | import com.enofeb.core.data.price.exchange.ExchangeRateResponse 4 | import com.enofeb.core.domain.Repository 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface PriceRepository : Repository { 8 | 9 | fun getExchangeRates(): Flow 10 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/domain/price/PriceRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.domain.price 2 | 3 | import com.enofeb.core.data.price.exchange.ExchangeRateResponse 4 | import com.enofeb.core.extensions.knock 5 | import com.enofeb.core.service.price.PriceService 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | class PriceRepositoryImpl(private val priceService: PriceService) : PriceRepository { 9 | 10 | override fun getExchangeRates(): Flow { 11 | return knock { 12 | priceService.getExchangeRates() 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/extensions/DoubleExt.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.extensions 2 | 3 | import java.math.RoundingMode 4 | import java.text.DecimalFormat 5 | 6 | fun Double.roundOffDecimal(): String { 7 | val df = DecimalFormat("#.##") 8 | df.roundingMode = RoundingMode.FLOOR 9 | return df.format(this).toString() 10 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/extensions/LongExt.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.extensions 2 | 3 | import kotlin.math.ln 4 | import kotlin.math.pow 5 | 6 | fun Long.formatNumber(): String { 7 | if (this < 1000) return "" + this 8 | val exp = (ln(this.toDouble()) / ln(1000.0)).toInt() 9 | return String.format("%.1f %c", this / 1000.0.pow(exp.toDouble()), "kMGTPE"[exp - 1]) 10 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/extensions/RepositoryExt.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.extensions 2 | 3 | import com.enofeb.core.domain.Repository 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.flow 6 | import retrofit2.HttpException 7 | import retrofit2.Response 8 | 9 | fun Repository.knock(apiCall: suspend () -> T): Flow { 10 | return flow { 11 | val apiResult = apiCall.invoke() 12 | if (apiResult is Response<*>) { 13 | if (apiResult.code() >= 400) { 14 | throw HttpException(apiResult) 15 | } else { 16 | emit(apiResult) 17 | } 18 | } else { 19 | emit(apiResult) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/extensions/String.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.extensions 2 | 3 | fun String.addVolPrefix(): String { 4 | return "Vol $this" 5 | } 6 | 7 | fun String.addPercentage(): String { 8 | return "$this %" 9 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/extensions/ViewModelExt.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.extensions 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.launch 7 | 8 | fun ViewModel.io(block: suspend CoroutineScope.() -> Unit) { 9 | viewModelScope.launch { block() } 10 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/service/market/MarketService.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.service.market 2 | 3 | import com.enofeb.core.constants.ApiConstants 4 | import com.enofeb.core.data.market.coin.Coin 5 | import com.enofeb.core.data.market.CurrencyType 6 | import com.enofeb.core.data.market.coin.CoinDetail 7 | import com.enofeb.core.data.market.popular.PopularCoinResponse 8 | import retrofit2.http.GET 9 | import retrofit2.http.Path 10 | import retrofit2.http.Query 11 | 12 | interface MarketService { 13 | 14 | @GET("${ApiConstants.API_PREFIX}${ApiConstants.API_VERSION}/coins/markets") 15 | suspend fun getCoinMarket(@Query("vs_currency") vsCurrency: String? = CurrencyType.USD.shortName): List? 16 | 17 | @GET("${ApiConstants.API_PREFIX}${ApiConstants.API_VERSION}/search/trending") 18 | suspend fun getPopularCoin(): PopularCoinResponse 19 | 20 | @GET("${ApiConstants.API_PREFIX}${ApiConstants.API_VERSION}/coins/{id}") 21 | suspend fun getCoinDetail(@Path("id") id: String): CoinDetail 22 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/service/price/PriceService.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.service.price 2 | 3 | import com.enofeb.core.constants.ApiConstants.Companion.API_PREFIX 4 | import com.enofeb.core.constants.ApiConstants.Companion.API_VERSION 5 | import com.enofeb.core.data.price.exchange.ExchangeRateResponse 6 | import retrofit2.http.GET 7 | 8 | interface PriceService { 9 | 10 | @GET("${API_PREFIX}${API_VERSION}/exchange_rates") 11 | suspend fun getExchangeRates(): ExchangeRateResponse? 12 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/state/State.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.state 2 | 3 | data class LoadingState(val isLoading: Boolean?) 4 | 5 | data class ErrorState(val message: String? = null) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/ui/dimens/Dimens.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.ui.dimens 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | val SmallPadding = 5.dp 6 | val DefaultPadding = 16.dp 7 | 8 | val SmallSpace = 8.dp -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.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) 9 | val LightBlack = Color(0xFF171717) 10 | val MindGreen = Color(0xFF0BDA51) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.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 | ) -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Typography 6 | import androidx.compose.material.darkColors 7 | import androidx.compose.material.lightColors 8 | import androidx.compose.runtime.Composable 9 | 10 | private val DarkColorPalette = darkColors( 11 | primary = Purple200, 12 | primaryVariant = Purple700, 13 | secondary = Teal200, 14 | background = LightBlack, 15 | surface = LightBlack 16 | ) 17 | 18 | private val LightColorPalette = lightColors( 19 | primary = Purple500, 20 | primaryVariant = Purple700, 21 | secondary = Teal200 22 | 23 | /* Other default colors to override 24 | background = Color.White, 25 | surface = Color.White, 26 | onPrimary = Color.White, 27 | onSecondary = Color.Black, 28 | onBackground = Color.Black, 29 | onSurface = Color.Black, 30 | */ 31 | ) 32 | 33 | @Composable 34 | fun CoinBoxTheme( 35 | darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit 36 | ) { 37 | val colors = DarkColorPalette 38 | 39 | val typography = Typography(defaultFontFamily = Montserrat) 40 | 41 | MaterialTheme( 42 | colors = colors, 43 | typography = typography, 44 | shapes = Shapes, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /base/core/src/main/java/com/enofeb/core/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.Font 6 | import androidx.compose.ui.text.font.FontFamily 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.unit.sp 9 | import com.enofeb.core.R 10 | 11 | val Montserrat = FontFamily( 12 | Font(R.font.montserrat_light, FontWeight.Light), 13 | Font(R.font.montserrat_regular, FontWeight.Normal), 14 | Font(R.font.montserrat_medium, FontWeight.Medium), 15 | Font(R.font.montserrat_semibold, FontWeight.SemiBold) 16 | ) 17 | 18 | 19 | val Typography = Typography( 20 | h1 = TextStyle( 21 | fontFamily = Montserrat, 22 | fontSize = 96.sp, 23 | fontWeight = FontWeight.Light, 24 | lineHeight = 117.sp, 25 | letterSpacing = (-1.5).sp 26 | ), 27 | h2 = TextStyle( 28 | fontFamily = Montserrat, 29 | fontSize = 60.sp, 30 | fontWeight = FontWeight.Light, 31 | lineHeight = 73.sp, 32 | letterSpacing = (-0.5).sp 33 | ), 34 | h3 = TextStyle( 35 | fontFamily = Montserrat, 36 | fontSize = 48.sp, 37 | fontWeight = FontWeight.Normal, 38 | lineHeight = 59.sp 39 | ), 40 | h4 = TextStyle( 41 | fontFamily = Montserrat, 42 | fontSize = 30.sp, 43 | fontWeight = FontWeight.SemiBold, 44 | lineHeight = 37.sp 45 | ), 46 | h5 = TextStyle( 47 | fontFamily = Montserrat, 48 | fontSize = 24.sp, 49 | fontWeight = FontWeight.SemiBold, 50 | lineHeight = 29.sp 51 | ), 52 | h6 = TextStyle( 53 | fontFamily = Montserrat, 54 | fontSize = 20.sp, 55 | fontWeight = FontWeight.SemiBold, 56 | lineHeight = 24.sp 57 | ), 58 | body1 = TextStyle( 59 | fontFamily = FontFamily.Default, 60 | fontWeight = FontWeight.Normal, 61 | fontSize = 16.sp 62 | ), 63 | overline = TextStyle( 64 | fontFamily = Montserrat, 65 | fontSize = 12.sp, 66 | fontWeight = FontWeight.SemiBold, 67 | lineHeight = 16.sp, 68 | letterSpacing = 1.sp 69 | ) 70 | /* Other default text styles to override 71 | button = TextStyle( 72 | fontFamily = FontFamily.Default, 73 | fontWeight = FontWeight.W500, 74 | fontSize = 14.sp 75 | ), 76 | caption = TextStyle( 77 | fontFamily = FontFamily.Default, 78 | fontWeight = FontWeight.Normal, 79 | fontSize = 12.sp 80 | ) 81 | */ 82 | ) -------------------------------------------------------------------------------- /base/core/src/main/res/font/montserrat_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/base/core/src/main/res/font/montserrat_light.ttf -------------------------------------------------------------------------------- /base/core/src/main/res/font/montserrat_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/base/core/src/main/res/font/montserrat_medium.ttf -------------------------------------------------------------------------------- /base/core/src/main/res/font/montserrat_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/base/core/src/main/res/font/montserrat_regular.ttf -------------------------------------------------------------------------------- /base/core/src/main/res/font/montserrat_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/base/core/src/main/res/font/montserrat_semibold.ttf -------------------------------------------------------------------------------- /base/core/src/test/java/com/enofeb/core/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.core 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.compose_version = '1.0.0-beta09' 4 | ext.kotlin_version = "1.5.0" 5 | ext.dagger_hilt = "2.37" 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath "com.android.tools.build:gradle:4.2.1" 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | classpath "com.google.dagger:hilt-android-gradle-plugin:$dagger_hilt" 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | jcenter() // Warning: this repository is going to shut down soon 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.`kotlin-dsl` 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | } 6 | 7 | repositories { 8 | jcenter() 9 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Dependencies.kt: -------------------------------------------------------------------------------- 1 | object CoreLibraries { 2 | 3 | const val kotlin = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlinVersion}" 4 | } 5 | 6 | 7 | object SupportLibraries { 8 | 9 | const val appCompat = "androidx.appcompat:appcompat:${Versions.appCompatVersion}" 10 | const val design = "com.google.android.material:material:${Versions.materialVersion}" 11 | const val recyclerView = "androidx.recyclerview:recyclerview:${Versions.recyclerViewVersion}" 12 | const val constraintLayout = 13 | "androidx.constraintlayout:constraintlayout:${Versions.constraintLayoutVersion}" 14 | const val androidXcore = "androidx.core:core:${Versions.xCoreVersion}" 15 | const val androidXFragment = "androidx.fragment:fragment:${Versions.xFragmentVersion}" 16 | const val androidXFragmentKtx = "androidx.fragment:fragment-ktx:${Versions.xFragmentVersion}" 17 | const val androidXActivityKtx = "androidx.activity:activity-ktx:${Versions.xActivityKtxVersion}" 18 | } 19 | 20 | object Libraries { 21 | 22 | const val viewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.viewModelVersion}" 23 | const val lifecycleExtensions = 24 | "androidx.lifecycle:lifecycle-extensions:${Versions.lifecycleVersion}" 25 | const val lifecycleRuntime = 26 | "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycleRuntimeVersion}" 27 | const val coroutinesCore = 28 | "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutinesVersion}" 29 | const val coroutinesAndroid = 30 | "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutinesVersion}" 31 | const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofitVersion}" 32 | const val gson = "com.squareup.retrofit2:converter-gson:${Versions.retrofitVersion}" 33 | const val okHttp = "com.squareup.okhttp3:okhttp:${Versions.okHttpVersion}" 34 | const val logInterceptor = 35 | "com.squareup.okhttp3:logging-interceptor:${Versions.okHttpLoggingInterceptorVersion}" 36 | const val daggerHilt = "com.google.dagger:hilt-android:${Versions.daggerHilt}" 37 | const val daggerCompiler = "com.google.dagger:hilt-compiler:${Versions.daggerHilt}" 38 | const val compose = "androidx.compose.ui:ui:${Versions.compose}" 39 | const val composeMaterial = "androidx.compose.material:material:${Versions.compose}" 40 | const val composeTool = "androidx.compose.ui:ui-tooling:${Versions.compose}" 41 | const val composeActivity = "androidx.activity:activity-compose:${Versions.composeActivity}" 42 | const val composeMaterialIcon = 43 | "androidx.compose.material:material-icons-extended:${Versions.compose}" 44 | const val navigationComponent = 45 | "androidx.navigation:navigation-fragment-ktx:${Versions.navigationComponent}" 46 | const val navigationComponentUi = 47 | "androidx.navigation:navigation-ui-ktx:${Versions.navigationComponent}" 48 | const val navigationComponentDynamic = 49 | "androidx.navigation:navigation-dynamic-features-fragment:${Versions.navigationComponent}" 50 | const val coil = "io.coil-kt:coil-compose:${Versions.coilVersion}" 51 | const val composeInsets = 52 | "com.google.accompanist:accompanist-insets:${Versions.accompanistVersion}" 53 | const val composePager = 54 | "com.google.accompanist:accompanist-pager:${Versions.accompanistVersion}" 55 | const val composePagerIndicator = 56 | "com.google.accompanist:accompanist-pager-indicators:${Versions.accompanistVersion}" 57 | const val composeUtil = "androidx.compose.ui:ui-util:${Versions.compose}" 58 | } 59 | 60 | object TestLibraries { 61 | 62 | const val jUnit = "junit:junit:${Versions.jUnitVersion}" 63 | const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espressoCoreVersion}" 64 | const val composeTest = "androidx.compose.ui:ui-test-junit4:${Versions.composeActivity}" 65 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Modules.kt: -------------------------------------------------------------------------------- 1 | object Modules { 2 | 3 | private const val DIRECTORY_BASE = ":base" 4 | private const val DIRECTORY_PRESENTATION = ":presentation" 5 | 6 | 7 | /* 8 | * Base modules 9 | */ 10 | const val core = "$DIRECTORY_BASE:core" 11 | 12 | /* 13 | * Presentation modules 14 | */ 15 | const val dashboard = "$DIRECTORY_PRESENTATION:dashboard" 16 | 17 | /* 18 | * Presentation modules 19 | */ 20 | const val price = "$DIRECTORY_PRESENTATION:price" 21 | 22 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Plugins.kt: -------------------------------------------------------------------------------- 1 | object Plugins { 2 | const val androidApplication = "com.android.application" 3 | const val androidLibrary = "com.android.library" 4 | const val kotlinAndroid = "kotlin-android" 5 | const val kotlinKapt = "kotlin-kapt" 6 | const val kotlinParcelize = "kotlin-parcelize" 7 | const val daggerHilt = "dagger.hilt.android.plugin" 8 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Versions.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | 3 | const val kotlinVersion = "1.5.10" 4 | const val appCompatVersion = "1.3.0" 5 | const val recyclerViewVersion = "1.2.1" 6 | const val materialVersion = "1.4.0-beta01" 7 | const val xCoreVersion = "1.5.0" 8 | const val viewModelVersion = "2.4.0-alpha01" 9 | const val lifecycleVersion = "2.3.1" 10 | const val lifecycleRuntimeVersion = "2.4.0-alpha02" 11 | const val espressoCoreVersion = "3.1.0" 12 | const val jUnitVersion = "4.12" 13 | const val coroutinesVersion = "1.5.0" 14 | const val xFragmentVersion = "1.3.4" 15 | const val xActivityKtxVersion = "1.2.3" 16 | const val constraintLayoutVersion = "2.0.4" 17 | const val retrofitVersion = "2.9.0" 18 | const val okHttpVersion = "5.0.0-alpha.2" 19 | const val okHttpLoggingInterceptorVersion = "5.0.0-alpha.2" 20 | const val daggerHilt = "2.37" 21 | const val compose = "1.0.1" 22 | const val composeActivity = "1.3.1" 23 | const val navigationComponent = "2.3.5" 24 | const val coilVersion = "1.3.2" 25 | const val accompanistVersion = "0.17.0" 26 | } -------------------------------------------------------------------------------- /common-android-library.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: Plugins.androidLibrary 2 | apply from: "$rootDir/common.gradle" -------------------------------------------------------------------------------- /common.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: Plugins.kotlinAndroid 2 | apply plugin: Plugins.kotlinKapt 3 | apply plugin: Plugins.kotlinParcelize 4 | apply plugin: Plugins.daggerHilt 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | minSdkVersion 23 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | vectorDrawables.useSupportLibrary = true 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | kotlinOptions { 33 | useIR = true 34 | } 35 | buildFeatures { 36 | compose true 37 | } 38 | composeOptions { 39 | kotlinCompilerExtensionVersion compose_version 40 | kotlinCompilerVersion '1.5.10' 41 | } 42 | } 43 | 44 | kapt { 45 | correctErrorTypes true 46 | } 47 | 48 | repositories { 49 | google() 50 | } 51 | 52 | dependencies { 53 | implementation CoreLibraries.kotlin 54 | implementation Libraries.compose 55 | implementation Libraries.composeMaterial 56 | implementation Libraries.composeTool 57 | implementation Libraries.composeActivity 58 | implementation Libraries.navigationComponent 59 | implementation Libraries.navigationComponentUi 60 | implementation Libraries.navigationComponentDynamic 61 | implementation Libraries.coil 62 | implementation Libraries.composeInsets 63 | implementation Libraries.composePager 64 | implementation Libraries.composePagerIndicator 65 | implementation Libraries.composeUtil 66 | } -------------------------------------------------------------------------------- /core-dependencies.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(Modules.core) 3 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | BASE_URL="https://api.coingecko.com" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 29 17:46:11 TRT 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.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 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /presentation/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /presentation/dashboard/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: "$rootDir/common-android-library.gradle" 2 | apply from: "$rootDir/core-dependencies.gradle" 3 | 4 | dependencies { 5 | implementation Libraries.daggerHilt 6 | kapt Libraries.daggerCompiler 7 | } -------------------------------------------------------------------------------- /presentation/dashboard/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofeb/CoinBox/d80ae65be7f61be2b385bd17656075c422069d16/presentation/dashboard/consumer-rules.pro -------------------------------------------------------------------------------- /presentation/dashboard/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 -------------------------------------------------------------------------------- /presentation/dashboard/src/androidTest/java/com/enofeb/dashboard/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.dashboard 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.enofeb.dashboard.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /presentation/dashboard/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/dashboard/src/main/java/com/enofeb/dashboard/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.dashboard.home 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.compose.foundation.Image 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.material.* 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.collectAsState 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.ColorFilter 18 | import androidx.compose.ui.platform.ComposeView 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.tooling.preview.Preview 21 | import androidx.compose.ui.unit.dp 22 | import androidx.compose.ui.unit.sp 23 | import androidx.fragment.app.viewModels 24 | import androidx.navigation.findNavController 25 | import coil.compose.rememberImagePainter 26 | import com.enofeb.core.base.BaseFragment 27 | import com.enofeb.core.data.market.coin.Coin 28 | import com.enofeb.core.data.market.order.HomeOrderType 29 | import com.enofeb.core.extensions.addPercentage 30 | import com.enofeb.core.extensions.addVolPrefix 31 | import com.enofeb.core.extensions.formatNumber 32 | import com.enofeb.core.extensions.roundOffDecimal 33 | import com.enofeb.core.ui.dimens.DefaultPadding 34 | import com.enofeb.core.ui.dimens.SmallPadding 35 | import com.enofeb.core.ui.theme.MindGreen 36 | import com.enofeb.dashboard.R 37 | import com.google.accompanist.pager.ExperimentalPagerApi 38 | import com.google.accompanist.pager.rememberPagerState 39 | import dagger.hilt.android.AndroidEntryPoint 40 | import com.google.accompanist.pager.* 41 | 42 | private val CoinImageSize = 10.dp 43 | private val CoinElevation = 10.dp 44 | private val ToolbarHeight = 32.dp 45 | 46 | @AndroidEntryPoint 47 | class HomeFragment : BaseFragment() { 48 | 49 | private val viewModel by viewModels() 50 | 51 | @ExperimentalMaterialApi 52 | @ExperimentalPagerApi 53 | override fun onCreateView( 54 | inflater: LayoutInflater, 55 | container: ViewGroup?, 56 | savedInstanceState: Bundle? 57 | ): View = ComposeView(requireContext()).apply { 58 | setContent { 59 | ComposeMagic { 60 | CoinScreen(viewModel) { 61 | val bundle = Bundle().apply { 62 | this.putString(COIN_ID, it) 63 | } 64 | findNavController().navigate(R.id.action_home_to_coinDetail, bundle) 65 | } 66 | } 67 | } 68 | } 69 | 70 | companion object { 71 | const val COIN_ID = "coin_id" 72 | } 73 | 74 | } 75 | 76 | @ExperimentalMaterialApi 77 | @ExperimentalPagerApi 78 | @Composable 79 | fun CoinScreen(viewModel: HomeViewModel, onItemClick: (String) -> Unit) { 80 | 81 | val state = viewModel.homeUiState.collectAsState().value 82 | 83 | val errorState = viewModel.errorState.collectAsState().value 84 | 85 | val loadingState = viewModel.loadingState.collectAsState().value 86 | 87 | val listOrderTypes: List = listOf( 88 | HomeOrderType.HOT, 89 | HomeOrderType.GAINER, 90 | HomeOrderType.LOSERS, 91 | HomeOrderType.TODAYHIGH 92 | ) 93 | 94 | val pagerState = rememberPagerState(pageCount = listOrderTypes.size) 95 | 96 | Scaffold(topBar = { HomeAppBar() }) { 97 | Column { 98 | MarketOrderTabs(listOrderTypes, pagerState) 99 | MarketOrderTabsContent( 100 | pagerState, 101 | state.hotCoins, 102 | state.gainCoins, 103 | state.loserCoins, 104 | state.todayHighCoins, 105 | onItemClick, 106 | loadingState.isLoading 107 | ) 108 | } 109 | } 110 | 111 | } 112 | 113 | @Composable 114 | fun CoinList(coins: List?, onItemClick: (String) -> Unit) { 115 | coins?.let { list -> 116 | Column(Modifier.fillMaxSize()) { 117 | LazyColumn { 118 | items( 119 | items = list, 120 | itemContent = { CoinItem(coin = it, onItemClick) }) 121 | } 122 | } 123 | } 124 | } 125 | 126 | @OptIn(ExperimentalMaterialApi::class) 127 | @Composable 128 | fun CoinItem(coin: Coin, onItemClick: (String) -> Unit) { 129 | Card( 130 | modifier = 131 | Modifier 132 | .fillMaxWidth() 133 | .padding(DefaultPadding), elevation = CoinElevation, 134 | backgroundColor = Color.Black, 135 | onClick = { onItemClick.invoke(coin.id) } 136 | ) { 137 | Row( 138 | modifier = Modifier.padding(DefaultPadding), 139 | horizontalArrangement = Arrangement.SpaceBetween, 140 | verticalAlignment = Alignment.CenterVertically 141 | ) { 142 | Row(verticalAlignment = Alignment.CenterVertically) { 143 | Image( 144 | painter = rememberImagePainter(coin.imageUrl), 145 | contentDescription = null, 146 | modifier = Modifier 147 | .size(DefaultPadding, DefaultPadding) 148 | .align(Alignment.CenterVertically) 149 | ) 150 | Column(Modifier.padding(start = SmallPadding)) { 151 | Text( 152 | text = coin.symbol.uppercase(), 153 | color = Color.White 154 | ) 155 | Text( 156 | text = coin.volume.formatNumber().addVolPrefix(), 157 | color = Color.LightGray, 158 | fontSize = 10.sp 159 | ) 160 | } 161 | } 162 | Text( 163 | text = coin.currentPrice.toString(), 164 | color = Color.White, fontSize = 14.sp 165 | ) 166 | PercentageCard( 167 | coin.changePercentage.roundOffDecimal().addPercentage(), 168 | if (coin.changePercentage > 0) { 169 | MindGreen 170 | } else { 171 | Color.Red 172 | } 173 | ) 174 | } 175 | } 176 | } 177 | 178 | @Composable 179 | fun ShowProgress() { 180 | Column( 181 | verticalArrangement = Arrangement.Center, 182 | horizontalAlignment = Alignment.CenterHorizontally, 183 | modifier = Modifier.fillMaxSize(), 184 | ) { 185 | CircularProgressIndicator() 186 | } 187 | } 188 | 189 | @ExperimentalPagerApi 190 | @ExperimentalMaterialApi 191 | @Composable 192 | fun MarketOrderTabs(pages: List, pagerState: PagerState) { 193 | 194 | TabRow(selectedTabIndex = pagerState.currentPage, 195 | indicator = { tabPositions -> 196 | TabRowDefaults.Indicator( 197 | modifier = Modifier.pagerTabIndicatorOffset(pagerState, tabPositions) 198 | ) 199 | }) { 200 | pages.forEachIndexed { index, item -> 201 | Tab( 202 | text = { Text(item.title) }, 203 | selected = pagerState.currentPage == index, 204 | onClick = {} 205 | ) 206 | } 207 | } 208 | } 209 | 210 | @ExperimentalPagerApi 211 | @ExperimentalMaterialApi 212 | @Composable 213 | fun MarketOrderTabsContent( 214 | pagerState: PagerState, 215 | hotCoins: List?, 216 | gainerCoins: List?, 217 | loserCoins: List?, 218 | todayHighCoins: List?, 219 | onItemClick: (String) -> Unit, 220 | isLoading: Boolean? 221 | ) { 222 | 223 | if (isLoading == true) { 224 | ShowProgress() 225 | } 226 | 227 | HorizontalPager(state = pagerState) { page -> 228 | when (page) { 229 | HomeOrderType.HOT.ordinal -> { 230 | CoinList(hotCoins, onItemClick) 231 | } 232 | HomeOrderType.GAINER.ordinal -> { 233 | CoinList(gainerCoins, onItemClick) 234 | } 235 | HomeOrderType.LOSERS.ordinal -> { 236 | CoinList(loserCoins, onItemClick) 237 | } 238 | HomeOrderType.TODAYHIGH.ordinal -> { 239 | CoinList(todayHighCoins, onItemClick) 240 | } 241 | else -> { 242 | ShowProgress() 243 | } 244 | } 245 | } 246 | } 247 | 248 | @Composable 249 | fun HomeAppBar() { 250 | TopAppBar(modifier = Modifier.fillMaxWidth()) { 251 | Box(Modifier.height(ToolbarHeight)) { 252 | Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { 253 | Image( 254 | painter = painterResource(R.drawable.ic_z_cash), 255 | contentDescription = null, 256 | modifier = Modifier.fillMaxWidth(), 257 | colorFilter = ColorFilter.tint(Color.White) 258 | ) 259 | } 260 | } 261 | } 262 | 263 | } 264 | 265 | @Composable 266 | fun PercentageCard(value: String, color: Color) { 267 | Card( 268 | backgroundColor = color 269 | ) { 270 | Text( 271 | text = value, 272 | color = Color.White, 273 | modifier = Modifier.padding(SmallPadding), 274 | fontSize = 14.sp 275 | ) 276 | } 277 | } 278 | 279 | @Preview 280 | @Composable 281 | fun ItemPreview() { 282 | Card( 283 | modifier = 284 | Modifier 285 | .fillMaxWidth() 286 | .padding(DefaultPadding), elevation = CoinElevation, 287 | backgroundColor = Color.Black 288 | ) { 289 | Row( 290 | modifier = Modifier.padding(DefaultPadding), 291 | horizontalArrangement = Arrangement.SpaceBetween, 292 | verticalAlignment = Alignment.CenterVertically 293 | ) { 294 | Row(verticalAlignment = Alignment.CenterVertically) { 295 | Image( 296 | painter = rememberImagePainter("https://assets.coingecko.com/coins/images/279/large/ethereum.png?1595348880"), 297 | contentDescription = null, 298 | Modifier.size(CoinImageSize, CoinImageSize) 299 | ) 300 | Column { 301 | Text(text = "ETH", color = Color.White) 302 | Text(text = "Vol 32M", color = Color.LightGray, fontSize = 10.sp) 303 | } 304 | } 305 | Text(text = "3224.5", color = Color.White) 306 | Card( 307 | backgroundColor = MindGreen 308 | ) { 309 | Text( 310 | text = "+%1.20", 311 | color = Color.White, 312 | modifier = Modifier.padding(SmallPadding), 313 | fontSize = 14.sp 314 | ) 315 | } 316 | } 317 | } 318 | } -------------------------------------------------------------------------------- /presentation/dashboard/src/main/java/com/enofeb/dashboard/home/HomeState.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.dashboard.home 2 | 3 | import com.enofeb.core.data.market.coin.Coin 4 | 5 | data class HomeState( 6 | val hotCoins: List? = emptyList(), 7 | val gainCoins: List? = emptyList(), 8 | val loserCoins: List? = emptyList(), 9 | val todayHighCoins: List? = emptyList(), 10 | val isLoading: Boolean? = false 11 | ) -------------------------------------------------------------------------------- /presentation/dashboard/src/main/java/com/enofeb/dashboard/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.dashboard.home 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.enofeb.core.base.BaseViewModel 5 | import com.enofeb.core.domain.market.MarketRepository 6 | import com.enofeb.core.state.ErrorState 7 | import com.enofeb.core.state.LoadingState 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.* 10 | import kotlinx.coroutines.launch 11 | import javax.inject.Inject 12 | 13 | @HiltViewModel 14 | class HomeViewModel @Inject constructor( 15 | private val marketRepository: MarketRepository 16 | ) : BaseViewModel() { 17 | 18 | private val _homeUiState = MutableStateFlow(HomeState()) 19 | 20 | val homeUiState: StateFlow = _homeUiState 21 | 22 | init { 23 | viewModelScope.launch { 24 | combine( 25 | marketRepository.getCoinMarket(), 26 | marketRepository.getPopularCoins() 27 | ) { hotCoins, _ -> 28 | val gainerCoins = hotCoins?.filter { it.changePercentage > 0 } 29 | val loserCoins = hotCoins?.filter { it.changePercentage < 0 } 30 | val todayHigh = hotCoins?.sortedBy { it.todayMaxPrice } 31 | HomeState( 32 | hotCoins = hotCoins, 33 | gainCoins = gainerCoins, 34 | loserCoins = loserCoins, 35 | todayHighCoins = todayHigh 36 | ) 37 | }.onEach { 38 | _homeUiState.value = it 39 | _loadingState.value = LoadingState(false) 40 | }.catch { 41 | _errorState.value = ErrorState(it.message) 42 | }.onStart { 43 | _loadingState.value = LoadingState(true) 44 | }.collect() 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /presentation/dashboard/src/main/java/com/enofeb/dashboard/home/detail/CoinDetailFragment.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.dashboard.home.detail 2 | 3 | import android.os.Bundle 4 | import android.text.Html 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.compose.foundation.* 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.foundation.shape.CircleShape 11 | import androidx.compose.material.ExperimentalMaterialApi 12 | import androidx.compose.material.MaterialTheme 13 | import androidx.compose.material.Surface 14 | import androidx.compose.material.Text 15 | import androidx.compose.runtime.* 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Brush 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.graphics.graphicsLayer 20 | import androidx.compose.ui.layout.ContentScale 21 | import androidx.compose.ui.layout.Layout 22 | import androidx.compose.ui.platform.ComposeView 23 | import androidx.compose.ui.platform.LocalDensity 24 | import androidx.compose.ui.unit.* 25 | import com.enofeb.core.base.BaseFragment 26 | import dagger.hilt.android.AndroidEntryPoint 27 | import androidx.compose.ui.util.lerp 28 | import androidx.fragment.app.viewModels 29 | import coil.compose.rememberImagePainter 30 | import com.enofeb.core.ui.dimens.DefaultPadding 31 | import com.enofeb.core.ui.dimens.SmallSpace 32 | import com.enofeb.dashboard.R 33 | import com.enofeb.dashboard.home.HomeFragment.Companion.COIN_ID 34 | import com.enofeb.dashboard.home.ShowProgress 35 | import com.google.accompanist.insets.statusBarsPadding 36 | import com.google.accompanist.pager.ExperimentalPagerApi 37 | import kotlin.math.min 38 | import kotlin.math.max 39 | 40 | private val ExpandedImageSize = 300.dp 41 | private val CollapsedImageSize = 150.dp 42 | private val ImageOverlap = 115.dp 43 | private val GradientScroll = 180.dp 44 | private val MinTitleOffset = 56.dp 45 | private val MinImageOffset = 12.dp 46 | private val TitleHeight = 128.dp 47 | private val HeaderHeight = 280.dp 48 | private val MaxTitleOffset = ImageOverlap + MinTitleOffset + GradientScroll 49 | private val HzPadding = Modifier.padding(horizontal = 24.dp) 50 | 51 | @AndroidEntryPoint 52 | class CoinDetailFragment : BaseFragment() { 53 | 54 | private val viewModel by viewModels() 55 | 56 | override fun onCreate(savedInstanceState: Bundle?) { 57 | super.onCreate(savedInstanceState) 58 | 59 | arguments?.let { 60 | viewModel.apply { 61 | id = it.getString(COIN_ID) 62 | getCoinDetail() 63 | } 64 | } 65 | } 66 | 67 | @ExperimentalMaterialApi 68 | @ExperimentalPagerApi 69 | override fun onCreateView( 70 | inflater: LayoutInflater, 71 | container: ViewGroup?, 72 | savedInstanceState: Bundle? 73 | ): View = ComposeView(requireContext()).apply { 74 | setContent { 75 | ComposeMagic { 76 | CoinDetailScreen(viewModel) 77 | } 78 | } 79 | } 80 | } 81 | 82 | @Composable 83 | fun CoinDetailScreen(viewModel: CoinDetailViewModel) { 84 | 85 | val state = viewModel.coinDetailState.collectAsState().value 86 | 87 | val loadingState = viewModel.loadingState.collectAsState().value 88 | 89 | if (loadingState.isLoading == true) { 90 | ShowProgress() 91 | } 92 | 93 | state.coinDetail?.let { coin -> 94 | Box( 95 | Modifier 96 | .fillMaxSize() 97 | ) { 98 | val scroll = rememberScrollState(0) 99 | Header() 100 | Body(scroll, Html.fromHtml(coin.description.detail).toString()) 101 | Title(scroll.value, coin.name, coin.symbol) 102 | Image( 103 | imageUrl = coin.image.large, 104 | scroll = scroll.value 105 | ) 106 | } 107 | } 108 | } 109 | 110 | //From Jetsnack google docs 111 | @Composable 112 | fun CollapsableImageLayout( 113 | collapseFraction: Float, 114 | modifier: Modifier = Modifier, 115 | content: @Composable () -> Unit 116 | ) { 117 | Layout(modifier = modifier, content = content) { measurables, contraints -> 118 | check(measurables.size == 1) 119 | 120 | val imageMaxSize = min(ExpandedImageSize.roundToPx(), contraints.maxWidth) 121 | val imageMinSize = max(CollapsedImageSize.roundToPx(), contraints.minWidth) 122 | val imageWidth = lerp(imageMaxSize, imageMinSize, collapseFraction) 123 | val imagePlaceable = measurables[0].measure(Constraints.fixed(imageWidth, imageWidth)) 124 | 125 | val imageY = lerp(MinTitleOffset, MinImageOffset, collapseFraction).roundToPx() 126 | val imageX = lerp( 127 | (contraints.maxWidth - imageWidth) / 2, 128 | contraints.maxWidth - imageWidth, 129 | collapseFraction 130 | ) 131 | 132 | layout( 133 | width = contraints.maxWidth, 134 | height = imageY + imageWidth 135 | ) { 136 | imagePlaceable.placeRelative(imageX, imageY) 137 | } 138 | 139 | } 140 | } 141 | 142 | @Composable 143 | fun Image( 144 | imageUrl: String, 145 | scroll: Int 146 | ) { 147 | val collapseRange = with(LocalDensity.current) { 148 | (MaxTitleOffset - MinTitleOffset).toPx() 149 | } 150 | val collapseFraction = (scroll / collapseRange).coerceIn(0f, 1f) 151 | 152 | CollapsableImageLayout( 153 | collapseFraction = collapseFraction, 154 | modifier = HzPadding.then(Modifier.statusBarsPadding()) 155 | ) { 156 | Surface( 157 | color = Color.LightGray, 158 | shape = CircleShape 159 | ) { 160 | Image( 161 | painter = rememberImagePainter(data = imageUrl, 162 | builder = { 163 | crossfade(true) 164 | placeholder(drawableResId = R.drawable.ic_z_cash) 165 | }), 166 | contentDescription = null, 167 | modifier = Modifier.fillMaxSize(), 168 | contentScale = ContentScale.Crop 169 | ) 170 | 171 | } 172 | } 173 | } 174 | 175 | @Composable 176 | fun Header() { 177 | Spacer( 178 | modifier = Modifier 179 | .height(HeaderHeight) 180 | .fillMaxWidth() 181 | .background(Brush.horizontalGradient(listOf(Color.White, Color.DarkGray))) 182 | ) 183 | } 184 | 185 | @Composable 186 | fun Title(scroll: Int, name: String, symbol: String) { 187 | val maxOffSet = with(LocalDensity.current) { MaxTitleOffset.toPx() } 188 | val minOffSet = with(LocalDensity.current) { MinTitleOffset.toPx() } 189 | val offset = (maxOffSet - scroll).coerceAtLeast(minOffSet) 190 | 191 | Column( 192 | verticalArrangement = Arrangement.Bottom, 193 | modifier = Modifier 194 | .heightIn(min = TitleHeight) 195 | .statusBarsPadding() 196 | .fillMaxWidth() 197 | .graphicsLayer { translationY = offset } 198 | .background(color = MaterialTheme.colors.background) 199 | ) { 200 | Spacer(Modifier.height(DefaultPadding)) 201 | 202 | Text(text = name, modifier = HzPadding, style = MaterialTheme.typography.h4) 203 | 204 | Text(text = symbol.uppercase(), modifier = HzPadding, style = MaterialTheme.typography.h4) 205 | 206 | Spacer(Modifier.height(SmallSpace)) 207 | } 208 | } 209 | 210 | @Composable 211 | fun Body( 212 | scroll: ScrollState, 213 | detail: String 214 | ) { 215 | Column { 216 | Spacer( 217 | modifier = Modifier 218 | .fillMaxWidth() 219 | .statusBarsPadding() 220 | .height(MinTitleOffset) 221 | ) 222 | Column( 223 | modifier = Modifier.verticalScroll(scroll) 224 | ) { 225 | Spacer(Modifier.height(GradientScroll)) 226 | Surface(Modifier.fillMaxWidth()) { 227 | Column { 228 | Spacer(Modifier.height(ImageOverlap)) 229 | Spacer(Modifier.height(TitleHeight)) 230 | 231 | Spacer(Modifier.height(DefaultPadding)) 232 | Text( 233 | text = detail, 234 | modifier = HzPadding 235 | ) 236 | Spacer(Modifier.height(DefaultPadding)) 237 | } 238 | } 239 | } 240 | } 241 | } 242 | 243 | 244 | -------------------------------------------------------------------------------- /presentation/dashboard/src/main/java/com/enofeb/dashboard/home/detail/CoinDetailState.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.dashboard.home.detail 2 | 3 | import com.enofeb.core.data.market.coin.CoinDetail 4 | 5 | data class CoinDetailState( 6 | val coinDetail: CoinDetail? = null 7 | ) -------------------------------------------------------------------------------- /presentation/dashboard/src/main/java/com/enofeb/dashboard/home/detail/CoinDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.enofeb.dashboard.home.detail 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.viewModelScope 5 | import com.enofeb.core.base.BaseViewModel 6 | import com.enofeb.core.domain.market.MarketRepository 7 | import com.enofeb.core.state.ErrorState 8 | import com.enofeb.core.state.LoadingState 9 | import com.enofeb.dashboard.home.HomeState 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.flow.* 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class CoinDetailViewModel @Inject constructor( 16 | private val marketRepository: MarketRepository 17 | ) : BaseViewModel() { 18 | 19 | var id: String? = null 20 | 21 | private val _coinDetailState = MutableStateFlow(CoinDetailState()) 22 | 23 | val coinDetailState: StateFlow = _coinDetailState 24 | 25 | fun getCoinDetail() { 26 | id?.let { coinId -> 27 | marketRepository.getCoinDetail(coinId).onEach {coinDetail-> 28 | coinDetail?.let { 29 | _coinDetailState.value = CoinDetailState(it) 30 | } 31 | _loadingState.value = LoadingState((false)) 32 | }.catch { 33 | _errorState.value = ErrorState(it.message) 34 | }.onStart { 35 | _loadingState.value = LoadingState((true)) 36 | }.launchIn(viewModelScope) 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /presentation/dashboard/src/main/res/drawable/ic_calculation.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/dashboard/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/dashboard/src/main/res/drawable/ic_z_cash.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /presentation/dashboard/src/main/res/navigation/nav_dashboard_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /presentation/dashboard/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MainActivity 3 | Home 4 | Price 5 | Coin Detail 6 | 7 | -------------------------------------------------------------------------------- /presentation/dashboard/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |