├── 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 |
4 |
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 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_coin_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 | 
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | xmlns:android
34 |
35 | ^$
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | xmlns:.*
45 |
46 | ^$
47 |
48 |
49 | BY_NAME
50 |
51 |
52 |
53 |
54 |
55 |
56 | .*:id
57 |
58 | http://schemas.android.com/apk/res/android
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | .*:name
68 |
69 | http://schemas.android.com/apk/res/android
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | name
79 |
80 | ^$
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | style
90 |
91 | ^$
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | .*
101 |
102 | ^$
103 |
104 |
105 | BY_NAME
106 |
107 |
108 |
109 |
110 |
111 |
112 | .*
113 |
114 | http://schemas.android.com/apk/res/android
115 |
116 |
117 | ANDROID_ATTRIBUTE_ORDER
118 |
119 |
120 |
121 |
122 |
123 |
124 | .*
125 |
126 | .*
127 |
128 |
129 | BY_NAME
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
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 |
--------------------------------------------------------------------------------