├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-playstore.png │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── themes.xml │ │ │ │ └── strings.xml │ │ │ ├── anim │ │ │ │ ├── slide_in_left.xml │ │ │ │ ├── slide_in_right.xml │ │ │ │ ├── slide_in_top.xml │ │ │ │ ├── slide_out_left.xml │ │ │ │ ├── slide_out_right.xml │ │ │ │ ├── slide_out_top.xml │ │ │ │ ├── slide_in_bottom.xml │ │ │ │ └── slide_out_bottom.xml │ │ │ ├── drawable │ │ │ │ ├── bg_border_white.xml │ │ │ │ ├── bg_custom_round.xml │ │ │ │ ├── ic_arrow_up.xml │ │ │ │ ├── ic_arrow_down.xml │ │ │ │ ├── ic_arrow_right.xml │ │ │ │ ├── ic_email.xml │ │ │ │ ├── bg_theme_splash_screen.xml │ │ │ │ ├── ic_star.xml │ │ │ │ ├── ic_search.xml │ │ │ │ ├── ic_timelapse.xml │ │ │ │ ├── ic_password.xml │ │ │ │ ├── ic_default_user.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── ic_no_connection.xml │ │ │ │ ├── ic_app_logo.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── menu │ │ │ │ ├── menu_coin_detail.xml │ │ │ │ └── menu_coin_list.xml │ │ │ ├── layout │ │ │ │ ├── layout_profile.xml │ │ │ │ ├── layout_internet_connection.xml │ │ │ │ ├── fragment_favourite_coins.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── fragment_coin.xml │ │ │ │ ├── rv_item_favourite_coin.xml │ │ │ │ ├── fragment_register.xml │ │ │ │ ├── rv_item_coin.xml │ │ │ │ ├── fragment_login.xml │ │ │ │ └── fragment_coin_detail.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ └── navigation │ │ │ │ └── nav_graph.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── mburakcakir │ │ │ │ └── cryptopricetracker │ │ │ │ ├── data │ │ │ │ ├── model │ │ │ │ │ ├── High24h.kt │ │ │ │ │ ├── Low24h.kt │ │ │ │ │ ├── UserModel.kt │ │ │ │ │ ├── Description.kt │ │ │ │ │ ├── FavouriteCoinModel.kt │ │ │ │ │ ├── CurrentPrice.kt │ │ │ │ │ ├── Image.kt │ │ │ │ │ ├── MarketData.kt │ │ │ │ │ ├── CoinDetailItem.kt │ │ │ │ │ └── CoinMarketItem.kt │ │ │ │ ├── db │ │ │ │ │ ├── CryptoDatabase.kt │ │ │ │ │ ├── entity │ │ │ │ │ │ └── CoinMarketEntity.kt │ │ │ │ │ └── dao │ │ │ │ │ │ └── CryptoDao.kt │ │ │ │ ├── network │ │ │ │ │ └── CryptoService.kt │ │ │ │ └── repository │ │ │ │ │ ├── CoinRepository.kt │ │ │ │ │ └── CoinRepositoryImpl.kt │ │ │ │ ├── util │ │ │ │ ├── enums │ │ │ │ │ ├── EntryType.kt │ │ │ │ │ ├── Status.kt │ │ │ │ │ └── EntryState.kt │ │ │ │ ├── Result.kt │ │ │ │ ├── Resource.kt │ │ │ │ ├── SharedPreferences.kt │ │ │ │ ├── Constants.kt │ │ │ │ ├── ValidationUtils.kt │ │ │ │ ├── extension │ │ │ │ │ ├── BindingAdapter.kt │ │ │ │ │ └── Extension.kt │ │ │ │ ├── CoinRepositoryUtils.kt │ │ │ │ ├── EntryUtils.kt │ │ │ │ ├── NetworkControllerUtils.kt │ │ │ │ └── CoinUtils.kt │ │ │ │ ├── App.kt │ │ │ │ ├── ui │ │ │ │ ├── entry │ │ │ │ │ ├── EntryFormState.kt │ │ │ │ │ ├── CustomTextWatcher.kt │ │ │ │ │ ├── register │ │ │ │ │ │ ├── RegisterViewModel.kt │ │ │ │ │ │ └── RegisterFragment.kt │ │ │ │ │ ├── login │ │ │ │ │ │ ├── LoginViewModel.kt │ │ │ │ │ │ └── LoginFragment.kt │ │ │ │ │ └── EntryViewModel.kt │ │ │ │ ├── detail │ │ │ │ │ ├── CoinDetailViewState.kt │ │ │ │ │ ├── CoinDetailViewModel.kt │ │ │ │ │ └── CoinDetailFragment.kt │ │ │ │ ├── favourite │ │ │ │ │ ├── FavouriteCoinsViewState.kt │ │ │ │ │ ├── FavouriteCoinsViewModel.kt │ │ │ │ │ ├── FavouriteCoinsAdapter.kt │ │ │ │ │ └── FavouriteCoinsFragment.kt │ │ │ │ ├── BaseViewModel.kt │ │ │ │ ├── coin │ │ │ │ │ ├── CoinViewState.kt │ │ │ │ │ ├── CoinAdapter.kt │ │ │ │ │ ├── CoinViewModel.kt │ │ │ │ │ └── CoinFragment.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ └── MainActivity.kt │ │ │ │ └── di │ │ │ │ ├── DataModule.kt │ │ │ │ ├── RepositoryModule.kt │ │ │ │ ├── NetworkModule.kt │ │ │ │ └── DatabaseModule.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── mburakcakir │ │ │ └── cryptopricetracker │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── mburakcakir │ │ └── cryptopricetracker │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro ├── google-services.json └── build.gradle ├── settings.gradle ├── .idea ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── compiler.xml ├── vcs.xml ├── misc.xml ├── gradle.xml └── jarRepositories.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "CryptoPriceTracker" -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/High24h.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | data class High24h(val usd: Double) -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/Low24h.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | data class Low24h(val usd: Double) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mburakcakir/CryptoPriceTracker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/enums/EntryType.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util.enums 2 | 3 | enum class EntryType { 4 | LOGIN, 5 | REGISTER 6 | } -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/enums/Status.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util.enums 2 | 3 | enum class Status { 4 | SUCCESS, 5 | ERROR, 6 | LOADING 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/UserModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | data class UserModel( 4 | val email: String, 5 | val password: String, 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/enums/EntryState.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util.enums 2 | 3 | enum class EntryState { 4 | USERNAME, 5 | PASSWORD, 6 | EMAIL 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/App.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() { 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/Description.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Description( 6 | @SerializedName("en") 7 | val en: String, 8 | ) -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_border_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_custom_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 15 16:38:34 EET 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/Result.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | data class Result( 4 | val success: Int? = null, 5 | val error: Int? = null, 6 | val warning: Int? = null, 7 | val loading: Int? = null 8 | ) 9 | 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/java/com/mburakcakir/cryptopricetracker/data/model/FavouriteCoinModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | data class FavouriteCoinModel( 4 | val id: String, 5 | val image: String, 6 | val name: String, 7 | val symbol: String 8 | ) { 9 | constructor() : this("", "", "", "") 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/CurrentPrice.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class CurrentPrice( 6 | @SerializedName("try") 7 | val TRY: Double, 8 | 9 | @SerializedName("usd") 10 | val usd: Double, 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/entry/EntryFormState.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.entry 2 | 3 | data class EntryFormState( 4 | // val nameError: String? = null, 5 | // var usernameError: String? = null, 6 | var passwordError: String? = null, 7 | var emailError: String? = null, 8 | val isDataValid: Boolean = false 9 | ) -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_coin_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/Image.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Image( 6 | @SerializedName("large") 7 | val large: String, 8 | 9 | @SerializedName("small") 10 | val small: String, 11 | 12 | @SerializedName("thumb") 13 | val thumb: String 14 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_up.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/detail/CoinDetailViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.detail 2 | 3 | import android.view.View 4 | import com.mburakcakir.cryptopricetracker.util.enums.Status 5 | 6 | class CoinDetailViewState(private val status: Status) { 7 | fun getProgressBarVisibility() = if (status == Status.LOADING) View.VISIBLE else View.GONE 8 | fun getViewVisibility() = if (status == Status.SUCCESS) View.VISIBLE else View.GONE 9 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mburakcakir/cryptopricetracker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker 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 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/favourite/FavouriteCoinsViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.favourite 2 | 3 | import android.view.View 4 | import com.mburakcakir.cryptopricetracker.util.enums.Status 5 | 6 | class FavouriteCoinsViewState(private val status: Status) { 7 | fun getProgressBarVisibility() = if (status == Status.LOADING) View.VISIBLE else View.GONE 8 | fun getRecyclerViewVisibility() = if (status == Status.SUCCESS) View.VISIBLE else View.GONE 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/entry/CustomTextWatcher.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.entry 2 | 3 | import android.text.Editable 4 | import android.text.TextWatcher 5 | 6 | open class CustomTextWatcher : TextWatcher { 7 | 8 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} 9 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} 10 | override fun afterTextChanged(s: Editable?) {} 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_email.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.di 2 | 3 | import com.google.firebase.auth.FirebaseAuth 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object DataModule { 13 | 14 | @Provides 15 | @Singleton 16 | fun provideFirebaseAuth(): FirebaseAuth = FirebaseAuth.getInstance() 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_theme_splash_screen.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/db/CryptoDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.mburakcakir.cryptopricetracker.data.db.dao.CryptoDao 6 | import com.mburakcakir.cryptopricetracker.data.db.entity.CoinMarketEntity 7 | 8 | @Database( 9 | entities = [CoinMarketEntity::class], 10 | version = 2, 11 | exportSchema = false 12 | ) 13 | abstract class CryptoDatabase : RoomDatabase() { 14 | abstract fun cryptoDao(): CryptoDao 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.google.firebase.auth.FirebaseAuth 7 | import com.mburakcakir.cryptopricetracker.util.Result 8 | 9 | open class BaseViewModel : ViewModel() { 10 | 11 | val _result = MutableLiveData() 12 | val result: LiveData = _result 13 | 14 | val firebaseAuth = FirebaseAuth.getInstance() 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/coin/CoinViewState.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.coin 2 | 3 | import android.view.View 4 | import com.mburakcakir.cryptopricetracker.util.enums.Status 5 | 6 | class CoinViewState(private val status: Status) { 7 | fun getProgressBarVisibility() = if (status == Status.LOADING) View.VISIBLE else View.GONE 8 | fun getRecyclerViewVisibility() = if (status == Status.SUCCESS) View.VISIBLE else View.GONE 9 | fun getErrorMessageVisibility() = if (status == Status.ERROR) View.VISIBLE else View.GONE 10 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | import com.mburakcakir.cryptopricetracker.util.enums.Status 4 | 5 | sealed class Resource(val status: Status, val data: T?, val message: Throwable?) { 6 | 7 | class Loading : Resource(status = Status.LOADING, data = null, message = null) 8 | class Error(exception: Throwable) : 9 | Resource(status = Status.ERROR, data = null, message = exception) 10 | 11 | class Success(data: T?) : Resource(status = Status.SUCCESS, data = data, message = null) 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/network/CryptoService.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.network 2 | 3 | import com.mburakcakir.cryptopricetracker.data.model.CoinDetailItem 4 | import com.mburakcakir.cryptopricetracker.data.model.CoinMarketItem 5 | import retrofit2.Response 6 | import retrofit2.http.GET 7 | import retrofit2.http.Path 8 | 9 | interface CryptoService { 10 | @GET("coins/markets?vs_currency=usd") 11 | suspend fun getAllCoins(): Response> 12 | 13 | @GET("coins/{id}") 14 | suspend fun getCoinByID(@Path("id") id: String): Response 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timelapse.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.di 2 | 3 | import com.mburakcakir.cryptopricetracker.data.repository.CoinRepository 4 | import com.mburakcakir.cryptopricetracker.data.repository.CoinRepositoryImpl 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | abstract class RepositoryModule { 14 | 15 | @Binds 16 | @Singleton 17 | abstract fun provideCoinRepository(repository: CoinRepositoryImpl): CoinRepository 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/SharedPreferences.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | import android.content.Context 4 | 5 | class SharedPreferences(context: Context) { 6 | private var sharedPreferences = context.getSharedPreferences(Constants.PREF_NAME, 0) 7 | private val editor = sharedPreferences.edit() 8 | 9 | fun saveRefreshInterval(duration: Int) { 10 | editor.apply { 11 | putString(Constants.REFRESH_INTERVAL, duration.toString()) 12 | commit() 13 | } 14 | } 15 | 16 | fun getRefreshInterval(): String? { 17 | return sharedPreferences.getString(Constants.REFRESH_INTERVAL, null) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/db/entity/CoinMarketEntity.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.db.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | 7 | @Entity(tableName = "tbl_coin_list") 8 | data class CoinMarketEntity( 9 | @PrimaryKey 10 | val cryptoID: String, 11 | 12 | val currentPrice: Double, 13 | 14 | val highestPrice24h: Double, 15 | 16 | val cryptoImage: String, 17 | 18 | val lastUpdated: String, 19 | 20 | val lowestPrice24h: Double, 21 | 22 | val name: String, 23 | 24 | val priceChange24h: Double, 25 | 26 | val priceChangePercentage24h: Double, 27 | 28 | val symbol: String 29 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/db/dao/CryptoDao.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.mburakcakir.cryptopricetracker.data.db.entity.CoinMarketEntity 8 | 9 | @Dao 10 | interface CryptoDao { 11 | 12 | @Insert(onConflict = OnConflictStrategy.REPLACE) 13 | suspend fun insertAllCrypto(listCrypto: List) 14 | 15 | @Query("SELECT * FROM tbl_coin_list WHERE name LIKE :searchParameter OR symbol LIKE :searchParameter") 16 | suspend fun getCryptoByParameter(searchParameter: String): List 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/MarketData.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | data class MarketData( 5 | @SerializedName("current_price") 6 | val currentPrice: CurrentPrice, 7 | 8 | @SerializedName("last_updated") 9 | val lastUpdated: String, 10 | 11 | @SerializedName("price_change_24h") 12 | val priceChange24h: Double, 13 | 14 | @SerializedName("price_change_percentage_24h") 15 | val priceChangePercentage24h: Double, 16 | 17 | @SerializedName("high_24h") 18 | val highestPrice24h: High24h, 19 | 20 | @SerializedName("low_24h") 21 | val lowestPrice24h: Low24h, 22 | 23 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | object Constants { 4 | 5 | const val INVALID_PASSWORD = 6 | "Password must contain uppercase letters, lowercase letters, numbers and must be at least 6 digits." 7 | const val INVALID_EMAIL = "Email format does not match." 8 | 9 | const val DB_NAME = "crypto.db" 10 | const val BASE_COLLECTION_NAME = "Cryptocurrency" 11 | const val DETAIL_COLLECTION_NAME = "listFavouriteCrypto" 12 | const val PREF_NAME: String = "CryptoPriceTracker" 13 | const val REFRESH_INTERVAL: String = "REFRESH_INTERVAL" 14 | const val PASSWORD_PATTERN: String = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$" 15 | 16 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_password.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #134978 10 | #FFFFFFFF 11 | #FFC107 12 | #EDEDED 13 | #DFDFDF 14 | #838383 15 | #F44336 16 | #4CAF50 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/ValidationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | import android.util.Patterns 4 | import java.util.regex.Pattern 5 | 6 | class ValidationUtils { 7 | fun isEmailValid(email: String): Boolean { 8 | return if (email.contains('@')) { 9 | Patterns.EMAIL_ADDRESS.matcher(email).matches() 10 | } else { 11 | false 12 | } 13 | } 14 | 15 | fun isPasswordValid(password: String): Boolean { 16 | val textPattern: Pattern = Pattern.compile(Constants.PASSWORD_PATTERN) 17 | return textPattern.matcher(password).matches() && password.length > 5 18 | } 19 | 20 | fun isUserNameValid(username: String): Boolean { 21 | return username.length > 3 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/CoinDetailItem.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class CoinDetailItem( 6 | @SerializedName("id") 7 | val id: String, 8 | 9 | @SerializedName("name") 10 | val name: String, 11 | 12 | @SerializedName("symbol") 13 | val symbol: String, 14 | 15 | @SerializedName("description") 16 | val description: Description, 17 | 18 | @SerializedName("hashing_algorithm") 19 | val hashingAlgorithm: String?, 20 | 21 | @SerializedName("image") 22 | val image: Image, 23 | 24 | @SerializedName("market_data") 25 | val marketData: MarketData, 26 | 27 | @SerializedName("last_updated") 28 | val lastUpdated: String, 29 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/repository/CoinRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.repository 2 | 3 | import com.mburakcakir.cryptopricetracker.data.db.entity.CoinMarketEntity 4 | import com.mburakcakir.cryptopricetracker.data.model.CoinDetailItem 5 | import com.mburakcakir.cryptopricetracker.data.model.CoinMarketItem 6 | import com.mburakcakir.cryptopricetracker.util.Resource 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface CoinRepository { 10 | suspend fun getAllCoins(): Flow>> 11 | suspend fun getCoinByID(id: String): Flow> 12 | suspend fun insertAllCoins(listCrypto: List): Flow> 13 | suspend fun getCoinsByParameter(parameter: String): Flow>> 14 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/entry/register/RegisterViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.entry.register 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.viewModelScope 5 | import com.mburakcakir.cryptopricetracker.ui.entry.EntryViewModel 6 | import kotlinx.coroutines.launch 7 | 8 | class RegisterViewModel : EntryViewModel() { 9 | 10 | fun insertUser(email: String, password: String) = viewModelScope.launch { 11 | firebaseAuth.createUserWithEmailAndPassword(email, password) 12 | .addOnSuccessListener { 13 | _resultEntry.postValue(true) 14 | } 15 | .addOnFailureListener { exception -> 16 | _resultEntry.postValue(false) 17 | Log.v("errorLogin", exception.toString()) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/mburakcakir/cryptopricetracker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker 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.mburakcakir.cryptopricetracker", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_profile.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_coin_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/extension/BindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util.extension 2 | 3 | import android.widget.EditText 4 | import android.widget.ImageView 5 | import androidx.databinding.BindingAdapter 6 | import coil.load 7 | import com.mburakcakir.cryptopricetracker.R 8 | import com.mburakcakir.cryptopricetracker.util.afterTextChanged 9 | 10 | 11 | @BindingAdapter("loadImageFromUrl") 12 | fun ImageView.loadImage(imageUrl: String) { 13 | // Glide.with(context).load(imageUrl).into(this) 14 | this.load(imageUrl) 15 | } 16 | 17 | @BindingAdapter("setArrowBackground") 18 | fun ImageView.setBackground(number: Double) { 19 | this.setBackgroundResource(if (number > 0) R.drawable.ic_arrow_up else R.drawable.ic_arrow_down) 20 | } 21 | 22 | @BindingAdapter("afterTextChanged") 23 | fun EditText.afterEditTextChanged(onClick: () -> Unit) { 24 | this.afterTextChanged { 25 | onClick.invoke() 26 | } 27 | return 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/CoinRepositoryUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | import retrofit2.Response 4 | 5 | suspend fun getResourceByNetworkRequest(request: suspend () -> Response): Resource { 6 | try { 7 | val response = request() 8 | if (response.isSuccessful) { 9 | response.body()?.apply { 10 | return Resource.Success(this) 11 | } 12 | } 13 | } catch (e: Exception) { 14 | e.printStackTrace() 15 | return Resource.Error(e) 16 | } 17 | 18 | return Resource.Loading() 19 | } 20 | 21 | suspend fun getResourceByDatabaseRequest(request: suspend () -> T): Resource { 22 | try { 23 | val result = request() 24 | result?.let { 25 | return Resource.Success(result) 26 | } 27 | } catch (e: Exception) { 28 | e.printStackTrace() 29 | return Resource.Error(e) 30 | } 31 | return Resource.Loading() 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.di 2 | 3 | import com.mburakcakir.cryptopricetracker.BuildConfig 4 | import com.mburakcakir.cryptopricetracker.data.network.CryptoService 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object NetworkModule { 16 | 17 | @Provides 18 | @Singleton 19 | fun provideRetrofit(): Retrofit = 20 | Retrofit.Builder() 21 | .addConverterFactory(GsonConverterFactory.create()) 22 | .baseUrl(BuildConfig.API_URL) 23 | .build() 24 | 25 | 26 | @Provides 27 | @Singleton 28 | fun provideCryptoService(retrofit: Retrofit): CryptoService = 29 | retrofit.create(CryptoService::class.java) 30 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/model/CoinMarketItem.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.model 2 | 3 | import android.os.Parcelable 4 | import com.google.gson.annotations.SerializedName 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Parcelize 8 | data class CoinMarketItem( 9 | @SerializedName("id") 10 | val cryptoID: String, 11 | 12 | @SerializedName("current_price") 13 | val currentPrice: Double, 14 | 15 | @SerializedName("high_24h") 16 | val highestPrice24h: Double, 17 | 18 | @SerializedName("image") 19 | val cryptoImage: String, 20 | 21 | @SerializedName("last_updated") 22 | val lastUpdated: String, 23 | 24 | @SerializedName("low_24h") 25 | val lowestPrice24h: Double, 26 | 27 | @SerializedName("name") 28 | val name: String, 29 | 30 | @SerializedName("price_change_24h") 31 | val priceChange24h: Double, 32 | 33 | @SerializedName("price_change_percentage_24h") 34 | val priceChangePercentage24h: Double, 35 | 36 | @SerializedName("symbol") 37 | val symbol: String, 38 | 39 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.mburakcakir.cryptopricetracker.data.db.CryptoDatabase 6 | import com.mburakcakir.cryptopricetracker.data.db.dao.CryptoDao 7 | import com.mburakcakir.cryptopricetracker.util.Constants 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.qualifiers.ApplicationContext 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object DatabaseModule { 18 | 19 | @Provides 20 | @Singleton 21 | fun provideDatabase(@ApplicationContext context: Context): CryptoDatabase = 22 | Room.databaseBuilder(context, CryptoDatabase::class.java, Constants.DB_NAME) 23 | .fallbackToDestructiveMigration() 24 | .build() 25 | 26 | @Provides 27 | @Singleton 28 | fun provideCryptoDao(database: CryptoDatabase): CryptoDao = database.cryptoDao() 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | import androidx.fragment.app.Fragment 10 | 11 | //abstract class BaseFragment : Fragment() { 12 | abstract class BaseFragment : Fragment() { 13 | 14 | private var _binding: T? = null 15 | protected val binding get() = _binding!! 16 | 17 | override fun onCreateView( 18 | inflater: LayoutInflater, 19 | container: ViewGroup?, 20 | savedInstanceState: Bundle? 21 | ): View { 22 | _binding = DataBindingUtil.inflate(inflater, getFragmentView(), container, false) 23 | return binding!!.root 24 | } 25 | 26 | override fun onDestroyView() { 27 | super.onDestroyView() 28 | _binding = null 29 | } 30 | 31 | abstract fun getFragmentView(): Int 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/extension/Extension.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | import android.content.Context 4 | import android.text.Editable 5 | import android.widget.EditText 6 | import android.widget.Toast 7 | import androidx.fragment.app.Fragment 8 | import androidx.navigation.NavDirections 9 | import androidx.navigation.fragment.findNavController 10 | import com.mburakcakir.cryptopricetracker.ui.entry.CustomTextWatcher 11 | 12 | infix fun Context.toast(message: String) { 13 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 14 | } 15 | 16 | infix fun Fragment.navigate(navDirections: NavDirections) { 17 | findNavController().navigate(navDirections) 18 | } 19 | 20 | fun Double.format(digits: Int) = "%.${digits}f".format(this) 21 | 22 | fun String.format() = "%$this%" 23 | 24 | fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { 25 | this.addTextChangedListener(object : CustomTextWatcher() { 26 | override fun afterTextChanged(editable: Editable?) { 27 | afterTextChanged.invoke(editable.toString()) 28 | } 29 | }) 30 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 8dp 5 | 16dp 6 | 24dp 7 | 8 | 12sp 9 | 14sp 10 | 16sp 11 | 18sp 12 | 13 | 20dp 14 | 15 | 16dp 16 | 8dp 17 | 18 | 1dp 19 | 3dp 20 | 21 | 6dp 22 | 23 | 150dp 24 | 30dp 25 | 26 | 50dp 27 | 28 | 200dp 29 | 32dp 30 | 48dp 31 | 32dp 32 | -------------------------------------------------------------------------------- /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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/EntryUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | import android.content.Context 4 | import android.content.DialogInterface 5 | import android.content.Intent 6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 7 | import com.mburakcakir.cryptopricetracker.R 8 | 9 | fun Context.verifyEmail() { 10 | val title = getString(R.string.warning) 11 | val message = getString(R.string.check_email_address) 12 | val check = getString(R.string.check) 13 | val discard = getString(R.string.discard) 14 | val packageName = getString(R.string.gmail_package_name) 15 | 16 | MaterialAlertDialogBuilder(this) 17 | .setTitle(title) 18 | .setMessage(message) 19 | .setCancelable(false) 20 | .setPositiveButton(check) { dialogInterface: DialogInterface, i: Int -> 21 | val intent = packageManager.getLaunchIntentForPackage(packageName) 22 | openGmail(intent) 23 | } 24 | 25 | .setNegativeButton(discard) { dialogInterface: DialogInterface, i: Int -> 26 | dialogInterface.dismiss() 27 | } 28 | .show() 29 | } 30 | 31 | fun Context.openGmail(intent: Intent?) { 32 | if (intent != null) 33 | startActivity(intent) 34 | else 35 | this toast getString(R.string.error_install_gmail) 36 | } -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "860362525295", 4 | "project_id": "cryptopricetracker-1d7b4", 5 | "storage_bucket": "cryptopricetracker-1d7b4.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:860362525295:android:0156654c8e1245ffdb9b9a", 11 | "android_client_info": { 12 | "package_name": "com.mburakcakir.cryptopricetracker" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "860362525295-lvnkd9khegmri951q039klm0ffnnhdnc.apps.googleusercontent.com", 18 | "client_type": 1, 19 | "android_info": { 20 | "package_name": "com.mburakcakir.cryptopricetracker", 21 | "certificate_hash": "2f904fb09c9a434ec53c2b62784c146c60cc36d9" 22 | } 23 | }, 24 | { 25 | "client_id": "860362525295-lrss1v526vnalen64qifcdpaioe0ircf.apps.googleusercontent.com", 26 | "client_type": 3 27 | } 28 | ], 29 | "api_key": [ 30 | { 31 | "current_key": "AIzaSyDAliFUVU4N6qUzuLKoT3Q4bKPC_ejmZCI" 32 | } 33 | ], 34 | "services": { 35 | "appinvite_service": { 36 | "other_platform_oauth_client": [ 37 | { 38 | "client_id": "860362525295-lrss1v526vnalen64qifcdpaioe0ircf.apps.googleusercontent.com", 39 | "client_type": 3 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | ], 46 | "configuration_version": "1" 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/entry/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.entry.login 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import com.mburakcakir.cryptopricetracker.ui.entry.EntryViewModel 8 | import kotlinx.coroutines.launch 9 | 10 | class LoginViewModel : EntryViewModel() { 11 | 12 | private val _isVerifiedSent = MutableLiveData() 13 | val isVerifiedSent: LiveData = _isVerifiedSent 14 | 15 | fun login(email: String, password: String) = viewModelScope.launch { 16 | firebaseAuth.signInWithEmailAndPassword(email, password) 17 | .addOnSuccessListener { 18 | _resultEntry.postValue(true) 19 | }.addOnFailureListener { exception -> 20 | _resultEntry.postValue(false) 21 | Log.v("errorLogin", exception.toString()) 22 | } 23 | } 24 | 25 | fun checkIfUserVerified(): Boolean { 26 | firebaseAuth.currentUser?.let { 27 | return it.isEmailVerified 28 | } 29 | return false 30 | } 31 | 32 | fun sendEmailVerify() { 33 | firebaseAuth.currentUser?.let { 34 | it.sendEmailVerification() 35 | .addOnSuccessListener { 36 | _isVerifiedSent.postValue(true) 37 | } 38 | 39 | .addOnFailureListener { 40 | _isVerifiedSent.postValue(false) 41 | } 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_internet_connection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 18 | 26 | 27 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_favourite_coins.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 17 | 18 | 21 | 22 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/NetworkControllerUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import android.net.Network 6 | import android.net.NetworkRequest 7 | import androidx.lifecycle.MutableLiveData 8 | 9 | class NetworkControllerUtils(context: Context) { 10 | 11 | private var _isNetworkConnected = MutableLiveData() 12 | var isNetworkConnected = _isNetworkConnected 13 | 14 | private val connectivityManager = 15 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 16 | private val networkCallback = object : ConnectivityManager.NetworkCallback() { 17 | 18 | override fun onAvailable(network: Network) { 19 | _isNetworkConnected.postValue(true) 20 | } 21 | 22 | override fun onLost(network: Network) { 23 | _isNetworkConnected.postValue(false) 24 | } 25 | } 26 | 27 | fun startNetworkCallback() { 28 | val builder = NetworkRequest.Builder() 29 | 30 | // API 24 and above (API 24 ve yukarısı) 31 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 32 | connectivityManager.registerDefaultNetworkCallback(networkCallback) 33 | } 34 | // API 23 ve below (API 23 ve aşağısı) 35 | else { 36 | connectivityManager.registerNetworkCallback( 37 | builder.build(), networkCallback 38 | ) 39 | } 40 | } 41 | 42 | fun stopNetworkCallback() { 43 | connectivityManager.unregisterNetworkCallback(ConnectivityManager.NetworkCallback()) 44 | } 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 22 | 23 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/data/repository/CoinRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.data.repository 2 | 3 | import com.mburakcakir.cryptopricetracker.data.db.dao.CryptoDao 4 | import com.mburakcakir.cryptopricetracker.data.db.entity.CoinMarketEntity 5 | import com.mburakcakir.cryptopricetracker.data.model.CoinDetailItem 6 | import com.mburakcakir.cryptopricetracker.data.model.CoinMarketItem 7 | import com.mburakcakir.cryptopricetracker.data.network.CryptoService 8 | import com.mburakcakir.cryptopricetracker.util.Resource 9 | import com.mburakcakir.cryptopricetracker.util.getResourceByDatabaseRequest 10 | import com.mburakcakir.cryptopricetracker.util.getResourceByNetworkRequest 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.flow 13 | import javax.inject.Inject 14 | 15 | class CoinRepositoryImpl @Inject constructor( 16 | private val cryptoService: CryptoService, 17 | private val cryptoDao: CryptoDao 18 | ) : CoinRepository { 19 | 20 | override suspend fun getAllCoins(): Flow>> = flow { 21 | emit(getResourceByNetworkRequest { cryptoService.getAllCoins() }) 22 | } 23 | 24 | override suspend fun getCoinByID(id: String): Flow> = flow { 25 | emit(getResourceByNetworkRequest { cryptoService.getCoinByID(id) }) 26 | } 27 | 28 | override suspend fun insertAllCoins(listCrypto: List): Flow> = 29 | flow { 30 | emit(getResourceByDatabaseRequest { cryptoDao.insertAllCrypto(listCrypto) }) 31 | } 32 | 33 | override suspend fun getCoinsByParameter(parameter: String): Flow>> = 34 | flow { 35 | emit(getResourceByDatabaseRequest { cryptoDao.getCryptoByParameter(parameter) }) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/favourite/FavouriteCoinsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.favourite 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import com.google.firebase.firestore.ktx.firestore 7 | import com.google.firebase.ktx.Firebase 8 | import com.mburakcakir.cryptopricetracker.data.model.FavouriteCoinModel 9 | import com.mburakcakir.cryptopricetracker.ui.BaseViewModel 10 | import com.mburakcakir.cryptopricetracker.util.Constants 11 | 12 | class FavouriteCoinsViewModel : BaseViewModel() { 13 | 14 | private val _favouriteCoins = MutableLiveData>() 15 | val favouriteCoins: LiveData> = _favouriteCoins 16 | 17 | private val _coinState = MutableLiveData() 18 | val coinState: LiveData = _coinState 19 | 20 | private val favouriteCoinsList: MutableList = mutableListOf() 21 | 22 | private val db = Firebase.firestore 23 | .collection(Constants.BASE_COLLECTION_NAME) 24 | .document(firebaseAuth.currentUser.uid) 25 | .collection(Constants.DETAIL_COLLECTION_NAME) 26 | 27 | fun getAllFavourites() { 28 | db.get() 29 | .addOnSuccessListener { document -> 30 | val list = document.documents 31 | list.forEach { 32 | val coinMarketItem = it.toObject(FavouriteCoinModel::class.java) 33 | coinMarketItem?.let { favouriteCoinModel -> 34 | favouriteCoinsList.add(favouriteCoinModel) 35 | } 36 | } 37 | _coinState.value = true 38 | _favouriteCoins.value = favouriteCoinsList 39 | } 40 | .addOnFailureListener { exception -> 41 | _coinState.value = false 42 | Log.v("exceptionFavourites", exception.toString()) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/coin/CoinAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.coin 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.mburakcakir.cryptopricetracker.data.model.CoinMarketItem 9 | import com.mburakcakir.cryptopricetracker.databinding.RvItemCoinBinding 10 | 11 | class CoinAdapter : ListAdapter(CoinCallback()) { 12 | 13 | private lateinit var coinOnClick: (CoinMarketItem) -> Unit 14 | 15 | fun setCoinOnClickListener(coinOnClick: (CoinMarketItem) -> Unit) { 16 | this.coinOnClick = coinOnClick 17 | } 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoinViewHolder = 20 | CoinViewHolder( 21 | RvItemCoinBinding.inflate( 22 | LayoutInflater.from(parent.context), 23 | parent, 24 | false 25 | ), coinOnClick 26 | ) 27 | 28 | override fun onBindViewHolder(holder: CoinViewHolder, position: Int) = 29 | holder.bind(getItem(position)) 30 | 31 | } 32 | 33 | class CoinViewHolder( 34 | private val binding: RvItemCoinBinding, 35 | private val coinOnClick: (CoinMarketItem) -> Unit 36 | ) : RecyclerView.ViewHolder(binding.root) { 37 | fun bind(coinMarketItem: CoinMarketItem) { 38 | binding.coin = coinMarketItem 39 | 40 | itemView.setOnClickListener { 41 | coinOnClick(coinMarketItem) 42 | } 43 | } 44 | } 45 | 46 | class CoinCallback : DiffUtil.ItemCallback() { 47 | override fun areItemsTheSame( 48 | oldItem: CoinMarketItem, 49 | newItem: CoinMarketItem 50 | ): Boolean = oldItem == newItem 51 | 52 | override fun areContentsTheSame( 53 | oldItem: CoinMarketItem, 54 | newItem: CoinMarketItem 55 | ): Boolean = oldItem == newItem 56 | 57 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 29 | 30 | 31 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/favourite/FavouriteCoinsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.favourite 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.mburakcakir.cryptopricetracker.data.model.FavouriteCoinModel 9 | import com.mburakcakir.cryptopricetracker.databinding.RvItemFavouriteCoinBinding 10 | 11 | class FavouriteCoinsAdapter : 12 | ListAdapter(CoinCallback()) { 13 | 14 | private lateinit var coinOnClick: (String) -> Unit 15 | 16 | fun setCoinOnClickListener(coinOnClick: (String) -> Unit) { 17 | this.coinOnClick = coinOnClick 18 | } 19 | 20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavouriteCoinViewHolder = 21 | FavouriteCoinViewHolder( 22 | RvItemFavouriteCoinBinding.inflate( 23 | LayoutInflater.from(parent.context), 24 | parent, 25 | false 26 | ), coinOnClick 27 | ) 28 | 29 | override fun onBindViewHolder(holder: FavouriteCoinViewHolder, position: Int) = 30 | holder.bind(getItem(position)) 31 | 32 | } 33 | 34 | class FavouriteCoinViewHolder( 35 | private val binding: RvItemFavouriteCoinBinding, 36 | private val coinOnClick: (String) -> Unit 37 | ) : RecyclerView.ViewHolder(binding.root) { 38 | fun bind(favouriteCoinModel: FavouriteCoinModel) { 39 | binding.coin = favouriteCoinModel 40 | 41 | itemView.setOnClickListener { 42 | coinOnClick(favouriteCoinModel.id) 43 | } 44 | } 45 | } 46 | 47 | class CoinCallback : DiffUtil.ItemCallback() { 48 | override fun areItemsTheSame( 49 | oldItem: FavouriteCoinModel, 50 | newItem: FavouriteCoinModel 51 | ): Boolean = oldItem == newItem 52 | 53 | override fun areContentsTheSame( 54 | oldItem: FavouriteCoinModel, 55 | newItem: FavouriteCoinModel 56 | ): Boolean = oldItem == newItem 57 | 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import android.view.View 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.navigation.NavController 8 | import androidx.navigation.findNavController 9 | import androidx.navigation.ui.AppBarConfiguration 10 | import androidx.navigation.ui.onNavDestinationSelected 11 | import androidx.navigation.ui.setupActionBarWithNavController 12 | import com.mburakcakir.cryptopricetracker.R 13 | import com.mburakcakir.cryptopricetracker.databinding.ActivityMainBinding 14 | import dagger.hilt.android.AndroidEntryPoint 15 | 16 | @AndroidEntryPoint 17 | class MainActivity : AppCompatActivity() { 18 | 19 | private lateinit var binding: ActivityMainBinding 20 | private lateinit var navController: NavController 21 | private lateinit var appBarConfiguration: AppBarConfiguration 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | setTheme(R.style.AppTheme) 26 | binding = ActivityMainBinding.inflate(layoutInflater) 27 | setContentView(binding.root) 28 | 29 | navController = findNavController(R.id.nav_host_fragment) 30 | appBarConfiguration = AppBarConfiguration(navController.graph) 31 | 32 | navController.addOnDestinationChangedListener { _, destination, _ -> 33 | if (destination.id == R.id.loginFragment || destination.id == R.id.registerFragment) { 34 | binding.toolbar.visibility = View.GONE 35 | } else { 36 | binding.toolbar.visibility = View.VISIBLE 37 | } 38 | } 39 | 40 | setSupportActionBar(binding.toolbar) 41 | setupActionBarWithNavController(navController) 42 | } 43 | 44 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 45 | return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item) 46 | } 47 | 48 | override fun onSupportNavigateUp(): Boolean { 49 | return navController.navigateUp() || super.onSupportNavigateUp() 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/favourite/FavouriteCoinsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.favourite 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.viewModels 6 | import com.mburakcakir.cryptopricetracker.R 7 | import com.mburakcakir.cryptopricetracker.databinding.FragmentFavouriteCoinsBinding 8 | import com.mburakcakir.cryptopricetracker.ui.BaseFragment 9 | import com.mburakcakir.cryptopricetracker.util.enums.Status 10 | import com.mburakcakir.cryptopricetracker.util.navigate 11 | import dagger.hilt.android.AndroidEntryPoint 12 | 13 | @AndroidEntryPoint 14 | class FavouriteCoinsFragment : BaseFragment() { 15 | 16 | private var favouriteCoinAdapter = FavouriteCoinsAdapter() 17 | private val favouriteCoinViewModel: FavouriteCoinsViewModel by viewModels() 18 | 19 | override fun getFragmentView(): Int = R.layout.fragment_favourite_coins 20 | 21 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 22 | super.onViewCreated(view, savedInstanceState) 23 | init() 24 | } 25 | 26 | private fun init() { 27 | 28 | setRecyclerView() 29 | 30 | observeCoins() 31 | } 32 | 33 | private fun setRecyclerView() { 34 | binding.state = FavouriteCoinsViewState(Status.LOADING) 35 | 36 | binding.rvFavouriteCoinList.adapter = favouriteCoinAdapter 37 | 38 | favouriteCoinAdapter.setCoinOnClickListener { 39 | this.navigate( 40 | FavouriteCoinsFragmentDirections.actionFavouriteCoinsFragmentToCoinDetailFragment( 41 | it 42 | ) 43 | ) 44 | } 45 | } 46 | 47 | 48 | private fun observeCoins() { 49 | favouriteCoinViewModel.getAllFavourites() 50 | 51 | favouriteCoinViewModel.favouriteCoins.observe(viewLifecycleOwner) { 52 | favouriteCoinAdapter.submitList(it) 53 | } 54 | 55 | favouriteCoinViewModel.coinState.observe(viewLifecycleOwner) { 56 | val status = if (it) Status.SUCCESS else Status.ERROR 57 | binding.state = FavouriteCoinsViewState(status) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_coin.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 19 | 20 | 26 | 27 | 34 | 35 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/util/CoinUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.util 2 | 3 | import com.mburakcakir.cryptopricetracker.data.db.entity.CoinMarketEntity 4 | import com.mburakcakir.cryptopricetracker.data.model.CoinDetailItem 5 | import com.mburakcakir.cryptopricetracker.data.model.CoinMarketItem 6 | 7 | 8 | fun String.formatUpdatedTime(): String { 9 | val year = substring(0, 4) 10 | val month = substring(5, 7) 11 | val day = substring(8, 10) 12 | val hour = substring(11, 13) 13 | val minute = substring(14, 16) 14 | val second = substring(17, 19) 15 | return "$day-$month-$year, ${Integer.parseInt(hour) + 3}:$minute:$second" 16 | } 17 | 18 | fun Number.formatPriceChange(): Double { 19 | return String.format("%.2f", this).replace(",", ".").toDouble() 20 | } 21 | 22 | fun setFavouriteMessage(isFavourite: Boolean): String { 23 | return if (isFavourite) "Added" else "Removed" 24 | } 25 | 26 | fun setCoinDetail(coinDetails: CoinDetailItem): CoinDetailItem { 27 | val lastUpdated = coinDetails.lastUpdated.formatUpdatedTime() 28 | val priceChange24h = coinDetails.marketData.priceChange24h.formatPriceChange() 29 | val priceChangePercentage24h = 30 | coinDetails.marketData.priceChangePercentage24h.formatPriceChange() 31 | val hashingAlgorithm = coinDetails.hashingAlgorithm ?: "-" 32 | 33 | val marketData = coinDetails.marketData.copy( 34 | priceChange24h = priceChange24h, 35 | priceChangePercentage24h = priceChangePercentage24h 36 | ) 37 | 38 | val copiedDetail = coinDetails.copy( 39 | lastUpdated = lastUpdated, 40 | marketData = marketData, 41 | hashingAlgorithm = hashingAlgorithm 42 | ) 43 | 44 | return copiedDetail 45 | 46 | } 47 | 48 | fun getCoinMarketEntity(coinMarketItemList: List): MutableList { 49 | val databaseList = mutableListOf() 50 | coinMarketItemList.forEach { 51 | val coinMarketEntity = CoinMarketEntity( 52 | it.cryptoID, 53 | it.currentPrice, 54 | it.highestPrice24h, 55 | it.cryptoImage, 56 | it.lastUpdated, 57 | it.lowestPrice24h, 58 | it.name, 59 | it.priceChange24h, 60 | it.priceChangePercentage24h, 61 | it.symbol 62 | ) 63 | databaseList.add(coinMarketEntity) 64 | } 65 | return databaseList 66 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_default_user.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/entry/EntryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.entry 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.mburakcakir.cryptopricetracker.ui.BaseViewModel 6 | import com.mburakcakir.cryptopricetracker.util.Constants 7 | import com.mburakcakir.cryptopricetracker.util.ValidationUtils 8 | import com.mburakcakir.cryptopricetracker.util.enums.EntryState 9 | import com.mburakcakir.cryptopricetracker.util.enums.EntryType 10 | 11 | open class EntryViewModel : BaseViewModel() { 12 | private val _entryForm = MutableLiveData() 13 | val entryFormState: LiveData = _entryForm 14 | private val _errorPassword = MutableLiveData("") 15 | private val _errorEmail = MutableLiveData("") 16 | 17 | private lateinit var entryType: EntryType 18 | private var typeList: MutableList = mutableListOf() 19 | 20 | val _resultEntry = MutableLiveData() 21 | val resultEntry: LiveData = _resultEntry 22 | 23 | init { 24 | _entryForm.value = EntryFormState() 25 | } 26 | 27 | fun isDataChanged( 28 | entryState: EntryState, 29 | text: String 30 | ) { 31 | when (entryState) { 32 | EntryState.EMAIL -> { 33 | _errorEmail.value = 34 | if (!isEmailValid(text)) Constants.INVALID_EMAIL 35 | else null 36 | } 37 | 38 | EntryState.PASSWORD -> { 39 | _errorPassword.value = 40 | if (!isPasswordValid(text)) Constants.INVALID_PASSWORD 41 | else null 42 | } 43 | 44 | } 45 | setEntryParameters() 46 | setEntryFormState() 47 | } 48 | 49 | private fun setEntryFormState() { 50 | _entryForm.value = EntryFormState( 51 | emailError = _errorEmail.value, 52 | passwordError = _errorPassword.value, 53 | isDataValid = isDataValid() 54 | ) 55 | 56 | } 57 | 58 | private fun isDataValid() = 59 | mutableListOf().apply { 60 | for (item in typeList) 61 | item?.let { this.add(it) } 62 | }.size == 0 63 | 64 | private fun setEntryParameters() { 65 | typeList = when (entryType) { 66 | EntryType.LOGIN -> mutableListOf(_errorEmail.value, _errorPassword.value) 67 | EntryType.REGISTER -> mutableListOf(_errorEmail.value, _errorPassword.value) 68 | } 69 | } 70 | 71 | fun setEntryType(entryType: EntryType) { 72 | this.entryType = entryType 73 | } 74 | 75 | private fun isPasswordValid(text: String) = ValidationUtils().isPasswordValid(text) 76 | private fun isEmailValid(text: String) = ValidationUtils().isEmailValid(text) 77 | } -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 16 | 21 | 22 | 27 | 30 | 31 | 32 | 37 | 40 | 45 | 46 | 47 | 52 | 57 | 58 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/entry/register/RegisterFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.entry.register 2 | 3 | import android.os.Bundle 4 | import android.text.Editable 5 | import android.view.View 6 | import android.widget.EditText 7 | import androidx.fragment.app.viewModels 8 | import com.mburakcakir.cryptopricetracker.R 9 | import com.mburakcakir.cryptopricetracker.databinding.FragmentRegisterBinding 10 | import com.mburakcakir.cryptopricetracker.ui.BaseFragment 11 | import com.mburakcakir.cryptopricetracker.ui.entry.CustomTextWatcher 12 | import com.mburakcakir.cryptopricetracker.util.enums.EntryState 13 | import com.mburakcakir.cryptopricetracker.util.enums.EntryType 14 | import com.mburakcakir.cryptopricetracker.util.navigate 15 | import com.mburakcakir.cryptopricetracker.util.toast 16 | import dagger.hilt.android.AndroidEntryPoint 17 | 18 | @AndroidEntryPoint 19 | class RegisterFragment : BaseFragment() { 20 | private val registerViewModel: RegisterViewModel by viewModels() 21 | 22 | override fun getFragmentView(): Int = R.layout.fragment_register 23 | 24 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 25 | super.onViewCreated(view, savedInstanceState) 26 | init() 27 | } 28 | 29 | private fun init() { 30 | 31 | checkInputAndClick() 32 | 33 | observeData() 34 | } 35 | 36 | private fun checkInputAndClick() { 37 | registerViewModel.setEntryType(EntryType.REGISTER) 38 | binding.lifecycleOwner = viewLifecycleOwner 39 | binding.registerViewModel = registerViewModel 40 | 41 | binding.edtMail.afterTextChanged { 42 | registerViewModel.isDataChanged( 43 | EntryState.EMAIL, 44 | binding.edtMail.text.toString() 45 | ) 46 | } 47 | } 48 | 49 | private fun observeData() { 50 | registerViewModel.entryFormState.observe(viewLifecycleOwner, { 51 | binding.btnRegister.isEnabled = it.isDataValid 52 | 53 | if (it.emailError.isNullOrEmpty().not()) 54 | binding.edtMail.error = it.emailError 55 | 56 | if (!it.passwordError.isNullOrEmpty().not()) 57 | binding.edtPassword.error = it.passwordError 58 | 59 | }) 60 | 61 | registerViewModel.resultEntry.observe(viewLifecycleOwner) { 62 | val resultMessage = if (it) { 63 | this.navigate(RegisterFragmentDirections.actionRegisterFragmentToLoginFragment()) 64 | getString(R.string.register_success) 65 | } else { 66 | getString(R.string.register_error) 67 | } 68 | 69 | requireContext() toast resultMessage 70 | } 71 | } 72 | 73 | private fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { 74 | this.addTextChangedListener(object : CustomTextWatcher() { 75 | override fun afterTextChanged(editable: Editable?) { 76 | afterTextChanged.invoke(editable.toString()) 77 | } 78 | }) 79 | } 80 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Crypto Price Tracker](https://github.com/mburakcakir/CryptoPriceTracker/tree/master/app/src/main/java/com/mburakcakir/cryptopricetracker) 2 | 3 | ## Purpose 4 | You can search for coins, track realtime coin prices, review the coins and add them to your favorites. 5 | ## Screenshot 6 |

7 | 8 |

9 | 10 | ## Libraries and tools 🛠 11 |
  • ViewModel
  • 12 |
  • LiveData
  • 13 |
  • Navigation
  • 14 |
  • Room
  • 15 |
  • Retrofit
  • 16 |
  • Lifecycle
  • 17 |
  • DataBinding
  • 18 |
  • Coroutines
  • 19 |
  • Hilt
  • 20 |
  • Firebase Authentication & Cloud Firestore
  • 21 |
  • Glide & Coil
  • 22 |
  • Material Design
  • 23 | 24 | ## Live Tracking Features 25 | 26 |   Live Coin Tracking        Live Input Tracking        Live Internet Tracking 27 | 28 |

    29 |     30 |     31 |     32 |

    33 | 34 | ## Architecture 35 | The app uses MVVM architecture to have a unidirectional flow of data, separation of concern, testability, and a lot more. 36 | 37 | ![Architecture](https://developer.android.com/topic/libraries/architecture/images/final-architecture.png) 38 | 39 | License 40 | -------- 41 | 42 | 43 | Copyright 2021 Muhammed Burak Çakır. 44 | 45 | Licensed under the Apache License, Version 2.0 (the "License"); 46 | you may not use this file except in compliance with the License. 47 | You may obtain a copy of the License at 48 | 49 | http://www.apache.org/licenses/LICENSE-2.0 50 | 51 | Unless required by applicable law or agreed to in writing, software 52 | distributed under the License is distributed on an "AS IS" BASIS, 53 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 54 | See the License for the specific language governing permissions and 55 | limitations under the License. 56 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CryptoPriceTracker 3 | 4 | 5 | Welcome 6 | Login with your email 7 | Not a member? 8 | Sign Up Now 9 | LoginActivity 10 | Email 11 | Password 12 | "Welcome!" 13 | Login Again 14 | Logging.. 15 | User not found 16 | Login successful 17 | An error occurred in the user registration. 18 | Login 19 | Register 20 | 21 | 22 | Registering.. 23 | User already registered 24 | Registration successful 25 | Registering.. 26 | 27 | Email 28 | Password 29 | 30 | 31 | Saving.. 32 | Saved 33 | Failed to save 34 | Refresh time(sec) 35 | Description 36 | Hashing Algorithm 37 | Highest(24h): 38 | Lowest(24h): 39 | Refresh Interval (sec) 40 | Search a coin 41 | Coin Name 42 | Coin Price Change 43 | Coin Symbol 44 | Coin Current Price 45 | 46 | 47 | Successfully added to favorites. 48 | Successfully removed from favorites. 49 | Failed to add. 50 | 51 | 52 | Email address not found. 53 | We sent you a validation.\nCheck your email to verify your address. 54 | WARNING 55 | CHECK 56 | DISCARD 57 | Please Install Gmail 58 | com.google.android.gm 59 | Loading 60 | Success 61 | Error 62 | Error Data 63 | Internet disconnected.\nThe problem may be caused by mobile data. Connect with Wi-Fi. 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/coin/CoinViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.coin 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import com.mburakcakir.cryptopricetracker.R 8 | import com.mburakcakir.cryptopricetracker.data.db.entity.CoinMarketEntity 9 | import com.mburakcakir.cryptopricetracker.data.model.CoinMarketItem 10 | import com.mburakcakir.cryptopricetracker.data.repository.CoinRepository 11 | import com.mburakcakir.cryptopricetracker.ui.BaseViewModel 12 | import com.mburakcakir.cryptopricetracker.util.Resource 13 | import com.mburakcakir.cryptopricetracker.util.Result 14 | import com.mburakcakir.cryptopricetracker.util.enums.Status 15 | import com.mburakcakir.cryptopricetracker.util.format 16 | import com.mburakcakir.cryptopricetracker.util.getCoinMarketEntity 17 | import dagger.hilt.android.lifecycle.HiltViewModel 18 | import kotlinx.coroutines.flow.catch 19 | import kotlinx.coroutines.flow.collect 20 | import kotlinx.coroutines.flow.onStart 21 | import kotlinx.coroutines.launch 22 | import javax.inject.Inject 23 | 24 | @HiltViewModel 25 | class CoinViewModel @Inject constructor(private val coinRepository: CoinRepository) : 26 | BaseViewModel() { 27 | 28 | private val _allCoins = MutableLiveData>>() 29 | val allCoins: LiveData>> = _allCoins 30 | 31 | private val _coinByParameter = MutableLiveData>>() 32 | val coinByParameter: LiveData>> = _coinByParameter 33 | 34 | fun getCoinsByParameter(parameter: String) = viewModelScope.launch { 35 | coinRepository.getCoinsByParameter(parameter.format()) 36 | .onStart { 37 | _result.value = Result(loading = R.string.loading) 38 | } 39 | .catch { 40 | Log.v("errorGetCoinByParameter", it.message.toString()) 41 | } 42 | .collect { 43 | _coinByParameter.value = it 44 | } 45 | } 46 | 47 | fun getAllCoins() = viewModelScope.launch { 48 | coinRepository.getAllCoins() 49 | .onStart { 50 | _result.value = Result(loading = R.string.loading) 51 | } 52 | .catch { 53 | Log.v("errorGetAllCoins", it.message.toString()) 54 | } 55 | .collect { 56 | _allCoins.value = it 57 | } 58 | } 59 | 60 | fun insertAllCoins(listCrypto: List) = viewModelScope.launch { 61 | val coinEntityList = getCoinMarketEntity(listCrypto) 62 | 63 | coinRepository.insertAllCoins(coinEntityList) 64 | .onStart { 65 | _result.value = Result(loading = R.string.coin_loading) 66 | } 67 | .collect { 68 | when (it.status) { 69 | Status.SUCCESS -> { 70 | it.data?.let { 71 | Result(success = R.string.coin_success) 72 | } 73 | } 74 | Status.ERROR -> Result(success = R.string.coin_error) 75 | } 76 | } 77 | } 78 | 79 | fun endSession() { 80 | firebaseAuth.signOut() 81 | } 82 | 83 | fun checkIfUserLoggedIn(): Boolean { 84 | return firebaseAuth.currentUser != null 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/entry/login/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.entry.login 2 | 3 | import android.os.Bundle 4 | import android.text.Editable 5 | import android.view.View 6 | import android.widget.EditText 7 | import androidx.fragment.app.viewModels 8 | import com.mburakcakir.cryptopricetracker.R 9 | import com.mburakcakir.cryptopricetracker.databinding.FragmentLoginBinding 10 | import com.mburakcakir.cryptopricetracker.ui.BaseFragment 11 | import com.mburakcakir.cryptopricetracker.ui.entry.CustomTextWatcher 12 | import com.mburakcakir.cryptopricetracker.util.enums.EntryState 13 | import com.mburakcakir.cryptopricetracker.util.enums.EntryType 14 | import com.mburakcakir.cryptopricetracker.util.navigate 15 | import com.mburakcakir.cryptopricetracker.util.toast 16 | import com.mburakcakir.cryptopricetracker.util.verifyEmail 17 | import dagger.hilt.android.AndroidEntryPoint 18 | 19 | @AndroidEntryPoint 20 | class LoginFragment : BaseFragment() { 21 | 22 | private val loginViewModel: LoginViewModel by viewModels() 23 | 24 | override fun getFragmentView(): Int = R.layout.fragment_login 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | init() 29 | } 30 | 31 | private fun init() { 32 | setInputAndClick() 33 | 34 | observeData() 35 | } 36 | 37 | private fun setInputAndClick() { 38 | loginViewModel.setEntryType(EntryType.LOGIN) 39 | binding.lifecycleOwner = viewLifecycleOwner 40 | binding.loginViewModel = loginViewModel 41 | 42 | binding.edtEmail.afterTextChanged { 43 | loginViewModel.isDataChanged( 44 | EntryState.EMAIL, 45 | binding.edtEmail.text.toString() 46 | ) 47 | } 48 | 49 | binding.btnRegister.setOnClickListener { 50 | this.navigate(LoginFragmentDirections.actionLoginFragmentToRegisterFragment()) 51 | } 52 | 53 | } 54 | 55 | private fun observeData() { 56 | loginViewModel.entryFormState.observe(viewLifecycleOwner, { 57 | binding.btnLogin.isEnabled = it.isDataValid 58 | 59 | if (it.passwordError.isNullOrEmpty().not()) 60 | binding.edtPassword.error = it.passwordError 61 | if (it.emailError.isNullOrEmpty().not()) 62 | binding.edtEmail.error = it.emailError 63 | }) 64 | 65 | loginViewModel.isVerifiedSent.observe(viewLifecycleOwner) { 66 | if (it) 67 | loginViewModel.sendEmailVerify() 68 | else 69 | requireContext() toast getString(R.string.error_email_address) 70 | } 71 | 72 | loginViewModel.resultEntry.observe(viewLifecycleOwner) { 73 | var resultMessage = if (it) { 74 | checkUserVerifiedAndNavigate() 75 | getString(R.string.login_success) 76 | } else { 77 | getString(R.string.login_error) 78 | } 79 | 80 | requireContext() toast resultMessage 81 | } 82 | } 83 | 84 | private fun checkUserVerifiedAndNavigate() { 85 | val isUserVerified = loginViewModel.checkIfUserVerified() 86 | 87 | if (!isUserVerified) 88 | requireContext().verifyEmail() 89 | 90 | this.navigate(LoginFragmentDirections.actionLoginFragmentToCoinFragment()) 91 | } 92 | 93 | private fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { 94 | addTextChangedListener(object : CustomTextWatcher() { 95 | override fun afterTextChanged(editable: Editable?) { 96 | afterTextChanged.invoke(editable.toString()) 97 | } 98 | }) 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/detail/CoinDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.detail 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.viewModelScope 7 | import com.google.firebase.firestore.ktx.firestore 8 | import com.google.firebase.ktx.Firebase 9 | import com.mburakcakir.cryptopricetracker.R 10 | import com.mburakcakir.cryptopricetracker.data.model.CoinDetailItem 11 | import com.mburakcakir.cryptopricetracker.data.model.FavouriteCoinModel 12 | import com.mburakcakir.cryptopricetracker.data.repository.CoinRepository 13 | import com.mburakcakir.cryptopricetracker.ui.BaseViewModel 14 | import com.mburakcakir.cryptopricetracker.util.Constants 15 | import com.mburakcakir.cryptopricetracker.util.Resource 16 | import com.mburakcakir.cryptopricetracker.util.Result 17 | import dagger.hilt.android.lifecycle.HiltViewModel 18 | import kotlinx.coroutines.flow.catch 19 | import kotlinx.coroutines.flow.collect 20 | import kotlinx.coroutines.flow.onStart 21 | import kotlinx.coroutines.launch 22 | import javax.inject.Inject 23 | 24 | @HiltViewModel 25 | class CoinDetailViewModel @Inject constructor(private val coinRepository: CoinRepository) : 26 | BaseViewModel() { 27 | 28 | private val _coinInfo = MutableLiveData>() 29 | val coinInfo: LiveData> = _coinInfo 30 | 31 | private val _isFavouriteAdded = MutableLiveData() 32 | val isFavouriteAdded: LiveData = _isFavouriteAdded 33 | 34 | private val _isFavouriteDeleted = MutableLiveData() 35 | val isFavouriteDeleted: LiveData = _isFavouriteDeleted 36 | 37 | private val _isFavourite = MutableLiveData() 38 | val isFavourite: LiveData = _isFavourite 39 | 40 | val db = Firebase.firestore 41 | .collection(Constants.BASE_COLLECTION_NAME) 42 | .document(firebaseAuth.currentUser.uid) 43 | .collection(Constants.DETAIL_COLLECTION_NAME) 44 | 45 | fun getCoinByID(id: String) = viewModelScope.launch { 46 | coinRepository.getCoinByID(id) 47 | .onStart { 48 | _result.value = Result(loading = R.string.loading) 49 | } 50 | .catch { 51 | Log.v("errorGetCoinByID", it.message.toString()) 52 | } 53 | .collect { 54 | _coinInfo.value = it 55 | } 56 | } 57 | 58 | fun addToFavourites(coinDetail: CoinDetailItem) { 59 | val favouriteCryptoModel = FavouriteCoinModel( 60 | coinDetail.id, 61 | coinDetail.image.small, 62 | coinDetail.name, 63 | coinDetail.symbol 64 | ) 65 | 66 | val favouriteDocument = db.document(coinDetail.id) 67 | 68 | favouriteDocument.set(favouriteCryptoModel) 69 | .addOnSuccessListener { 70 | _isFavouriteAdded.postValue(true) 71 | } 72 | .addOnFailureListener { 73 | _isFavouriteAdded.postValue(false) 74 | } 75 | 76 | } 77 | 78 | fun isFavourite(cryptoID: String) { 79 | val favouriteDocument = db.document(cryptoID) 80 | 81 | favouriteDocument.get() 82 | .addOnSuccessListener { document -> 83 | _isFavourite.value = document.exists() 84 | } 85 | .addOnFailureListener { exception -> 86 | _isFavourite.value = false 87 | } 88 | } 89 | 90 | fun deleteFavourite(cryptoID: String) { 91 | val favouriteDocument = db.document(cryptoID) 92 | 93 | favouriteDocument 94 | .delete() 95 | .addOnSuccessListener { _isFavouriteDeleted.postValue(true) } 96 | .addOnFailureListener { _isFavouriteDeleted.postValue(false) } 97 | } 98 | 99 | 100 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/rv_item_favourite_coin.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 17 | 18 | 23 | 24 | 28 | 29 | 36 | 37 | 52 | 53 | 68 | 69 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_register.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 26 | 27 | 34 | 35 | 42 | 43 | 52 | 53 | 54 | 55 | 62 | 63 | 74 | 75 | 76 | 77 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'kotlin-parcelize' 6 | id 'androidx.navigation.safeargs' 7 | id 'com.google.gms.google-services' 8 | id 'dagger.hilt.android.plugin' 9 | } 10 | 11 | 12 | android { 13 | compileSdkVersion 30 14 | buildToolsVersion "30.0.3" 15 | 16 | defaultConfig { 17 | applicationId "com.mburakcakir.cryptopricetracker" 18 | minSdkVersion 21 19 | targetSdkVersion 30 20 | versionCode 1 21 | versionName "1.0" 22 | 23 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 24 | } 25 | 26 | dataBinding { 27 | enabled = true 28 | } 29 | 30 | buildFeatures { 31 | viewBinding true 32 | } 33 | 34 | buildTypes { 35 | release { 36 | minifyEnabled false 37 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 38 | } 39 | } 40 | 41 | buildTypes { 42 | debug { 43 | buildConfigField 'String', 'API_URL', "\"https://api.coingecko.com/api/v3/\"" 44 | } 45 | release { 46 | buildConfigField 'String', 'API_URL', "\"https://api.coingecko.com/api/v3/\"" 47 | } 48 | } 49 | 50 | compileOptions { 51 | sourceCompatibility JavaVersion.VERSION_1_8 52 | targetCompatibility JavaVersion.VERSION_1_8 53 | } 54 | kotlinOptions { 55 | jvmTarget = '1.8' 56 | } 57 | } 58 | 59 | dependencies { 60 | 61 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 62 | implementation 'androidx.core:core-ktx:1.3.2' 63 | implementation 'androidx.appcompat:appcompat:1.2.0' 64 | implementation 'com.google.android.material:material:1.3.0' 65 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 66 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 67 | testImplementation 'junit:junit:4.13.2' 68 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 69 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 70 | 71 | // Material 72 | implementation "com.google.android.material:material:$materialVersion" 73 | 74 | // Room 75 | implementation "androidx.room:room-runtime:$room_version" 76 | kapt "androidx.room:room-compiler:$room_version" 77 | implementation "androidx.room:room-ktx:$room_version" 78 | 79 | // Coroutines 80 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" 81 | 82 | // Viewmodel and LiveData 83 | implementation "androidx.lifecycle:lifecycle-extensions:$liveDataVersion" 84 | api "androidx.lifecycle:lifecycle-livedata-ktx:$lifeCycleLiveDataVersion" 85 | api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifeCycleViewModelVersion" 86 | 87 | // Glide 88 | implementation "com.github.bumptech.glide:glide:$glideVersion" 89 | kapt "com.github.bumptech.glide:compiler:$glideVersion" 90 | 91 | // Lottiefiles 92 | implementation "com.airbnb.android:lottie:$lottieVersion" 93 | 94 | // Retrofit 95 | implementation "com.squareup.retrofit2:retrofit:$retrofitLibraryVersion" 96 | implementation "com.squareup.retrofit2:converter-gson:$retrofitLibraryVersion" 97 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitLibraryVersion" 98 | implementation "com.squareup.okhttp:okhttp:$okHttpVersion" 99 | 100 | // Firebase Storage 101 | implementation "com.firebaseui:firebase-ui-storage:$firebaseUiVersion" 102 | 103 | // Firebase Auth, CloudFirestore And Analytics 104 | implementation "com.google.firebase:firebase-analytics:$firebaseAnalyticsVersion" 105 | implementation "com.google.firebase:firebase-auth:$firebaseAuthVersion" 106 | implementation platform("com.google.firebase:firebase-bom:$firebaseBomVersion") 107 | implementation 'com.google.firebase:firebase-analytics-ktx' 108 | implementation "com.google.firebase:firebase-firestore-ktx:$firestoreVersion" 109 | 110 | // Navigation Component 111 | implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" 112 | implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" 113 | 114 | // CircleImageView 115 | implementation "de.hdodenhof:circleimageview:$circleImageViewVersion" 116 | 117 | // Coil 118 | implementation("io.coil-kt:coil:$coilVersion") 119 | 120 | //Hilt 121 | implementation "com.google.dagger:hilt-android:$hiltVersion" 122 | kapt "com.google.dagger:hilt-compiler:$hiltVersion" 123 | 124 | 125 | 126 | 127 | } -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 20 | 22 | 23 | 135 | 136 | 138 | 139 | -------------------------------------------------------------------------------- /app/src/main/res/layout/rv_item_coin.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 23 | 24 | 31 | 32 | 45 | 46 | 58 | 59 | 71 | 72 | 80 | 81 | 96 | 97 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/java/com/mburakcakir/cryptopricetracker/ui/coin/CoinFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.coin 2 | 3 | import android.os.Bundle 4 | import android.view.Menu 5 | import android.view.MenuInflater 6 | import android.view.MenuItem 7 | import android.view.View 8 | import androidx.appcompat.widget.SearchView 9 | import androidx.fragment.app.viewModels 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.mburakcakir.cryptopricetracker.R 13 | import com.mburakcakir.cryptopricetracker.databinding.FragmentCoinBinding 14 | import com.mburakcakir.cryptopricetracker.ui.BaseFragment 15 | import com.mburakcakir.cryptopricetracker.util.NetworkControllerUtils 16 | import com.mburakcakir.cryptopricetracker.util.SharedPreferences 17 | import com.mburakcakir.cryptopricetracker.util.enums.Status 18 | import com.mburakcakir.cryptopricetracker.util.navigate 19 | import dagger.hilt.android.AndroidEntryPoint 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.delay 22 | import kotlinx.coroutines.launch 23 | 24 | @AndroidEntryPoint 25 | class CoinFragment : BaseFragment() { 26 | 27 | private var coinAdapter = CoinAdapter() 28 | private val coinViewModel: CoinViewModel by viewModels() 29 | 30 | private lateinit var sharedPreferences: SharedPreferences 31 | 32 | private val networkController: NetworkControllerUtils by lazy { 33 | NetworkControllerUtils(requireContext()).apply { 34 | startNetworkCallback() 35 | } 36 | } 37 | 38 | override fun getFragmentView(): Int = R.layout.fragment_coin 39 | 40 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 41 | super.onViewCreated(view, savedInstanceState) 42 | checkIfUserLoggedIn() 43 | } 44 | 45 | private fun init() { 46 | 47 | setToolbar() 48 | 49 | setSwipeRefreshLayout() 50 | 51 | checkInternetConnectionAndFetchData() 52 | 53 | setRecyclerView() 54 | 55 | observeCoins() 56 | 57 | repeatRequestByRefreshInterval() 58 | 59 | } 60 | 61 | private fun checkIfUserLoggedIn() { 62 | val sessionState = coinViewModel.checkIfUserLoggedIn() 63 | 64 | if (sessionState) 65 | init() 66 | else 67 | this.navigate(CoinFragmentDirections.actionCoinFragmentToLoginFragment()) 68 | 69 | } 70 | 71 | private fun setToolbar() { 72 | setHasOptionsMenu(true) 73 | } 74 | 75 | private fun setSwipeRefreshLayout() { 76 | binding.state = CoinViewState(Status.LOADING) 77 | 78 | binding.swipeRefreshLayout.setOnRefreshListener { 79 | binding.swipeRefreshLayout.isRefreshing = true 80 | coinViewModel.getAllCoins() 81 | 82 | } 83 | } 84 | 85 | private fun checkInternetConnectionAndFetchData() { 86 | networkController.isNetworkConnected.observe(viewLifecycleOwner) { internetConnected -> 87 | if (internetConnected) checkIsDataFetched() 88 | else binding.state = CoinViewState(Status.ERROR) 89 | } 90 | } 91 | 92 | private fun checkIsDataFetched() { 93 | if (coinViewModel.allCoins.value?.data == null) 94 | coinViewModel.getAllCoins() 95 | else { 96 | binding.state = CoinViewState(Status.SUCCESS) 97 | } 98 | } 99 | 100 | private fun setRecyclerView() { 101 | binding.rvCoinList.adapter = coinAdapter 102 | 103 | coinAdapter.setCoinOnClickListener { 104 | this.navigate(CoinFragmentDirections.actionCoinFragmentToCoinDetailFragment(it.cryptoID)) 105 | } 106 | 107 | coinAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { 108 | override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { 109 | super.onItemRangeInserted(positionStart, itemCount) 110 | binding.rvCoinList.smoothScrollToPosition(positionStart) 111 | } 112 | }) 113 | } 114 | 115 | private fun observeCoins() { 116 | coinViewModel.allCoins.observe(viewLifecycleOwner) { 117 | when (it.status) { 118 | Status.SUCCESS -> { 119 | coinAdapter.submitList(it.data) 120 | coinViewModel.insertAllCoins(it.data!!) 121 | 122 | binding.swipeRefreshLayout.isRefreshing = false 123 | } 124 | } 125 | binding.state = CoinViewState(it.status) 126 | } 127 | } 128 | 129 | private fun repeatRequestByRefreshInterval() { 130 | sharedPreferences = SharedPreferences(requireContext()) 131 | sharedPreferences.getRefreshInterval()?.let { 132 | this.lifecycleScope.launch(Dispatchers.IO) { 133 | while (true) { 134 | coinViewModel.getAllCoins() 135 | delay(Integer.parseInt(it).toLong() * 1000) 136 | } 137 | } 138 | 139 | } 140 | } 141 | 142 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 143 | inflater.inflate(R.menu.menu_coin_list, menu) 144 | 145 | val searchItem = menu.findItem(R.id.action_search).apply { 146 | // expandActionView() 147 | } 148 | 149 | val searchView = searchItem?.actionView as SearchView 150 | 151 | searchView.apply { 152 | queryHint = getString(R.string.coin_search) 153 | setOnQueryTextListener(onQueryTextListener) 154 | } 155 | 156 | return super.onCreateOptionsMenu(menu, inflater) 157 | } 158 | 159 | private val onQueryTextListener = object : SearchView.OnQueryTextListener { 160 | override fun onQueryTextSubmit(query: String?): Boolean { 161 | return true 162 | } 163 | 164 | override fun onQueryTextChange(newText: String?): Boolean { 165 | if (newText.isNullOrEmpty().not()) coinViewModel.getCoinsByParameter(newText!!) 166 | else coinViewModel.getAllCoins() 167 | 168 | return true 169 | } 170 | } 171 | 172 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 173 | when (item.itemId) { 174 | R.id.action_exit_app -> { 175 | coinViewModel.endSession() 176 | this.navigate(CoinFragmentDirections.actionCoinFragmentToLoginFragment()) 177 | } 178 | } 179 | return super.onOptionsItemSelected(item) 180 | } 181 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mburakcakir/cryptopricetracker/ui/detail/CoinDetailFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mburakcakir.cryptopricetracker.ui.detail 2 | 3 | import android.graphics.PorterDuff 4 | import android.graphics.PorterDuffColorFilter 5 | import android.os.Bundle 6 | import android.view.* 7 | import androidx.core.content.ContextCompat 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.lifecycleScope 10 | import androidx.navigation.fragment.navArgs 11 | import com.bumptech.glide.Glide 12 | import com.mburakcakir.cryptopricetracker.R 13 | import com.mburakcakir.cryptopricetracker.data.model.CoinDetailItem 14 | import com.mburakcakir.cryptopricetracker.databinding.FragmentCoinDetailBinding 15 | import com.mburakcakir.cryptopricetracker.ui.BaseFragment 16 | import com.mburakcakir.cryptopricetracker.ui.MainActivity 17 | import com.mburakcakir.cryptopricetracker.util.SharedPreferences 18 | import com.mburakcakir.cryptopricetracker.util.enums.Status 19 | import com.mburakcakir.cryptopricetracker.util.setCoinDetail 20 | import com.mburakcakir.cryptopricetracker.util.toast 21 | import dagger.hilt.android.AndroidEntryPoint 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.delay 24 | import kotlinx.coroutines.launch 25 | 26 | @AndroidEntryPoint 27 | class CoinDetailFragment : BaseFragment() { 28 | 29 | private val args by navArgs() 30 | private lateinit var coinID: String 31 | 32 | private val coinDetailViewModel: CoinDetailViewModel by viewModels() 33 | private var isFavourite: Boolean = false 34 | private lateinit var sharedPreferences: SharedPreferences 35 | private lateinit var menuItem: MenuItem 36 | 37 | override fun getFragmentView(): Int = R.layout.fragment_coin_detail 38 | 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 40 | super.onViewCreated(view, savedInstanceState) 41 | init() 42 | } 43 | 44 | private fun init() { 45 | 46 | setToolbar() 47 | 48 | setCoinData() 49 | 50 | observeData() 51 | 52 | } 53 | 54 | private fun setToolbar() { 55 | setHasOptionsMenu(true) 56 | } 57 | 58 | private fun setCoinData() { 59 | coinID = args.coinID 60 | coinDetailViewModel.isFavourite(coinID) 61 | 62 | sharedPreferences = SharedPreferences(requireContext()) 63 | sharedPreferences.getRefreshInterval()?.let { 64 | binding.edtInterval.setText(sharedPreferences.getRefreshInterval()) 65 | } 66 | 67 | binding.apply { 68 | state = CoinDetailViewState(Status.LOADING) 69 | edtInterval.setText(sharedPreferences.getRefreshInterval()) 70 | 71 | edtInterval.setOnKeyListener(onKeyListener) 72 | } 73 | } 74 | 75 | private fun setRefreshInterval() { 76 | val refreshInterval = binding.edtInterval.text.toString() 77 | repeatRequestByRefreshInterval(Integer.parseInt(refreshInterval)) 78 | requireContext() toast "All data and details will be refreshed every $refreshInterval seconds." 79 | } 80 | 81 | private fun repeatRequestByRefreshInterval(refreshInterval: Int) { 82 | this.lifecycleScope.launch(Dispatchers.IO) { 83 | while (true) { 84 | coinDetailViewModel.getCoinByID(coinID) 85 | sharedPreferences.saveRefreshInterval(refreshInterval) 86 | delay(refreshInterval.toLong() * 1000) 87 | } 88 | } 89 | } 90 | 91 | private fun observeData() { 92 | coinDetailViewModel.getCoinByID(coinID) 93 | coinDetailViewModel.coinInfo.observe(viewLifecycleOwner) { 94 | when (it.status) { 95 | Status.SUCCESS -> setCoinDetails(it.data!!) 96 | } 97 | binding.state = CoinDetailViewState(it.status) 98 | } 99 | 100 | coinDetailViewModel.isFavouriteAdded.observe(viewLifecycleOwner) { 101 | if (it) 102 | requireContext() toast getString(R.string.favourite_add_success) 103 | } 104 | 105 | coinDetailViewModel.isFavouriteDeleted.observe(viewLifecycleOwner) { 106 | if (it) 107 | requireContext() toast getString(R.string.favourite_delete_success) 108 | } 109 | 110 | coinDetailViewModel.isFavourite.observe(viewLifecycleOwner) { 111 | if (it) { 112 | isFavourite = it 113 | menuItem.changeIconColor(isFavourite) 114 | } 115 | } 116 | 117 | } 118 | 119 | private fun setCoinDetails(coinDetails: CoinDetailItem) { 120 | binding.coinDetail = setCoinDetail(coinDetails) 121 | (requireActivity() as MainActivity).supportActionBar?.apply { 122 | title = "${coinDetails.name} (${coinDetails.symbol})" 123 | } 124 | Glide.with(requireContext()).load(coinDetails.image.small).into(binding.imgIconImage) 125 | } 126 | 127 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 128 | inflater.inflate(R.menu.menu_coin_detail, menu) 129 | menuItem = menu.findItem(R.id.action_fav) 130 | return super.onCreateOptionsMenu(menu, inflater) 131 | } 132 | 133 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 134 | when (item.itemId) { 135 | R.id.action_fav -> { 136 | if (isFavourite) 137 | coinDetailViewModel.deleteFavourite(coinID) 138 | else 139 | coinDetailViewModel.addToFavourites(coinDetailViewModel.coinInfo.value?.data!!) 140 | 141 | isFavourite = !isFavourite 142 | item.changeIconColor(isFavourite) 143 | } 144 | } 145 | return super.onOptionsItemSelected(item) 146 | } 147 | 148 | private infix fun MenuItem.changeIconColor(isFavourite: Boolean) { 149 | val color = if (isFavourite) R.color.yellow else R.color.white 150 | 151 | icon.colorFilter = PorterDuffColorFilter( 152 | ContextCompat.getColor(requireContext(), color), 153 | PorterDuff.Mode.SRC_IN 154 | ) 155 | } 156 | 157 | private val onKeyListener = object : View.OnKeyListener { 158 | override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean { 159 | if (keyCode == KeyEvent.KEYCODE_ENTER && event?.action == KeyEvent.ACTION_UP) { 160 | setRefreshInterval() 161 | return true 162 | } 163 | return false 164 | } 165 | 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 23 | 24 | 31 | 32 | 40 | 41 | 53 | 54 | 61 | 62 | 74 | 75 | 76 | 77 | 85 | 86 | 98 | 99 | 100 | 101 | 112 | 113 | 120 | 121 | 133 | 134 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_no_connection.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 22 | 26 | 30 | 34 | 38 | 42 | 46 | 50 | 54 | 58 | 62 | 66 | 70 | 74 | 78 | 82 | 86 | 90 | 94 | 98 | 102 | 106 | 110 | 114 | 115 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 63 | 66 | 69 | 72 | 75 | 78 | 81 | 84 | 87 | 90 | 93 | 96 | 99 | 102 | 105 | 108 | 111 | 114 | 117 | 120 | 123 | 126 | 129 | 132 | 135 | 138 | 141 | 144 | 147 | 150 | 153 | 156 | 159 | 162 | 165 | 168 | 171 | 174 | 177 | 178 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 38 | 41 | 44 | 47 | 50 | 53 | 56 | 59 | 62 | 65 | 68 | 71 | 74 | 77 | 80 | 83 | 86 | 89 | 92 | 95 | 98 | 101 | 104 | 107 | 110 | 113 | 116 | 119 | 122 | 125 | 128 | 131 | 134 | 137 | 140 | 143 | 146 | 149 | 152 | 155 | 158 | 161 | 164 | 167 | 170 | 173 | 176 | 179 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_coin_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 15 | 16 | 17 | 20 | 21 | 26 | 27 | 33 | 34 | 40 | 41 | 45 | 46 | 54 | 55 | 65 | 66 | 76 | 77 | 86 | 87 | 96 | 97 | 106 | 107 | 117 | 118 | 128 | 129 | 139 | 140 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 165 | 166 | 176 | 177 | 187 | 188 | 194 | 195 | 206 | 207 | 208 | 209 | 221 | 222 | 231 | 232 | 242 | 243 | 244 | 258 | 259 | 267 | 268 | 269 | 279 | 280 | 281 | 282 | 283 | 284 | --------------------------------------------------------------------------------