├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── 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
│ │ │ ├── anim
│ │ │ │ ├── slide_in_right.xml
│ │ │ │ ├── slide_out_left.xml
│ │ │ │ ├── fade_in.xml
│ │ │ │ └── fade_out.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── values
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── strings.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── colors.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_quakes.xml
│ │ │ │ ├── fragment_quakes.xml
│ │ │ │ └── quake_item.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── inspiringteam
│ │ │ │ └── xchange
│ │ │ │ ├── BaseView.kt
│ │ │ │ ├── util
│ │ │ │ ├── Constants.kt
│ │ │ │ ├── ConnectivityUtils
│ │ │ │ │ ├── OnlineChecker.kt
│ │ │ │ │ └── DefaultOnlineChecker.kt
│ │ │ │ ├── schedulers
│ │ │ │ │ ├── BaseSchedulerProvider.kt
│ │ │ │ │ ├── ImmediateSchedulerProvider.kt
│ │ │ │ │ └── SchedulerProvider.kt
│ │ │ │ ├── DisplayUtils
│ │ │ │ │ ├── SortUtils.java
│ │ │ │ │ ├── GravityUtils.kt
│ │ │ │ │ └── TimeUtils.kt
│ │ │ │ ├── chromeTabsUtils
│ │ │ │ │ ├── ServiceConnectionCallback.kt
│ │ │ │ │ ├── ServiceConnection.kt
│ │ │ │ │ └── ChromeTabsWrapper.kt
│ │ │ │ ├── providers
│ │ │ │ │ ├── ResourceProvider.kt
│ │ │ │ │ └── BaseResourceProvider.kt
│ │ │ │ └── ActivityUtils.kt
│ │ │ │ ├── data
│ │ │ │ ├── models
│ │ │ │ │ ├── QuakeWrapper.kt
│ │ │ │ │ ├── QuakesResponse.kt
│ │ │ │ │ └── Quake.kt
│ │ │ │ └── source
│ │ │ │ │ ├── remote
│ │ │ │ │ ├── QuakesApiService.kt
│ │ │ │ │ ├── QuakesRemoteDataModule.kt
│ │ │ │ │ └── QuakesRemoteDataSource.kt
│ │ │ │ │ ├── scopes
│ │ │ │ │ ├── Local.kt
│ │ │ │ │ └── Remote.kt
│ │ │ │ │ ├── QuakesDataSource.kt
│ │ │ │ │ ├── local
│ │ │ │ │ ├── QuakesDatabase.kt
│ │ │ │ │ ├── QuakesLocalDataModule.kt
│ │ │ │ │ ├── QuakesDao.kt
│ │ │ │ │ └── QuakesLocalDataSource.kt
│ │ │ │ │ ├── QuakesRepositoryModule.kt
│ │ │ │ │ └── QuakesRepository.kt
│ │ │ │ ├── ui
│ │ │ │ └── quakes
│ │ │ │ │ ├── QuakesUiModel.kt
│ │ │ │ │ ├── NoQuakesModel.kt
│ │ │ │ │ ├── QuakeItem.kt
│ │ │ │ │ ├── QuakesActivity.kt
│ │ │ │ │ ├── QuakesModule.kt
│ │ │ │ │ ├── QuakesAdapter.kt
│ │ │ │ │ ├── QuakeItemViewHolder.kt
│ │ │ │ │ ├── QuakesViewModel.kt
│ │ │ │ │ └── QuakesFragment.kt
│ │ │ │ ├── di
│ │ │ │ ├── scopes
│ │ │ │ │ ├── AppScoped.kt
│ │ │ │ │ ├── FragmentScoped.kt
│ │ │ │ │ └── ActivityScoped.kt
│ │ │ │ ├── ViewModelKey.kt
│ │ │ │ ├── AppModule.kt
│ │ │ │ ├── ViewModelModule.kt
│ │ │ │ ├── ActivityBindingModule.kt
│ │ │ │ ├── quakes
│ │ │ │ │ └── QuakesViewModelFactory.kt
│ │ │ │ ├── AppComponent.kt
│ │ │ │ └── UtilsModule.kt
│ │ │ │ ├── App.kt
│ │ │ │ └── ScrollChildSwipeRefreshLayout.kt
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── inspiringteam
│ │ │ └── xchange
│ │ │ ├── quakes
│ │ │ └── QuakesScreenTest.kt
│ │ │ └── data
│ │ │ └── source
│ │ │ └── local
│ │ │ ├── QuakesDaoTest.kt
│ │ │ └── QuakesLocalDataSourceTest.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── inspiringteam
│ │ └── xchange
│ │ ├── data
│ │ └── source
│ │ │ ├── remote
│ │ │ └── QuakesRemoteDataSourceTest.kt
│ │ │ └── QuakesRepositoryTest.kt
│ │ └── quakes
│ │ └── QuakesViewModelTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── readme_pics
├── open_tab.gif
├── scrolling.gif
├── forcing_update.gif
├── mvvm_diagram.png
└── mvvm_dagger_dependency.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── caches
│ └── build_file_checksums.ser
├── vcs.xml
├── misc.xml
├── runConfigurations.xml
├── gradle.xml
├── jarRepositories.xml
└── codeStyles
│ └── Project.xml
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/readme_pics/open_tab.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/readme_pics/open_tab.gif
--------------------------------------------------------------------------------
/readme_pics/scrolling.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/readme_pics/scrolling.gif
--------------------------------------------------------------------------------
/readme_pics/forcing_update.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/readme_pics/forcing_update.gif
--------------------------------------------------------------------------------
/readme_pics/mvvm_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/readme_pics/mvvm_diagram.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.idea/caches/build_file_checksums.ser:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/.idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/readme_pics/mvvm_dagger_dependency.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/readme_pics/mvvm_dagger_dependency.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/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/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/BaseView.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange
2 |
3 | interface BaseView {
4 | fun bindViewModel()
5 | fun unbindViewModel()
6 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/catalinghita8/android-mvvm-rxjava2-dagger2/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/catalinghita8/android-mvvm-rxjava2-dagger2/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/libraries
5 | /.idea/modules.xml
6 | /.idea/workspace.xml
7 | .DS_Store
8 | /build
9 | /captures
10 | .externalNativeBuild
11 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util
2 |
3 | object Constants {
4 | const val QUAKES_API_BASE_URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/"
5 | const val QUAKES_ROOM_DB_STRING = "Quakes.db"
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/ConnectivityUtils/OnlineChecker.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.ConnectivityUtils
2 |
3 | /**
4 | * Simple interface that contains online/offline state indicator
5 | */
6 | interface OnlineChecker {
7 | fun isOnline(): Boolean
8 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Mar 09 17:11: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.7.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/models/QuakeWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.models
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | class QuakeWrapper(@field:Expose @field:SerializedName("properties") var quake: Quake)
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/QuakesUiModel.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | class QuakesUiModel(
4 | val isQuakesListVisible: Boolean,
5 | val quakes: List,
6 | val isNoQuakesViewVisible: Boolean,
7 | val noQuakesModel: NoQuakesModel?
8 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/NoQuakesModel.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | import androidx.annotation.StringRes
4 |
5 | /**
6 | * The string that should be displayed when there are no quakes.
7 | */
8 | class NoQuakesModel(@field:StringRes @get:StringRes var text: Int)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/scopes/AppScoped.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di.scopes
2 |
3 | import javax.inject.Scope
4 |
5 | /**
6 | * Replacement scope for @Singleton to improve readability
7 | */
8 | @MustBeDocumented
9 | @Scope
10 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
11 | annotation class AppScoped
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/models/QuakesResponse.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.models
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 | import java.util.*
6 |
7 | data class QuakesResponse(@field:Expose @field:SerializedName("features") var quakeWrapperList: MutableList)
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 16dp
5 | 4dp
6 |
7 | 4dp
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/remote/QuakesApiService.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.remote
2 |
3 | import retrofit2.http.GET
4 | import com.inspiringteam.xchange.data.models.QuakesResponse
5 | import io.reactivex.Single
6 |
7 | interface QuakesApiService {
8 | @get:GET("1.0_day.geojson")
9 | val quakes: Single
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/schedulers/BaseSchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.schedulers
2 |
3 | import io.reactivex.Scheduler
4 |
5 | /**
6 | * Allow providing different types of [Scheduler]s.
7 | */
8 | interface BaseSchedulerProvider {
9 | fun computation(): Scheduler
10 | fun io(): Scheduler
11 | fun ui(): Scheduler
12 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/scopes/Local.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.scopes
2 |
3 | import javax.inject.Qualifier
4 |
5 | /**
6 | * This scope has been created for Dagger to differentiate between types of Data Sources
7 | */
8 | @Qualifier
9 | @MustBeDocumented
10 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
11 | annotation class Local
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/scopes/Remote.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.scopes
2 |
3 | import javax.inject.Qualifier
4 |
5 | /**
6 | * This scope has been created for Dagger to differentiate between types of Data Sources
7 | */
8 | @Qualifier
9 | @MustBeDocumented
10 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
11 | annotation class Remote
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | xChange
3 | Hello fragment
4 | Error while loading quakes
5 | The Quakes were lost! Our best detectives are working on it..
6 | Risk:
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/scopes/FragmentScoped.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di.scopes
2 |
3 | import javax.inject.Scope
4 |
5 | @Scope
6 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
7 | @Target(
8 | AnnotationTarget.ANNOTATION_CLASS,
9 | AnnotationTarget.CLASS,
10 | AnnotationTarget.FUNCTION,
11 | AnnotationTarget.PROPERTY_GETTER,
12 | AnnotationTarget.PROPERTY_SETTER
13 | )
14 | annotation class FragmentScoped
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/QuakesDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source
2 |
3 | import com.inspiringteam.xchange.data.models.Quake
4 | import io.reactivex.Single
5 |
6 | interface QuakesDataSource {
7 | fun getQuakes(): Single>
8 | fun getQuake(quakeId: String): Single
9 | fun saveQuakes(quakes: List)
10 | fun saveQuake(quake: Quake)
11 | fun deleteAllQuakes()
12 | fun deleteQuake(quakeId: String)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/local/QuakesDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.local
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.inspiringteam.xchange.data.models.Quake
6 |
7 | /**
8 | * The Room Database that contains the Quakes table.
9 | */
10 | @Database(entities = [Quake::class], version = 2, exportSchema = false)
11 | abstract class QuakesDatabase : RoomDatabase() {
12 | abstract fun quakesDao(): QuakesDao
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/ViewModelKey.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.MapKey
5 | import kotlin.reflect.KClass
6 |
7 | @MustBeDocumented
8 | @Target(
9 | AnnotationTarget.FUNCTION,
10 | AnnotationTarget.PROPERTY_GETTER,
11 | AnnotationTarget.PROPERTY_SETTER
12 | )
13 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
14 | @MapKey
15 | internal annotation class ViewModelKey(val value: KClass)
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/ConnectivityUtils/DefaultOnlineChecker.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.ConnectivityUtils
2 |
3 | import android.net.ConnectivityManager
4 |
5 | /**
6 | * Custom OnlineChecker
7 | */
8 | class DefaultOnlineChecker(private val connectivityManager: ConnectivityManager) : OnlineChecker {
9 | override fun isOnline(): Boolean {
10 | val netInfo = connectivityManager.activeNetworkInfo
11 | return netInfo != null && netInfo.isConnectedOrConnecting
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/QuakeItem.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | import com.inspiringteam.xchange.data.models.Quake
4 | import rx.functions.Action0
5 |
6 | /**
7 | * A quake that should be displayed as an item in a list of quake.
8 | * Contains the quake and the action that should be triggered when taping on the item a
9 | */
10 | class QuakeItem(
11 | val quake: Quake,
12 | /**
13 | * @return the action to be triggered on click events
14 | */
15 | val onClickAction: Action0
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/DisplayUtils/SortUtils.java:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.DisplayUtils;
2 |
3 | import com.inspiringteam.xchange.data.models.Quake;
4 |
5 | import java.util.Collections;
6 | import java.util.List;
7 |
8 | public final class SortUtils {
9 |
10 |
11 | public static List sortByNewest(List list) {
12 | Collections.sort(list, (e1, e2) -> e1.getTimeStamp().compareTo(e2.getTimeStamp()));
13 | Collections.reverse(list);
14 |
15 | return list;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/chromeTabsUtils/ServiceConnectionCallback.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.chromeTabsUtils
2 |
3 | import androidx.browser.customtabs.CustomTabsClient
4 |
5 | interface ServiceConnectionCallback {
6 | /**
7 | * Called when the service is connected.
8 | *
9 | * @param client a CustomTabsClient
10 | */
11 | fun onServiceConnected(client: CustomTabsClient)
12 |
13 | /**
14 | * Called when the service is disconnected.
15 | */
16 | fun onServiceDisconnected()
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/scopes/ActivityScoped.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di.scopes
2 |
3 | import javax.inject.Scope
4 |
5 | /**
6 | * In Dagger, an unscoped component cannot depend on a scoped component. As
7 | * [AppComponent] is a scoped component @AppScoped, we create a custom
8 | * scope to be used by all fragment components. Additionally, a component with a specific scope
9 | * cannot have a sub component with the same scope.
10 | */
11 | @MustBeDocumented
12 | @Scope
13 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
14 | annotation class ActivityScoped
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_quakes.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/schedulers/ImmediateSchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.schedulers
2 |
3 | import io.reactivex.Scheduler
4 | import io.reactivex.schedulers.Schedulers
5 |
6 | /**
7 | * Implementation of the [BaseSchedulerProvider] making all [Scheduler]s immediate.
8 | */
9 | class ImmediateSchedulerProvider : BaseSchedulerProvider {
10 | override fun computation(): Scheduler {
11 | return Schedulers.trampoline()
12 | }
13 |
14 | override fun io(): Scheduler {
15 | return Schedulers.trampoline()
16 | }
17 |
18 | override fun ui(): Scheduler {
19 | return Schedulers.trampoline()
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/App.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange
2 |
3 | import com.inspiringteam.xchange.di.DaggerAppComponent
4 | import dagger.android.AndroidInjector
5 | import dagger.android.DaggerApplication
6 |
7 | /**
8 | * We create a custom Application class that extends [DaggerApplication].
9 | * We then override applicationInjector() which tells Dagger how to make our @AppScoped Component
10 | * We never have to call `component.inject(this)` as [DaggerApplication] will do that for us.
11 | */
12 | class App : DaggerApplication() {
13 | override fun applicationInjector(): AndroidInjector {
14 | return DaggerAppComponent.builder().application(this).build()
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import dagger.Binds
6 | import dagger.Module
7 |
8 | /**
9 | * This is the app's Dagger module. We use this to bind our Application class as a Context in the AppComponent.
10 | * By using Dagger Android we do not need to pass our Application instance to any module,
11 | * we simply need to expose our Application as Context.
12 | * through Dagger.Android our Application & Activities are provided into your graph for us.
13 | */
14 | @Module
15 | abstract class AppModule {
16 | // Expose Application as an injectable context
17 | @Binds
18 | abstract fun bindContext(application: Application?): Context?
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/providers/ResourceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.providers
2 |
3 | import android.content.Context
4 | import androidx.annotation.StringRes
5 | import com.google.common.base.Preconditions
6 |
7 | /**
8 | * Concrete implementation of the [BaseResourceProvider] interface.
9 | */
10 | class ResourceProvider(context: Context) : BaseResourceProvider {
11 | private val mContext: Context = Preconditions.checkNotNull(context, "context cannot be null")
12 |
13 | override fun getString(@StringRes id: Int): String {
14 | return mContext.getString(id)
15 | }
16 |
17 | override fun getString(@StringRes id: Int, vararg formatArgs: Any?): String {
18 | return mContext.getString(id, *formatArgs)
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.inspiringteam.xchange.di.quakes.QuakesViewModelFactory
6 | import com.inspiringteam.xchange.di.scopes.AppScoped
7 | import com.inspiringteam.xchange.ui.quakes.QuakesViewModel
8 | import dagger.Binds
9 | import dagger.Module
10 | import dagger.multibindings.IntoMap
11 |
12 | @Module
13 | abstract class ViewModelModule {
14 | @Binds
15 | @IntoMap
16 | @ViewModelKey(QuakesViewModel::class)
17 | abstract fun bindQuakesViewModel(quakesViewModel: QuakesViewModel?): ViewModel?
18 | @Binds
19 | @AppScoped
20 | abstract fun bindViewModelFactory(factory: QuakesViewModelFactory?): ViewModelProvider.Factory?
21 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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 | android.enableJetifier=true
10 | android.useAndroidX=true
11 | org.gradle.jvmargs=-Xmx1536m
12 | # When configured, Gradle will run in incubating parallel mode.
13 | # This option should only be used with decoupled projects. More details, visit
14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15 | # org.gradle.parallel=true
16 |
--------------------------------------------------------------------------------
/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
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/providers/BaseResourceProvider.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.providers
2 |
3 | import androidx.annotation.StringRes
4 |
5 | /**
6 | * Resolves application's resources.
7 | */
8 | interface BaseResourceProvider {
9 | /**
10 | * Resolves text's id to String.
11 | *
12 | * @param id to be fetched from the resources
13 | * @return String representation of the {@param id}
14 | */
15 | fun getString(@StringRes id: Int): String
16 |
17 | /**
18 | * Resolves text's id to String and formats it.
19 | *
20 | * @param resId to be fetched from the resources
21 | * @param formatArgs format arguments
22 | * @return String representation of the {@param resId}
23 | */
24 | fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/ActivityUtils.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentManager
5 | import androidx.fragment.app.FragmentTransaction
6 |
7 | import com.google.common.base.Preconditions.checkNotNull
8 |
9 | object ActivityUtils {
10 | /**
11 | * The fragment is added to the container view with id frameId. The operation is
12 | * performed by the fragmentManager.
13 | */
14 | fun addFragmentToActivity(fragmentManager: FragmentManager,
15 | fragment: Fragment, frameId: Int) {
16 | checkNotNull(fragmentManager)
17 | checkNotNull(fragment)
18 | val transaction = fragmentManager.beginTransaction()
19 | transaction.add(frameId, fragment)
20 | transaction.commit()
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/local/QuakesLocalDataModule.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.local
2 |
3 | import android.app.Application
4 | import androidx.room.Room
5 | import com.inspiringteam.xchange.di.scopes.AppScoped
6 | import com.inspiringteam.xchange.util.Constants
7 | import dagger.Module
8 | import dagger.Provides
9 |
10 | @Module
11 | class QuakesLocalDataModule {
12 | @AppScoped
13 | @Provides
14 | fun provideDb(context: Application): QuakesDatabase {
15 | return Room.databaseBuilder(
16 | context.applicationContext,
17 | QuakesDatabase::class.java,
18 | Constants.QUAKES_ROOM_DB_STRING
19 | )
20 | .fallbackToDestructiveMigration()
21 | .build()
22 | }
23 |
24 | @AppScoped
25 | @Provides
26 | fun provideQuakesDao(db: QuakesDatabase): QuakesDao {
27 | return db.quakesDao()
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/QuakesActivity.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | import android.os.Bundle
4 | import com.inspiringteam.xchange.R
5 | import com.inspiringteam.xchange.util.ActivityUtils.addFragmentToActivity
6 | import dagger.android.support.DaggerAppCompatActivity
7 | import javax.inject.Inject
8 |
9 | class QuakesActivity : DaggerAppCompatActivity() {
10 | @JvmField
11 | @Inject
12 | var injectedFragment: QuakesFragment? = null
13 |
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_quakes)
17 |
18 | // Set up fragment
19 | var fragment = supportFragmentManager.findFragmentById(R.id.contentFrame) as QuakesFragment?
20 | if (fragment == null) {
21 | fragment = injectedFragment
22 | addFragmentToActivity(supportFragmentManager, fragment!!, R.id.contentFrame)
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/QuakesModule.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | import com.inspiringteam.xchange.di.scopes.ActivityScoped
4 | import com.inspiringteam.xchange.di.scopes.FragmentScoped
5 | import com.inspiringteam.xchange.ui.quakes.QuakesModule.QuakesAbstractModule
6 | import com.inspiringteam.xchange.util.providers.BaseResourceProvider
7 | import com.inspiringteam.xchange.util.providers.ResourceProvider
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.android.ContributesAndroidInjector
11 |
12 | @Module(includes = [QuakesAbstractModule::class])
13 | class QuakesModule {
14 | @ActivityScoped
15 | @Provides
16 | fun provideResourceProvider(context: QuakesActivity?): BaseResourceProvider {
17 | return ResourceProvider(context!!)
18 | }
19 |
20 | @Module
21 | interface QuakesAbstractModule {
22 | @FragmentScoped
23 | @ContributesAndroidInjector
24 | fun quakesFragment(): QuakesFragment?
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/DisplayUtils/GravityUtils.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.DisplayUtils
2 |
3 | import java.util.*
4 |
5 | object GravityUtils {
6 | private val gravityStrings: MutableList =
7 | Arrays.asList(" Very low", " Low", " Medium", " High", " Very High")
8 |
9 | fun toGravityString(g: Int): String {
10 | var index: Int = 0
11 | if (0 < g && g < 151) index = 0 else if (g < 251) index = 1 else if (g < 451) index =
12 | 2 else if (g < 751) index = 3 else if (g < 1001) index = 4
13 | return gravityStrings.get(index)
14 | }
15 |
16 | fun toMagnitudeColor(m: Double): Int {
17 | var index: Int = 0
18 | if (0 < m && m < 1.2) index = 0 else if (m < 1.5) index = 1 else if (m < 2) index =
19 | 2 else if (m < 2.5) index = 3 else if (m < 3) index = 4 else if (m < 4) index =
20 | 5 else if (m < 4.5) index = 6 else if (m < 5) index = 7 else if (m < 5.5) index =
21 | 8 else if (m < 6) index = 9 else if (m < 6.5) index = 10 else if (m < 100) index = 11
22 | return index
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/schedulers/SchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.schedulers
2 |
3 | import io.reactivex.Scheduler
4 | import io.reactivex.android.schedulers.AndroidSchedulers
5 | import io.reactivex.schedulers.Schedulers
6 |
7 | /**
8 | * Provides different types of schedulers.
9 | */
10 | class SchedulerProvider // Prevent direct instantiation.
11 | private constructor() : BaseSchedulerProvider {
12 | override fun computation(): Scheduler {
13 | return Schedulers.computation()
14 | }
15 |
16 | override fun io(): Scheduler {
17 | return Schedulers.io()
18 | }
19 |
20 | override fun ui(): Scheduler {
21 | return AndroidSchedulers.mainThread()
22 | }
23 |
24 | companion object {
25 | private var INSTANCE: SchedulerProvider? = null
26 |
27 | @get:Synchronized
28 | val instance: SchedulerProvider
29 | get() {
30 | if (INSTANCE == null) {
31 | INSTANCE = SchedulerProvider()
32 | }
33 | return INSTANCE!!
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/chromeTabsUtils/ServiceConnection.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.chromeTabsUtils
2 |
3 | import android.content.ComponentName
4 | import androidx.browser.customtabs.CustomTabsClient
5 | import androidx.browser.customtabs.CustomTabsServiceConnection
6 | import java.lang.ref.WeakReference
7 |
8 | class ServiceConnection(connectionCallback: ServiceConnectionCallback) :
9 | CustomTabsServiceConnection() {
10 | // A weak reference to the ServiceConnectionCallback to avoid leaking it.
11 | private val mConnectionCallback: WeakReference
12 |
13 | override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
14 | val connectionCallback = mConnectionCallback.get()
15 | connectionCallback?.onServiceConnected(client)
16 | }
17 |
18 | override fun onServiceDisconnected(name: ComponentName) {
19 | val connectionCallback = mConnectionCallback.get()
20 | connectionCallback?.onServiceDisconnected()
21 | }
22 |
23 | init {
24 | mConnectionCallback = WeakReference(connectionCallback)
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/ActivityBindingModule.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di
2 |
3 | import com.inspiringteam.xchange.di.scopes.ActivityScoped
4 | import com.inspiringteam.xchange.ui.quakes.QuakesActivity
5 | import com.inspiringteam.xchange.ui.quakes.QuakesModule
6 | import dagger.Module
7 | import dagger.android.ContributesAndroidInjector
8 |
9 | /**
10 | * We want Dagger.Android to create a Subcomponent which has a parent Component of
11 | * whichever module ActivityBindingModule is on (AppComponent, here).
12 | * we never need to tell AppComponent that it is going to have all or any of these subcomponents
13 | * nor do we need to tell these subcomponents that AppComponent exists.
14 | * We are also telling Dagger.Android that this generated SubComponent needs to include the specified modules and
15 | * be aware of a scope annotation @ActivityScoped
16 | * In this case, when Dagger.Android annotation processor runs it will create 1 subcomponent for us
17 | */
18 | @Module
19 | abstract class ActivityBindingModule {
20 | @ActivityScoped
21 | @ContributesAndroidInjector(modules = [QuakesModule::class])
22 | abstract fun quakesActivity(): QuakesActivity?
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ScrollChildSwipeRefreshLayout.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.View
6 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
7 |
8 | /**
9 | * Extends [SwipeRefreshLayout] to support non-direct descendant scrolling views.
10 | *
11 | *
12 | * [SwipeRefreshLayout] works as expected when a scroll view is a direct child: it triggers
13 | * the refresh only when the view is on top. This class adds a way (@link #setScrollUpChild} to
14 | * define which view controls this behavior.
15 | */
16 | class ScrollChildSwipeRefreshLayout : SwipeRefreshLayout {
17 | private var mScrollUpChild: View? = null
18 |
19 | constructor(context: Context?) : super(context!!) {}
20 | constructor(context: Context?, attrs: AttributeSet?) : super(
21 | context!!, attrs
22 | ) {
23 | }
24 |
25 | override fun canChildScrollUp(): Boolean {
26 | return if (mScrollUpChild != null) {
27 | canScrollHorizontally(-1)
28 | } else super.canChildScrollUp()
29 | }
30 |
31 | fun setScrollUpChild(view: View?) {
32 | mScrollUpChild = view
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/DisplayUtils/TimeUtils.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.DisplayUtils
2 |
3 | import java.util.*
4 | import java.util.concurrent.TimeUnit
5 |
6 | object TimeUtils {
7 | private val times: MutableList = Arrays.asList(
8 | TimeUnit.DAYS.toMillis(365),
9 | TimeUnit.DAYS.toMillis(30),
10 | TimeUnit.DAYS.toMillis(1),
11 | TimeUnit.HOURS.toMillis(1),
12 | TimeUnit.MINUTES.toMillis(1),
13 | TimeUnit.SECONDS.toMillis(1)
14 | )
15 | private val timesString: MutableList =
16 | Arrays.asList("year", "month", "day", "hour", "minute", "second")
17 |
18 | fun toDuration(duration: Long): String {
19 | val res: StringBuffer = StringBuffer()
20 | for (i in times.indices) {
21 | val current: Long = times.get(i)
22 | val temp: Long = duration / current
23 | if (temp > 0) {
24 | res.append(temp).append(" ").append(timesString.get(i))
25 | .append(if (temp != 1L) "s" else "").append(" ago")
26 | break
27 | }
28 | }
29 | if (("" == res.toString())) return "Just now" else return res.toString()
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/quakes/QuakesViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di.quakes
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.inspiringteam.xchange.di.scopes.AppScoped
6 | import javax.inject.Inject
7 | import javax.inject.Provider
8 |
9 |
10 | @Suppress("UNCHECKED_CAST")
11 | @AppScoped
12 | class QuakesViewModelFactory @Inject
13 | constructor(
14 | private val creators: Map,
15 | @JvmSuppressWildcards Provider>
16 | ) : ViewModelProvider.Factory {
17 |
18 | override fun create(modelClass: Class): T {
19 | var creator: Provider? = creators[modelClass]
20 | if (creator == null) {
21 | for ((key, value) in creators) {
22 | if (modelClass.isAssignableFrom(key)) {
23 | creator = value
24 | break
25 | }
26 | }
27 | }
28 | if (creator == null) {
29 | throw IllegalArgumentException("unknown model class " + modelClass)
30 | }
31 | try {
32 | return creator.get() as T
33 | } catch (e: Exception) {
34 | throw RuntimeException(e)
35 | }
36 |
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di
2 |
3 | import com.inspiringteam.xchange.data.source.QuakesRepositoryModule
4 | import com.inspiringteam.xchange.di.scopes.AppScoped
5 | import dagger.BindsInstance
6 | import dagger.Component
7 | import dagger.android.AndroidInjector
8 | import dagger.android.support.AndroidSupportInjectionModule
9 | import android.app.Application
10 | import com.inspiringteam.xchange.App
11 |
12 | /**
13 | * This is the root Dagger component.
14 | * [AndroidSupportInjectionModule]
15 | * is the module from Dagger.Android that helps with the generation
16 | * and location of subcomponents, which will be in our case, activities
17 | */
18 | @AppScoped
19 | @Component(
20 | modules = [
21 | QuakesRepositoryModule::class,
22 | ViewModelModule::class,
23 | UtilsModule::class,
24 | AppModule::class,
25 | ActivityBindingModule::class,
26 | AndroidSupportInjectionModule::class]
27 | )
28 | interface AppComponent : AndroidInjector {
29 |
30 | // Application will just be provided into our app graph
31 | @Component.Builder
32 | interface Builder {
33 |
34 | @BindsInstance
35 | fun application(application: Application): AppComponent.Builder
36 |
37 | fun build(): AppComponent
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 | // colors for gravity of quakes
8 | #98FB98
9 | #90EE90
10 | #7CFC00
11 | #228B22
12 | #006400
13 |
14 | #FFFF00
15 |
16 | #FFA500
17 | #FF8C00
18 | #FF7F50
19 | #FF6347
20 | #FF4500
21 |
22 | #FF0000
23 |
24 |
25 | - @color/green0
26 | - @color/green1
27 | - @color/green2
28 | - @color/green3
29 | - @color/green4
30 | - @color/yellow0
31 | - @color/orange0
32 | - @color/orange1
33 | - @color/orange2
34 | - @color/orange3
35 | - @color/orange4
36 | - @color/red0
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/local/QuakesDao.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.local
2 |
3 | import androidx.room.*
4 | import com.inspiringteam.xchange.data.models.Quake
5 | import io.reactivex.Single
6 | import java.util.*
7 |
8 | /**
9 | * Room Dao interface
10 | */
11 | @Dao
12 | interface QuakesDao {
13 | @Query("SELECT * FROM Quakes ")
14 | fun getQuakes(): Single>
15 |
16 | /**
17 | * Retrieve a quake by id.
18 | *
19 | * @param quakeId the Quake id.
20 | * @return the quake with quakeId
21 | */
22 | @Query("SELECT * FROM Quakes WHERE id = :quakeId")
23 | fun getQuakeById(quakeId: String): Single
24 |
25 | /**
26 | * Insert Quake in the database. If the Quake already exists, ignore the action.
27 | *
28 | * @param Quake to be inserted.
29 | */
30 | @Insert(onConflict = OnConflictStrategy.REPLACE)
31 | fun insertQuake(Quake: Quake)
32 |
33 | /**
34 | * Delete a Quake by id.
35 | *
36 | * @return the number of Quakes deleted. This should always be 1.
37 | */
38 | @Query("DELETE FROM Quakes WHERE id = :QuakeId")
39 | fun deleteQuakeById(QuakeId: String): Int
40 |
41 | /**
42 | * Delete all Quake (items).
43 | */
44 | @Query("DELETE FROM Quakes")
45 | fun deleteQuakes()
46 |
47 | @Update
48 | fun updateQuake(Quake: Quake): Int
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/remote/QuakesRemoteDataModule.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.remote
2 |
3 | import com.google.gson.FieldNamingPolicy
4 | import com.google.gson.Gson
5 | import com.google.gson.GsonBuilder
6 | import com.inspiringteam.xchange.di.scopes.AppScoped
7 | import com.inspiringteam.xchange.util.Constants
8 | import dagger.Module
9 | import dagger.Provides
10 | import retrofit2.Retrofit
11 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
12 | import retrofit2.converter.gson.GsonConverterFactory
13 |
14 | @Module
15 | class QuakesRemoteDataModule {
16 | @AppScoped
17 | @Provides
18 | fun provideQuakesService(retrofit: Retrofit): QuakesApiService {
19 | return retrofit.create(QuakesApiService::class.java)
20 | }
21 |
22 | @Provides
23 | @AppScoped
24 | fun provideRetrofit(gson: Gson): Retrofit {
25 | return Retrofit.Builder()
26 | .addConverterFactory(GsonConverterFactory.create(gson))
27 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
28 | .baseUrl(Constants.QUAKES_API_BASE_URL)
29 | .build()
30 | }
31 |
32 | @Provides
33 | @AppScoped
34 | fun provideGson(): Gson {
35 | val gsonBuilder = GsonBuilder()
36 | gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
37 | return gsonBuilder.create()
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/inspiringteam/xchange/quakes/QuakesScreenTest.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.quakes
2 |
3 | import android.app.Activity
4 | import androidx.test.filters.LargeTest
5 | import androidx.test.runner.AndroidJUnit4
6 | import com.inspiringteam.xchange.R
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 | import androidx.test.rule.ActivityTestRule
10 |
11 | import com.inspiringteam.xchange.ui.quakes.QuakesActivity
12 | import org.junit.Rule
13 |
14 | import androidx.test.espresso.Espresso.onView
15 | import androidx.test.espresso.assertion.ViewAssertions.matches
16 | import androidx.test.espresso.intent.rule.IntentsTestRule
17 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
18 | import androidx.test.espresso.matcher.ViewMatchers.withId
19 |
20 |
21 | @RunWith(AndroidJUnit4::class)
22 | @LargeTest
23 | class QuakesScreenTest {
24 | @Rule
25 | @JvmField
26 | val activityRule = activityTestRule()
27 |
28 | // More complex tests should be added as app's complexity rises
29 | @Test
30 | fun displayItemsInList() {
31 | // check if the ListView is visible
32 | onView(withId(R.id.quakesListView))
33 | .check(matches(isDisplayed()))
34 | }
35 | }
36 |
37 | inline fun activityTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) =
38 | ActivityTestRule(T::class.java, initialTouchMode, launchActivity)
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/di/UtilsModule.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.di
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.net.ConnectivityManager
6 | import com.inspiringteam.xchange.di.scopes.AppScoped
7 | import com.inspiringteam.xchange.util.chromeTabsUtils.ChromeTabsWrapper
8 | import com.inspiringteam.xchange.util.ConnectivityUtils.DefaultOnlineChecker
9 | import com.inspiringteam.xchange.util.ConnectivityUtils.OnlineChecker
10 | import com.inspiringteam.xchange.util.schedulers.BaseSchedulerProvider
11 | import com.inspiringteam.xchange.util.schedulers.SchedulerProvider
12 | import dagger.Module
13 | import dagger.Provides
14 |
15 | @Module
16 | class UtilsModule {
17 | @Provides
18 | @AppScoped
19 | fun provideConnectivityManager(context: Application): ConnectivityManager {
20 | return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
21 | }
22 |
23 | @AppScoped
24 | @Provides
25 | fun providesChromeTabsWrapper(app: Application): ChromeTabsWrapper {
26 | return ChromeTabsWrapper(app.applicationContext)
27 | }
28 |
29 | @Provides
30 | @AppScoped
31 | fun onlineChecker(cm: ConnectivityManager): OnlineChecker {
32 | return DefaultOnlineChecker(cm)
33 | }
34 |
35 | @AppScoped
36 | @Provides
37 | fun provideSchedulerProvider(): BaseSchedulerProvider {
38 | return SchedulerProvider.instance
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/QuakesAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.BaseAdapter
7 | import com.inspiringteam.xchange.R
8 | import java.util.*
9 |
10 | internal class QuakesAdapter(private var quakes: MutableList) : BaseAdapter() {
11 |
12 | fun replaceData(quakes: MutableList) {
13 | this.quakes = quakes
14 | notifyDataSetChanged()
15 | }
16 |
17 | override fun getCount(): Int {
18 | return quakes.size
19 | }
20 |
21 | override fun getItem(i: Int): QuakeItem {
22 | return quakes[i]
23 | }
24 |
25 | override fun getItemId(i: Int): Long {
26 | return i.toLong()
27 | }
28 |
29 | override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
30 | var rowView = convertView
31 | val viewHolder: QuakeItemViewHolder
32 | if (rowView == null) {
33 | val inflater = LayoutInflater.from(parent?.context)
34 | rowView = inflater.inflate(R.layout.quake_item, parent, false)
35 | viewHolder = QuakeItemViewHolder(rowView)
36 | rowView.tag = viewHolder
37 | } else
38 | viewHolder = rowView.tag as QuakeItemViewHolder
39 |
40 | val quakeItem = getItem(position)
41 | viewHolder.bindItem(quakeItem)
42 | return rowView!!
43 | }
44 |
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/QuakesRepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source
2 |
3 | import com.inspiringteam.xchange.data.source.local.QuakesDao
4 | import com.inspiringteam.xchange.data.source.local.QuakesLocalDataModule
5 | import com.inspiringteam.xchange.data.source.local.QuakesLocalDataSource
6 | import com.inspiringteam.xchange.data.source.remote.QuakesApiService
7 | import com.inspiringteam.xchange.data.source.remote.QuakesRemoteDataModule
8 | import com.inspiringteam.xchange.data.source.remote.QuakesRemoteDataSource
9 | import com.inspiringteam.xchange.data.source.scopes.Local
10 | import com.inspiringteam.xchange.data.source.scopes.Remote
11 | import com.inspiringteam.xchange.di.scopes.AppScoped
12 | import com.inspiringteam.xchange.util.schedulers.BaseSchedulerProvider
13 | import dagger.Module
14 | import dagger.Provides
15 |
16 | @Module(includes = [QuakesLocalDataModule::class, QuakesRemoteDataModule::class])
17 | class QuakesRepositoryModule {
18 | @Provides
19 | @Local
20 | @AppScoped
21 | fun provideQuakesLocalDataSource(
22 | quakesDao: QuakesDao?,
23 | schedulerProvider: BaseSchedulerProvider?
24 | ): QuakesDataSource {
25 | return QuakesLocalDataSource(quakesDao!!, schedulerProvider!!)
26 | }
27 |
28 | @Provides
29 | @Remote
30 | @AppScoped
31 | fun provideQuakesRemoteDataSource(apiService: QuakesApiService?): QuakesDataSource {
32 | return QuakesRemoteDataSource(apiService!!)
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_quakes.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
19 |
20 |
24 |
25 |
26 |
32 |
33 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/remote/QuakesRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.remote
2 |
3 | import com.inspiringteam.xchange.data.models.Quake
4 | import com.inspiringteam.xchange.data.models.QuakeWrapper
5 | import com.inspiringteam.xchange.data.models.QuakesResponse
6 | import com.inspiringteam.xchange.data.source.QuakesDataSource
7 | import com.inspiringteam.xchange.di.scopes.AppScoped
8 | import io.reactivex.Observable
9 | import io.reactivex.Single
10 | import java.lang.Exception
11 | import javax.inject.Inject
12 |
13 | /**
14 | * Remote Data Source implementation
15 | */
16 | @AppScoped
17 | class QuakesRemoteDataSource @Inject constructor(private val mApiService: QuakesApiService) :
18 | QuakesDataSource {
19 | /**
20 | * Fresh items are retrieved from Remote API
21 | */
22 | override fun getQuakes(): Single> {
23 | return mApiService.quakes
24 | .flatMap { response: QuakesResponse ->
25 | Observable.fromIterable(response.quakeWrapperList).toList()
26 | }
27 | .flatMap { wrappersResponse: List? ->
28 | Observable.fromIterable(wrappersResponse)
29 | .map { wrapper: QuakeWrapper ->
30 | wrapper.quake.timeStampAdded = System.currentTimeMillis()
31 | wrapper.quake
32 | }.toList()
33 | }
34 | }
35 |
36 | /**
37 | * These methods should be implemented when required
38 | * (e.g. when a cloud service is integrated)
39 | */
40 | override fun getQuake(quakeId: String): Single {
41 | throw Exception("Not implemented")
42 | }
43 |
44 | override fun saveQuakes(quakes: List) {}
45 | override fun saveQuake(quake: Quake) {}
46 | override fun deleteAllQuakes() {}
47 | override fun deleteQuake(quakeId: String) {}
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/QuakeItemViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | import android.view.View
4 | import android.widget.TextView
5 | import com.inspiringteam.xchange.R
6 | import com.inspiringteam.xchange.util.DisplayUtils.GravityUtils
7 | import com.inspiringteam.xchange.util.DisplayUtils.TimeUtils
8 | import rx.functions.Action0
9 |
10 | /**
11 | * View holder for the quake item.
12 | */
13 | internal class QuakeItemViewHolder(private val rowView: View) : View.OnClickListener {
14 | private val titleTextView: TextView = rowView.findViewById(R.id.title_tv)
15 | private val magnitudeTextView: TextView = rowView.findViewById(R.id.magnitude_tv)
16 | private val timeStampTextView: TextView = rowView.findViewById(R.id.timestamp_tv)
17 | private val gravityTextView: TextView = rowView.findViewById(R.id.gravity_tv)
18 | private var clickAction: Action0? = null
19 |
20 |
21 | init {
22 | rowView.setOnClickListener(this)
23 | }
24 |
25 | fun bindItem(quakeItem: QuakeItem) {
26 | val dangerColorsArray = rowView.context.resources.getIntArray(R.array.danger_color_array)
27 | val magnitude = quakeItem.quake.magnitude ?: 0.0
28 | val dangerIndex = GravityUtils.toMagnitudeColor(magnitude)
29 | rowView.setBackgroundColor(dangerColorsArray[dangerIndex])
30 | magnitudeTextView.text = magnitude.toString()
31 | titleTextView.text = quakeItem.quake.location
32 | timeStampTextView.text =
33 | TimeUtils.toDuration(System.currentTimeMillis() - (quakeItem.quake.timeStamp ?: 0))
34 | val intro = rowView.context.resources.getString(R.string.quake_item_risk)
35 | val gravity = intro + GravityUtils.toGravityString(quakeItem.quake.gravity)
36 | gravityTextView.text = gravity
37 | clickAction = quakeItem.onClickAction
38 | }
39 |
40 | override fun onClick(v: View) {
41 | clickAction?.call()
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/util/chromeTabsUtils/ChromeTabsWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.util.chromeTabsUtils
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import androidx.browser.customtabs.CustomTabsClient
7 | import androidx.browser.customtabs.CustomTabsIntent
8 | import androidx.browser.customtabs.CustomTabsServiceConnection
9 | import androidx.core.content.ContextCompat
10 | import com.inspiringteam.xchange.R
11 |
12 | // TODO Deprecate this wrapper
13 | class ChromeTabsWrapper constructor(private val mContext: Context) : ServiceConnectionCallback {
14 | private var mConnection: CustomTabsServiceConnection? = null
15 | private var mClient: CustomTabsClient? = null
16 |
17 | fun openCustomtab(url: String?) {
18 | val builder: CustomTabsIntent.Builder = CustomTabsIntent.Builder()
19 | builder.setExitAnimations(mContext, R.anim.fade_in, R.anim.fade_out)
20 | builder.setToolbarColor(ContextCompat.getColor(mContext, R.color.colorPrimary))
21 | val customTabsIntent: CustomTabsIntent = builder.build()
22 | customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
23 | customTabsIntent.launchUrl(mContext, Uri.parse(url))
24 | }
25 |
26 | fun bindCustomTabsService() {
27 | if (mClient != null) return
28 | if (mConnection == null) {
29 | mConnection = ServiceConnection(this)
30 | }
31 | CustomTabsClient.bindCustomTabsService(mContext, CUSTOM_TAB_PACKAGE_NAME, mConnection)
32 | }
33 |
34 | fun unbindCustomTabsService() {
35 | if (mConnection == null) return
36 | mContext.unbindService(mConnection!!)
37 | mClient = null
38 | mConnection = null
39 | }
40 |
41 | public override fun onServiceConnected(client: CustomTabsClient) {
42 | mClient = client
43 | }
44 |
45 | public override fun onServiceDisconnected() {
46 | mClient = null
47 | }
48 |
49 | companion object {
50 | private val CUSTOM_TAB_PACKAGE_NAME: String = "com.android.chrome"
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/local/QuakesLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.local
2 |
3 | import com.google.common.base.Preconditions
4 | import com.inspiringteam.xchange.data.models.Quake
5 | import com.inspiringteam.xchange.data.source.QuakesDataSource
6 | import com.inspiringteam.xchange.di.scopes.AppScoped
7 | import com.inspiringteam.xchange.util.schedulers.BaseSchedulerProvider
8 | import io.reactivex.Completable
9 | import io.reactivex.Single
10 | import java.util.*
11 | import javax.inject.Inject
12 |
13 | /**
14 | * Concrete implementation of the Local Data Source
15 | */
16 | @AppScoped
17 | class QuakesLocalDataSource @Inject constructor(
18 | quakesDao: QuakesDao,
19 | schedulerProvider: BaseSchedulerProvider
20 | ) : QuakesDataSource {
21 |
22 | private val mQuakesDao: QuakesDao
23 | private val mSchedulerProvider: BaseSchedulerProvider
24 |
25 | init {
26 | Preconditions.checkNotNull(schedulerProvider, "scheduleProvider cannot be null")
27 | Preconditions.checkNotNull(quakesDao, "quakesDao cannot be null")
28 | mQuakesDao = quakesDao
29 | mSchedulerProvider = schedulerProvider
30 | }
31 |
32 | /**
33 | * Items are retrieved from disk
34 | */
35 | override fun getQuakes(): Single> {
36 | return mQuakesDao.getQuakes().map { it.toList() }
37 | }
38 |
39 | override fun getQuake(quakeId: String): Single {
40 | return mQuakesDao.getQuakeById(quakeId)
41 | }
42 |
43 | override fun saveQuakes(quakes: List) {
44 | Preconditions.checkNotNull(quakes)
45 | for (quake in quakes) saveQuake(quake)
46 | }
47 |
48 | override fun saveQuake(quake: Quake) {
49 | Preconditions.checkNotNull(quake)
50 | Completable.fromRunnable { mQuakesDao.insertQuake(quake) }
51 | .subscribeOn(mSchedulerProvider.io()).subscribe()
52 | }
53 |
54 | override fun deleteAllQuakes() {
55 | Completable.fromRunnable { mQuakesDao.deleteQuakes() }
56 | .subscribeOn(mSchedulerProvider.io()).subscribe()
57 | }
58 |
59 | override fun deleteQuake(quakeId: String) {
60 | Completable.fromRunnable { mQuakesDao.deleteQuakeById(quakeId) }
61 | .subscribeOn(mSchedulerProvider.io()).subscribe()
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/.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 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/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/layout/quake_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
26 |
27 |
38 |
39 |
40 |
45 |
46 |
54 |
55 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/app/src/test/java/com/inspiringteam/xchange/data/source/remote/QuakesRemoteDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.remote
2 |
3 | import com.inspiringteam.xchange.data.models.Quake
4 | import com.inspiringteam.xchange.data.models.QuakeWrapper
5 | import com.inspiringteam.xchange.data.models.QuakesResponse
6 | import io.reactivex.Single
7 | import io.reactivex.subscribers.TestSubscriber
8 | import org.junit.Assert
9 | import org.junit.Before
10 | import org.junit.Test
11 | import org.mockito.Mock
12 | import org.mockito.Mockito
13 | import org.mockito.MockitoAnnotations
14 | import java.io.IOException
15 | import java.util.*
16 |
17 | /**
18 | * Test
19 | * SUT - [QuakesRemoteDataSource]
20 | */
21 | class QuakesRemoteDataSourceTest constructor() {
22 | @Mock
23 | var mQuakeService: QuakesApiService? = null
24 | private var mRemoteDataSource: QuakesRemoteDataSource? = null
25 | @Before
26 | @Throws(Exception::class)
27 | fun setup() {
28 | // init mocks
29 | MockitoAnnotations.initMocks(this)
30 |
31 | // get reference to the class in test
32 | mRemoteDataSource = QuakesRemoteDataSource((mQuakeService)!!)
33 | }
34 |
35 | @Test
36 | fun testPreConditions() {
37 | Assert.assertNotNull(mRemoteDataSource)
38 | }
39 |
40 | /**
41 | * Test scenario states:
42 | * Remote Source should get the correct results in success scenario
43 | */
44 | @Test
45 | @Throws(Exception::class)
46 | fun testRemoteApiResponse_success() {
47 | val testSubscriber: TestSubscriber> =
48 | TestSubscriber>()
49 | val listQuakes: kotlin.collections.MutableList = ArrayList()
50 |
51 | // set up mock response
52 | val mockQuakeResponse: QuakesResponse = QuakesResponse()
53 | val tempQuake: Quake = Quake("id", "location")
54 | listQuakes.add(tempQuake)
55 | val quakeWrapper: QuakeWrapper = QuakeWrapper(tempQuake)
56 | val wrapperList: kotlin.collections.MutableList = ArrayList()
57 | wrapperList.add(quakeWrapper)
58 | mockQuakeResponse.setquakeWrapperList(wrapperList)
59 |
60 | // prepare fake response
61 | Mockito.`when`(mQuakeService!!.quakes)
62 | .thenReturn(Single.just(mockQuakeResponse))
63 |
64 | // trigger response
65 | mRemoteDataSource!!.getQuakes().toFlowable().subscribe(testSubscriber)
66 | val result: kotlin.collections.MutableList = testSubscriber.values().get(0)
67 | testSubscriber.assertValue(listQuakes)
68 | }
69 |
70 | /**
71 | * Test scenario states:
72 | * Remote Source should get the correct results in failure scenario
73 | */
74 | @Test
75 | @Throws(Exception::class)
76 | fun testRemoteApiResponse_failure() {
77 | val testSubscriber: TestSubscriber> =
78 | TestSubscriber>()
79 |
80 | // prepare fake exception
81 | val exception: Throwable = IOException()
82 |
83 | // prepare fake response
84 | Mockito.`when`(mQuakeService!!.quakes).thenReturn(Single.error(exception))
85 |
86 | // assume the repository calls the remote DataSource
87 | mRemoteDataSource!!.getQuakes().toFlowable().subscribe(testSubscriber)
88 | testSubscriber.assertError(IOException::class.java)
89 | }
90 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/source/QuakesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source
2 |
3 | import com.google.common.base.Preconditions
4 | import com.inspiringteam.xchange.data.models.Quake
5 | import com.inspiringteam.xchange.data.source.scopes.Local
6 | import com.inspiringteam.xchange.data.source.scopes.Remote
7 | import com.inspiringteam.xchange.di.scopes.AppScoped
8 | import com.inspiringteam.xchange.util.ConnectivityUtils.OnlineChecker
9 | import com.inspiringteam.xchange.util.DisplayUtils.SortUtils
10 | import io.reactivex.Single
11 | import java.util.*
12 | import javax.inject.Inject
13 |
14 | /**
15 | * Consists of a functional set of methods that allow ViewModels to access appropriate data flows
16 | */
17 | @AppScoped
18 | class QuakesRepository
19 | /**
20 | * Dagger allows us to have a single instance of the repository throughout the app
21 | *
22 | * @param quakesRemoteDataSource the backend data source (Remote Source)
23 | * @param quakesLocalDataSource the device storage data source (Local Source)
24 | */ @Inject constructor(
25 | @param:Remote private val quakesRemoteDataSource: QuakesDataSource,
26 | @param:Local private val quakesLocalDataSource: QuakesDataSource,
27 | private val onlineChecker: OnlineChecker
28 | ) : QuakesDataSource {
29 | /**
30 | * The retrieval logic sets the Local Source as the primary source
31 | * In case of an active internet connection and the absence of Local database
32 | * or if it contains stale data, the Remote Source is queried and the Local one is refreshed
33 | */
34 | override fun getQuakes(): Single> {
35 | return quakesLocalDataSource.getQuakes()
36 | .flatMap { data: List ->
37 | if (onlineChecker.isOnline() && (data.isEmpty() || isStale(data)))
38 | return@flatMap getFreshQuakes()
39 | Single.just(SortUtils.sortByNewest(data))
40 | }
41 | }
42 |
43 | override fun getQuake(quakeId: String): Single {
44 | Preconditions.checkNotNull(quakeId)
45 | return quakesLocalDataSource.getQuake(quakeId)
46 | }
47 |
48 | override fun saveQuakes(quakes: List) {
49 | Preconditions.checkNotNull(quakes)
50 | quakesLocalDataSource.saveQuakes(quakes)
51 | quakesRemoteDataSource.saveQuakes(quakes)
52 | }
53 |
54 | override fun saveQuake(quake: Quake) {
55 | Preconditions.checkNotNull(quake)
56 | quakesLocalDataSource.saveQuake(quake)
57 | quakesRemoteDataSource.saveQuake(quake)
58 | }
59 |
60 | override fun deleteAllQuakes() {
61 | quakesLocalDataSource.deleteAllQuakes()
62 | quakesRemoteDataSource.deleteAllQuakes()
63 | }
64 |
65 | override fun deleteQuake(quakeId: String) {
66 | quakesLocalDataSource.deleteQuake(quakeId)
67 | quakesRemoteDataSource.deleteQuake(quakeId)
68 | }
69 |
70 | /**
71 | * Helper methods, should be encapsulated
72 | */
73 | private fun isStale(data: List): Boolean {
74 | // It's enough for 1 item to be stale
75 | return !data[0].isUpToDate
76 | }
77 |
78 | /**
79 | * Contains data refreshing logic
80 | * Both sources are emptied, then new items are retrieved from querying the Remote Source
81 | * and finally, sources are replenished
82 | */
83 | private fun getFreshQuakes(): Single> {
84 | deleteAllQuakes()
85 | return quakesRemoteDataSource
86 | .getQuakes()
87 | .doOnSuccess { quakes: List ->
88 | saveQuakes(quakes)
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/data/models/Quake.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.models
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.Ignore
6 | import androidx.room.PrimaryKey
7 | import com.google.common.base.Objects
8 | import com.google.gson.annotations.Expose
9 | import com.google.gson.annotations.SerializedName
10 |
11 | /**
12 | * Immutable model class for a Quake.
13 | */
14 | @Entity(tableName = "quakes")
15 | class Quake {
16 | @PrimaryKey
17 | @ColumnInfo(name = "id")
18 | @SerializedName("code")
19 | @Expose
20 | var id: String = ""
21 |
22 | @SerializedName("mag")
23 | @ColumnInfo(name = "mag")
24 | @Expose
25 | var magnitude: Double? = null
26 |
27 | @SerializedName("time")
28 | @ColumnInfo(name = "time")
29 | @Expose
30 | var timeStamp: Long? = null
31 |
32 | @ColumnInfo(name = "timeAdded")
33 | @Expose
34 | var timeStampAdded: Long? = null
35 |
36 | @SerializedName("place")
37 | @ColumnInfo(name = "place")
38 | @Expose
39 | var location: String
40 |
41 | @SerializedName("url")
42 | @ColumnInfo(name = "url")
43 | @Expose
44 | var url: String? = null
45 |
46 | @SerializedName("sig")
47 | @ColumnInfo(name = "sig")
48 | @Expose
49 | var gravity: Int = 0
50 |
51 | @Ignore
52 | constructor(id: String, location: String) {
53 | this.id = id
54 | this.location = location
55 | }
56 |
57 | @Ignore
58 | constructor(id: String, location: String, url: String?) {
59 | this.id = id
60 | this.location = location
61 | this.url = url
62 | }
63 |
64 | @Ignore
65 | constructor(magnitude: Double?, location: String) {
66 | this.magnitude = magnitude
67 | this.location = location
68 | }
69 |
70 | @Ignore
71 | constructor(magnitude: Double?, location: String, timeStamp: Long?, timeStampAdded: Long?) {
72 | this.magnitude = magnitude
73 | this.timeStamp = timeStamp
74 | this.location = location
75 | this.timeStampAdded = timeStampAdded
76 | }
77 |
78 | constructor(
79 | id: String,
80 | magnitude: Double?,
81 | timeStamp: Long?,
82 | timeStampAdded: Long?,
83 | location: String,
84 | url: String?,
85 | gravity: Int
86 | ) {
87 | this.id = id
88 | this.magnitude = magnitude
89 | this.timeStamp = timeStamp
90 | this.timeStampAdded = timeStampAdded
91 | this.location = location
92 | this.url = url
93 | this.gravity = gravity
94 | }
95 |
96 | val isUpToDate: Boolean
97 | get() {
98 | return System.currentTimeMillis() - (timeStampAdded)!! < STALE_MS
99 | }
100 |
101 | public override fun hashCode(): Int {
102 | return Objects.hashCode(id, location, magnitude)
103 | }
104 |
105 | public override fun equals(obj: Any?): Boolean {
106 | if (this === obj) return true
107 | if (obj == null || javaClass != obj.javaClass) return false
108 | val quake: Quake = obj as Quake
109 | return (Objects.equal(id, quake.id) &&
110 | Objects.equal(magnitude, quake.magnitude) &&
111 | Objects.equal(location, quake.location))
112 | }
113 |
114 | public override fun toString(): String {
115 | return location + magnitude
116 | }
117 |
118 | companion object {
119 | @Ignore
120 | private val STALE_MS: Long = (5 * 60 * 1000 // Data is stale after 5 minutes
121 | ).toLong()
122 | }
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/QuakesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.inspiringteam.xchange.R
5 | import com.inspiringteam.xchange.data.models.Quake
6 | import com.inspiringteam.xchange.data.source.QuakesRepository
7 | import com.inspiringteam.xchange.di.scopes.AppScoped
8 | import com.inspiringteam.xchange.util.chromeTabsUtils.ChromeTabsWrapper
9 | import io.reactivex.Observable
10 | import io.reactivex.Single
11 | import io.reactivex.subjects.BehaviorSubject
12 | import io.reactivex.subjects.PublishSubject
13 | import javax.inject.Inject
14 |
15 | /**
16 | * ViewModel for quakes screen
17 | */
18 | @AppScoped
19 | class QuakesViewModel @Inject constructor(
20 | private val repository: QuakesRepository,
21 | private val tabsWrapper: ChromeTabsWrapper
22 | ) : ViewModel() {
23 | // Use a BehaviourSubject because we are interested in the last object that was emitted before
24 | // subscribing. We ensure that the loading indicator has the correct visibility.
25 | private val loadingIndicatorSubject: BehaviorSubject =
26 | BehaviorSubject.createDefault(false)
27 |
28 | // Use a PublishSubject because we are not interested in the last object that was emitted
29 | // before subscribing. We avoid displaying the snackBar multiple times.
30 | private val snackBarTextView: PublishSubject = PublishSubject.create()
31 |
32 | /**
33 | * @return a stream of ids that should be displayed in the snackBar
34 | */
35 | val snackBarMessage: Observable
36 | get() = snackBarTextView.hide()
37 |
38 | /**
39 | * @return a stream that emits true if the progress indicator should be displayed, false otherwise
40 | */
41 | val loadingIndicatorVisibility: Observable
42 | get() = loadingIndicatorSubject.hide()
43 |
44 | /**
45 | * @return the model for the quakes screen
46 | */
47 | fun getUiModel(isForcedCall: Boolean): Single {
48 | return getQuakeItems(isForcedCall)
49 | .doOnSubscribe { loadingIndicatorSubject.onNext(true) }
50 | .doOnSuccess { loadingIndicatorSubject.onNext(false) }
51 | .doOnError { snackBarTextView.onNext(R.string.loading_quakes_error) }
52 | .map { quakes: List -> constructQuakesModel(quakes) }
53 | }
54 |
55 | fun bindTabsService() = tabsWrapper.unbindCustomTabsService()
56 |
57 | fun unbindTabsService() = tabsWrapper.unbindCustomTabsService()
58 |
59 | private fun getNoQuakesModel() = NoQuakesModel(R.string.no_quakes)
60 |
61 | private fun handleQuakeClicked(quake: Quake) = tabsWrapper.openCustomtab(quake.url)
62 |
63 | private fun getQuakeItems(isForcedCall: Boolean): Single> {
64 | if (isForcedCall)
65 | repository.deleteAllQuakes()
66 | return repository.getQuakes()
67 | .flatMap { list: List? ->
68 | Observable
69 | .fromIterable(list)
70 | .map { quake: Quake -> constructQuakeItem(quake) }.toList()
71 | }
72 | }
73 |
74 | private fun constructQuakeItem(quake: Quake): QuakeItem {
75 | return QuakeItem(quake) { handleQuakeClicked(quake) }
76 | }
77 |
78 | private fun constructQuakesModel(quakes: List): QuakesUiModel {
79 | val isQuakesListVisible = quakes.isNotEmpty()
80 | val isNoQuakesViewVisible = !isQuakesListVisible
81 | var noQuakesModel: NoQuakesModel? = null
82 | if (quakes.isEmpty())
83 | noQuakesModel = getNoQuakesModel()
84 | return QuakesUiModel(
85 | isQuakesListVisible,
86 | quakes,
87 | isNoQuakesViewVisible,
88 | noQuakesModel
89 | )
90 | }
91 |
92 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/inspiringteam/xchange/data/source/local/QuakesDaoTest.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.local
2 |
3 | import androidx.room.Room
4 | import androidx.test.InstrumentationRegistry
5 | import androidx.test.runner.AndroidJUnit4
6 | import com.inspiringteam.xchange.data.models.Quake
7 | import io.reactivex.subscribers.TestSubscriber
8 | import org.junit.After
9 | import org.junit.Before
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import java.util.*
13 |
14 | @RunWith(AndroidJUnit4::class)
15 | class QuakesDaoTest constructor() {
16 | private var mDatabase: QuakesDatabase? = null
17 | private var mQuakeTestSubscriber: TestSubscriber? = null
18 | private var mQuakesTestSubscriber: TestSubscriber>? = null
19 | @Before
20 | fun initDb() {
21 | // using an in-memory database because the information stored here disappears when the
22 | // process is killed
23 | mDatabase = Room.inMemoryDatabaseBuilder(
24 | InstrumentationRegistry.getContext(),
25 | QuakesDatabase::class.java
26 | ).build()
27 | mQuakeTestSubscriber = TestSubscriber()
28 | mQuakesTestSubscriber = TestSubscriber>()
29 | }
30 |
31 | @After
32 | fun closeDb() {
33 | mDatabase!!.close()
34 | }
35 |
36 | /**
37 | * Test scenario states:
38 | * Upon insertion of a quake, the correct item is retrieved
39 | */
40 | @Test
41 | fun insertQuakeAndGetById() {
42 | // insert quake
43 | mDatabase!!.quakesDao().insertQuake(QUAKES.get(0))
44 |
45 | // getting the Quake by id from the database
46 | mDatabase!!.quakesDao()
47 | .getQuakeById(QUAKES.get(0).id).toFlowable().subscribe(mQuakeTestSubscriber)
48 |
49 | // the loaded data contains the expected values
50 | mQuakeTestSubscriber.assertValue(QUAKES.get(0))
51 | }
52 |
53 | /**
54 | * Test scenario states:
55 | * Upon insertion of quakes, the correct list is retrieved
56 | */
57 | @Test
58 | fun insertQuakesAndGet() {
59 | // insert quakes
60 | mDatabase!!.quakesDao().insertQuake(QUAKES.get(0))
61 | mDatabase!!.quakesDao().insertQuake(QUAKES.get(1))
62 | mDatabase!!.quakesDao().insertQuake(QUAKES.get(2))
63 |
64 |
65 | // getting quakes from the database
66 | mDatabase!!.quakesDao()
67 | .getQuakes().toFlowable().subscribe(mQuakesTestSubscriber)
68 |
69 | // the loaded data contains the expected values
70 | mQuakesTestSubscriber.assertValue(QUAKES)
71 | }
72 |
73 | /**
74 | * Test scenario states:
75 | * Upon insertion of a conflictual quake, the latter one should be retrieved
76 | */
77 | @Test
78 | fun insertQuakeAndReplaceOnConflict() {
79 | val conflictualQuake: Quake = Quake("id1", "locationN")
80 |
81 | // insert initial quake
82 | mDatabase!!.quakesDao().insertQuake(QUAKES.get(0))
83 |
84 | // insert conflictual quake
85 | mDatabase!!.quakesDao().insertQuake(conflictualQuake)
86 |
87 |
88 | // getting quake from the database
89 | mDatabase!!.quakesDao()
90 | .getQuakeById("id1").toFlowable().subscribe(mQuakeTestSubscriber)
91 |
92 | // the loaded data contains the expected values
93 | mQuakeTestSubscriber!!.assertValue(conflictualQuake)
94 | }
95 |
96 | /**
97 | * Test scenario states:
98 | * Upon insertion of 1 quake and deletion of all records, we should retrieve an empty list
99 | * fom the database
100 | */
101 | @Test
102 | fun deleteQuakeAndGetQuakes_Scenario() {
103 | // given a quake (item) inserted
104 | mDatabase!!.quakesDao().insertQuake(QUAKES.get(0))
105 |
106 | // deleting all data
107 | mDatabase!!.quakesDao().deleteQuakes()
108 |
109 | // getting the data
110 | mDatabase!!.quakesDao().getQuakes().toFlowable().subscribe(mQuakesTestSubscriber)
111 |
112 | // the list should be empty
113 | mQuakesTestSubscriber!!.assertValue(ArrayList())
114 | }
115 |
116 | companion object {
117 | private val QUAKES: MutableList = Arrays.asList(
118 | Quake("id1", "location1"),
119 | Quake("id2", "location2"),
120 | Quake("id3", "location3")
121 | )
122 | }
123 | }
--------------------------------------------------------------------------------
/.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 | xmlns:android
29 |
30 | ^$
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | xmlns:.*
40 |
41 | ^$
42 |
43 |
44 | BY_NAME
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:id
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | .*:name
63 |
64 | http://schemas.android.com/apk/res/android
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | name
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | style
85 |
86 | ^$
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | .*
96 |
97 | ^$
98 |
99 |
100 | BY_NAME
101 |
102 |
103 |
104 |
105 |
106 |
107 | .*
108 |
109 | http://schemas.android.com/apk/res/android
110 |
111 |
112 | ANDROID_ATTRIBUTE_ORDER
113 |
114 |
115 |
116 |
117 |
118 |
119 | .*
120 |
121 | .*
122 |
123 |
124 | BY_NAME
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/inspiringteam/xchange/data/source/local/QuakesLocalDataSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source.local
2 |
3 | import androidx.room.Room
4 | import androidx.test.runner.AndroidJUnit4
5 | import com.inspiringteam.xchange.data.models.Quake
6 | import com.inspiringteam.xchange.util.schedulers.BaseSchedulerProvider
7 | import com.inspiringteam.xchange.util.schedulers.ImmediateSchedulerProvider
8 | import org.junit.Before
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 |
12 | /**
13 | * Test
14 | * SUT - [QuakesLocalDataSource]
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class QuakesLocalDataSourceTest constructor() {
18 | private var mLocalDataSource: QuakesLocalDataSource? = null
19 | private var mDatabase: QuakesDatabase? = null
20 | private var mSchedulerProvider: BaseSchedulerProvider? = null
21 | @Before
22 | fun setup() {
23 | // using an in-memory database for testing, since it doesn't survive killing the process
24 | mDatabase = Room.inMemoryDatabaseBuilder(
25 | androidx.test.InstrumentationRegistry.getContext(),
26 | QuakesDatabase::class.java
27 | )
28 | .build()
29 | val quakesDao: QuakesDao = mDatabase!!.quakesDao()
30 | mSchedulerProvider = ImmediateSchedulerProvider()
31 |
32 | // Make sure that we're not keeping a reference to the wrong instance.
33 | mLocalDataSource = QuakesLocalDataSource(quakesDao, mSchedulerProvider)
34 | }
35 |
36 | @org.junit.After
37 | fun cleanUp() {
38 | mDatabase!!.quakesDao().deleteQuakes()
39 | mDatabase!!.close()
40 | }
41 |
42 | @org.junit.Test
43 | fun testPreConditions() {
44 | junit.framework.Assert.assertNotNull(mLocalDataSource)
45 | }
46 |
47 | /**
48 | * Test scenario states:
49 | * Local Source should get the correct result upon saving a quake
50 | */
51 | @org.junit.Test
52 | fun saveQuake_retrievesQuake() {
53 | // When saved into the quakes repository
54 | mLocalDataSource!!.saveQuake(QUAKE)
55 |
56 | // Then the quake can be retrieved from the persistent repository
57 | val testSubscriber: io.reactivex.subscribers.TestSubscriber =
58 | io.reactivex.subscribers.TestSubscriber()
59 | mLocalDataSource!!.getQuake(QUAKE.id).toFlowable().subscribe(testSubscriber)
60 | testSubscriber.assertValue(QUAKE)
61 | }// Given 2 new quakes in the persistent repository
62 |
63 | // Then the quakes can be retrieved from the persistent repository
64 | /**
65 | * Test scenario states:
66 | * Local Source should get the correct result upon saving quakes
67 | */
68 | @get:Test
69 | val quakes_retrieveSavedQuakes: Unit
70 | get() {
71 | // Given 2 new quakes in the persistent repository
72 | val newQuake1: Quake = Quake("id1", "location1")
73 | mLocalDataSource!!.saveQuake(newQuake1)
74 | val newQuake2: Quake = Quake("id2", "location2")
75 | mLocalDataSource!!.saveQuake(newQuake2)
76 |
77 | // Then the quakes can be retrieved from the persistent repository
78 | val testSubscriber: io.reactivex.subscribers.TestSubscriber> =
79 | io.reactivex.subscribers.TestSubscriber>()
80 | mLocalDataSource!!.getQuakes().toFlowable().subscribe(testSubscriber)
81 | val result: kotlin.collections.MutableList = testSubscriber.values().get(0)
82 | org.junit.Assert.assertThat>(
83 | result,
84 | org.hamcrest.core.IsCollectionContaining.hasItems(newQuake1, newQuake2)
85 | )
86 | }//Given that no Quake has been saved
87 | //When querying for a quake, no values are returned.
88 | /**
89 | * Test scenario states:
90 | * Local Source should get the correct result upon having no data
91 | */
92 | @get:Test
93 | val quake_whenQuakeNotSaved: Unit
94 | get() {
95 | //Given that no Quake has been saved
96 | //When querying for a quake, no values are returned.
97 | val testSubscriber: io.reactivex.subscribers.TestSubscriber =
98 | io.reactivex.subscribers.TestSubscriber()
99 | mLocalDataSource!!.getQuake("some_id").toFlowable().subscribe(testSubscriber)
100 | testSubscriber.assertNoValues()
101 | }
102 |
103 | /**
104 | * Test scenario states:
105 | * Local Source should get the correct result upon emptying source
106 | */
107 | @org.junit.Test
108 | fun deleteAllQuakes_emptyListOfRetrievedQuakes() {
109 | // Given a new quake in the persistent repository
110 | mLocalDataSource!!.saveQuake(QUAKE)
111 |
112 | // When all quakes are deleted
113 | mLocalDataSource!!.deleteAllQuakes()
114 |
115 | // Then the retrieved quakes is an empty list
116 | val testSubscriber: io.reactivex.subscribers.TestSubscriber> =
117 | io.reactivex.subscribers.TestSubscriber>()
118 | mLocalDataSource!!.getQuakes().toFlowable().subscribe(testSubscriber)
119 | val result: MutableList = testSubscriber.values().get(0)
120 | org.junit.Assert.assertThat(result.size, org.hamcrest.core.Is.`is`(0))
121 | }
122 |
123 | companion object {
124 | private val QUAKE: Quake = Quake("id", "location")
125 | }
126 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android-MVVM-RxJava2-Dagger2
2 |
3 | ## ! Sample has been migrated to Kotlin.
4 |
5 | This repository contains a detailed sample application that uses MVVM as its presentation layer pattern. **The app aims to be extremely flexible to creating variants for automated and manual testing.** Also, the project implements and follows the guidelines presented in Google Sample [MVVM+RXJAVA-android](https://github.com/googlesamples/android-architecture/tree/dev-todo-mvvm-rxjava/).
6 |
7 | Essential dependencies are Dagger2 with Dagger-android, RxJava2 with RxAndroid, Room, Retrofit and Espresso. Other noteworthy dependencies would be Mockito, Chrome CustomTabs and Guava.
8 | ## App Demo
9 | This app displays latest earthquakes from all around the world. A number of quakes is fetched realtime upon request. If offline, the app displays the most recent loaded quakes.
10 |
11 | 
12 | 
13 | 
14 | ## Presentation Layer
15 | MVVM pattern is integrated to facilitate testing and to allow separating the user interface logic from business logic.
16 |
17 | As Views were passive in MVP, here the View layer is much more flexibile as an indefinite number of Views can bind to a ViewModel. Also, MVVM enforces a clear separation between Views and their master - ViewModel, as the latter holds no reference to Views. The model layer is completely isolated and centralized through the repository pattern.
18 |
19 | 
20 |
21 | ## Model Layer
22 | The model layer is structured on repository pattern so that the ViewModel has no clue on the origins of the data.
23 |
24 | The repository handles data interactions and transactions from two main data sources - local and remote:
25 | - `QuakesRemoteDataSource` defined by a REST API consumed with [Retrofit](http://square.github.io/retrofit)
26 | - `QuakesLocalDataSource` defined by a SQL database consumed with [Room](https://developer.android.com/topic/libraries/architecture/room)
27 |
28 | There are two main use-cases, online and offline. In both use cases, `QuakesLocalDataSource` has priority. In the online use-case if the local data is stale, new data is fetched from the `NewsRemoteDataSource` and the repository data is refreshed. In case of no internet connection, `QuakesLocalDataSource` is always queried.
29 |
30 | Decoupling is also inforced within the Model layer (entirely consisted by `QuakesRepository`). Therefore, lower level components (which are the data sources: `QuakesRemoteDataSource` and `QuakesLocalDatasource`) are decoupled through `QuakesDataSource` interface. Also, through their dependence on the same interface, these data sources are interchangeable.
31 |
32 | In this manner, the project respects the DIP (Dependency Inversion Principle) as both low and high level modules depend on abstractions.
33 | ### Reactive approach
34 | It is extremely important to note that this project is not essentially a reactive app as it is not capitalizing the enormous potential of a fully reactive approach.
35 | Nevertheless, the app was intended to have a flexible and efficient testing capability, rather than a fully reactive build.
36 |
37 | Even in this case, we are able to notice RxJava's benefits when data is being retrieved from the repository through different sources and then is channeled through the ViewModel and finally consumed in Views.
38 | - Data Flow is centralized.
39 | - Threading is much easier, with no need for the dreaded `AsyncTasks`.
40 | - Error handling is straightforward and comfortable.
41 | ## Dependency Injection
42 | Dagger2 is used to externalize the creation of dependencies from the classes that use them. Android specific helpers are provided by `Dagger-Android` and the most significant advantage is that they generate a subcomponent for each `Activity` through a new code generator.
43 | Such subcomponent is:
44 | ```kotlin
45 | @ActivityScoped
46 | @ContributesAndroidInjector(modules = [QuakesModule::class])
47 | abstract fun quakesActivity(): QuakesActivity?
48 | ```
49 | The below diagram illustrates the most significant relations between components and modules. An important note is the fact that the ViewModel is now `@AppScoped` whereas in MVP the Presenter is `@ActivityScoped` - this is mainly due to the fact that in MVVM the ViewModel is a Android Architecture Component so therefore has a greater scope than Views. You can also get a quick glance on how annotations help us define custom Scopes in order to properly handle classes instantiation.
50 | 
51 | _Note: The above diagram might help you understand how Dagger-android works. Also, only essential components/modules/objects are included here, this is suggested by the "…"_
52 | ## Testing
53 | The apps' components are extremely easy to test due to DI achieved through Dagger and the project's structure, but as well for the reason that the data flow is centralized with RxJava which results in highly testable pieces of code.
54 |
55 | Unit tests are conducted with the help of Mockito and Instrumentation tests with the help of Espresso.
56 | ## Strong points
57 | - Decoupling level is high.
58 | - Data Flow is centralized through RxJava.
59 | - Possess high flexibility to create variants for automated and manual testing.
60 | - Possess lightweight structure due to MVVM presentation pattern.
61 | - Is scalable and easy to expand.
62 | ## Weak points
63 | - Possess high code base - simpler approaches will certainly lower code size
64 | - Possess medium complexity - other approaches might lower complexity and increase efficiency.
65 |
66 | # Final notes:
67 | - The app is not a polished ready-to-publish product, it acts as a boilerplate project or as a starting point for android enthusiasts out there.
68 | - Using this project as your starting point and expanding it is also encouraged, as at this point it is very easy to add new modules.
69 |
--------------------------------------------------------------------------------
/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/test/java/com/inspiringteam/xchange/quakes/QuakesViewModelTest.java:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.quakes;
2 |
3 | import com.google.common.collect.Lists;
4 | import com.inspiringteam.xchange.R;
5 | import com.inspiringteam.xchange.data.models.Quake;
6 | import com.inspiringteam.xchange.data.source.QuakesRepository;
7 | import com.inspiringteam.xchange.ui.quakes.QuakeItem;
8 | import com.inspiringteam.xchange.ui.quakes.QuakesUiModel;
9 | import com.inspiringteam.xchange.ui.quakes.QuakesViewModel;
10 | import com.inspiringteam.xchange.util.chromeTabsUtils.ChromeTabsWrapper;
11 |
12 | import org.junit.Before;
13 | import org.junit.Test;
14 | import org.mockito.Mock;
15 | import org.mockito.MockitoAnnotations;
16 |
17 | import java.util.ArrayList;
18 | import java.util.List;
19 |
20 | import io.reactivex.Single;
21 | import io.reactivex.observers.TestObserver;
22 |
23 |
24 | import static org.junit.Assert.assertEquals;
25 | import static org.junit.Assert.assertFalse;
26 | import static org.junit.Assert.assertNotNull;
27 | import static org.junit.Assert.assertNull;
28 | import static org.junit.Assert.assertTrue;
29 | import static org.mockito.Matchers.eq;
30 | import static org.mockito.Mockito.verify;
31 | import static org.mockito.Mockito.when;
32 |
33 |
34 | /**
35 | * Unit test for {@link QuakesViewModel}
36 | */
37 | public class QuakesViewModelTest {
38 | private static List QUAKES;
39 |
40 | private QuakesViewModel mViewModel;
41 |
42 | private TestObserver mQuakesSubscriber;
43 |
44 | private TestObserver mProgressIndicatorSubscriber;
45 |
46 | private TestObserver mSnackbarTextSubscriber;
47 |
48 | @Mock
49 | private QuakesRepository mQuakesRepository;
50 |
51 | @Mock
52 | private ChromeTabsWrapper mChromeTabsWrapper;
53 |
54 | @Before
55 | public void setupQuakesViewModel() {
56 | // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
57 | // inject the mocks in the test the initMocks method needs to be called.
58 | MockitoAnnotations.initMocks(this);
59 |
60 | // Get a reference to the class under test
61 | mViewModel = new QuakesViewModel(mQuakesRepository, mChromeTabsWrapper);
62 |
63 |
64 | QUAKES = Lists.newArrayList(new Quake("id1", "location1"),
65 | new Quake("id2", "location2"));
66 |
67 | mQuakesSubscriber = new TestObserver<>();
68 | mProgressIndicatorSubscriber = new TestObserver<>();
69 | mSnackbarTextSubscriber = new TestObserver<>();
70 | }
71 |
72 | @Test
73 | public void progressIndicator_emits_whenSubscribedToData() {
74 | // Given that the quake repository never emits
75 | when(mQuakesRepository.getQuakes()).thenReturn(Single.never());
76 |
77 | // Given that we are subscribed to the progress indicator
78 | mProgressIndicatorSubscriber = mViewModel.getLoadingIndicatorVisibility().test();
79 |
80 | // When subscribed to the quakes model
81 | mViewModel.getUiModel(false).subscribe();
82 |
83 | // The progress indicator emits initially false and then true
84 | mProgressIndicatorSubscriber.assertValues(false, true);
85 | }
86 |
87 | @Test
88 | public void snackbarText_emits_whenError_whenRetrievingData() {
89 | // Given an error when retrieving quakes
90 | when(mQuakesRepository.getQuakes()).thenReturn(Single.error(new RuntimeException()));
91 |
92 | // Given that we are subscribed to the snackbar text
93 | mViewModel.getSnackBarMessage().subscribe(mSnackbarTextSubscriber);
94 |
95 | // When subscribed to the quakes model
96 | mViewModel.getUiModel(false).subscribe(mQuakesSubscriber);
97 |
98 | // The snackbar emits an error message
99 | mSnackbarTextSubscriber.assertValue(R.string.loading_quakes_error);
100 | }
101 |
102 | @Test
103 | public void getQuakesModel_emits_whenQuakes() {
104 | // Given that we are subscribed to the emissions of the UI model
105 | withQuakesInRepositoryAndSubscribed(QUAKES);
106 |
107 | // The Quakes model containing the list of Quakes is emitted
108 | mQuakesSubscriber.assertValueCount(1);
109 | QuakesUiModel model = mQuakesSubscriber.values().get(0);
110 | assertQuakesModelWithQuakesVisible(model);
111 | }
112 |
113 | @Test
114 | public void forceUpdateQuakes_updatesQuakesRepository() {
115 | // Given that the quake repository never emits
116 | when(mQuakesRepository.getQuakes()).thenReturn(Single.never());
117 |
118 | // When calling force update
119 | mViewModel.getUiModel(true);
120 |
121 | // The quakes are refreshed in the repository
122 | verify(mQuakesRepository).deleteAllQuakes();
123 | }
124 |
125 | @Test
126 | public void QuakeItem_tapAction_opensQuakeDetails() {
127 | Quake tempQuake = new Quake("id", "location", "https://google.com");
128 |
129 | // Given a quake
130 | withQuakeInRepositoryAndSubscribed(tempQuake);
131 | // And list of quake items is emitted
132 | List items = mQuakesSubscriber.values().get(0).getQuakes();
133 | QuakeItem QuakeItem = items.get(0);
134 |
135 | // When triggering the click action
136 | QuakeItem.getOnClickAction().call();
137 |
138 | // Opening of the Quake details is called with the correct action
139 | verify(mChromeTabsWrapper).openCustomtab(tempQuake.getUrl());
140 | }
141 |
142 | private void withQuakeInRepositoryAndSubscribed(Quake Quake) {
143 | List Quakes = new ArrayList<>();
144 | Quakes.add(Quake);
145 | withQuakesInRepositoryAndSubscribed(Quakes);
146 | }
147 |
148 | private void assertQuakesModelWithQuakesVisible(QuakesUiModel model) {
149 | assertTrue(model.isQuakesListVisible());
150 | assertQuakeItems(model.getQuakes());
151 | assertFalse(model.isNoQuakesViewVisible());
152 | assertNull(model.getNoQuakesModel());
153 | }
154 |
155 | private void withQuakesInRepositoryAndSubscribed(List quakes) {
156 | // Given that the quake repository returns quakes
157 | when(mQuakesRepository.getQuakes()).thenReturn(Single.just(quakes));
158 |
159 | // Given that we are subscribed to the quakes
160 | mViewModel.getUiModel(false).subscribe(mQuakesSubscriber);
161 | }
162 |
163 | private void assertQuakeItems(List items) {
164 | // check if the QuakeItems are the expected ones
165 | assertEquals(items.size(), QUAKES.size());
166 |
167 | assertQuake(items.get(0), QUAKES.get(0));
168 | assertQuake(items.get(1), QUAKES.get(1));
169 | }
170 |
171 | private void assertQuake(QuakeItem QuakeItem, Quake Quake) {
172 | assertEquals(QuakeItem.getQuake(), Quake);
173 | assertNotNull(QuakeItem.getOnClickAction());
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/app/src/main/java/com/inspiringteam/xchange/ui/quakes/QuakesFragment.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.ui.quakes
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.annotation.StringRes
9 | import androidx.core.content.ContextCompat
10 | import androidx.fragment.app.Fragment
11 | import androidx.lifecycle.ViewModelProvider
12 | import androidx.lifecycle.ViewModelProviders
13 | import com.google.android.material.snackbar.Snackbar
14 | import com.inspiringteam.xchange.BaseView
15 | import com.inspiringteam.xchange.R
16 | import com.inspiringteam.xchange.data.models.Quake
17 | import com.inspiringteam.xchange.di.scopes.ActivityScoped
18 | import io.reactivex.android.schedulers.AndroidSchedulers
19 | import io.reactivex.disposables.CompositeDisposable
20 | import io.reactivex.schedulers.Schedulers
21 | import kotlinx.android.synthetic.main.fragment_quakes.*
22 | import java.util.*
23 | import javax.inject.Inject
24 |
25 | @ActivityScoped
26 | class QuakesFragment @Inject constructor() : Fragment(), BaseView {
27 | private var viewModel: QuakesViewModel? = null
28 | private var adapter: QuakesAdapter? = null
29 | private var subscription: CompositeDisposable = CompositeDisposable()
30 |
31 | @JvmField
32 | @Inject
33 | var viewModelFactory: ViewModelProvider.Factory? = null
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 | viewModel = ViewModelProviders.of(this, viewModelFactory).get(
38 | QuakesViewModel::class.java
39 | )
40 | }
41 |
42 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
43 | super.onViewCreated(view, savedInstanceState)
44 | adapter = QuakesAdapter(mutableListOf())
45 | quakesListView.adapter = adapter
46 | setupSwipeRefreshLayout()
47 | }
48 |
49 | override fun onCreateView(
50 | inflater: LayoutInflater, container: ViewGroup?,
51 | savedInstanceState: Bundle?
52 | ): View? {
53 | return inflater.inflate(R.layout.fragment_quakes, container, false)
54 | }
55 |
56 | override fun onResume() {
57 | super.onResume()
58 | bindViewModel()
59 | }
60 |
61 | override fun onPause() {
62 | super.onPause()
63 | unbindViewModel()
64 | }
65 |
66 | override fun bindViewModel() {
67 | // Use a CompositeDisposable to gather all the subscriptions, so all of them can be
68 | // later unsubscribed together
69 | subscription = CompositeDisposable()
70 |
71 | // The ViewModel holds an observable containing the state of the UI.
72 | // Subscribe to the emissions of the Ui Model
73 | // Update the view at every emission of the UI Model
74 | subscription.add(
75 | viewModel!!.getUiModel(false)
76 | .subscribeOn(Schedulers.computation())
77 | .observeOn(AndroidSchedulers.mainThread())
78 | .subscribe(
79 | { model: QuakesUiModel -> updateView(model) }
80 | ) //onError
81 | { error: Throwable? ->
82 | Log.d(TAG, "Error loading quakes")
83 | error?.printStackTrace()
84 | })
85 |
86 | // Subscribe to the emissions of the snackbar text
87 | subscription.add(
88 | viewModel!!.snackBarMessage
89 | .subscribeOn(Schedulers.computation())
90 | .observeOn(AndroidSchedulers.mainThread())
91 | .subscribe(
92 | { message: Int -> showSnackbar(message) }
93 | ) //onError
94 | { error: Throwable? -> Log.d(TAG, "Error showing snackbar", error) })
95 |
96 | // Subscribe to the emissions of the loading indicator visibility
97 | subscription.add(
98 | viewModel!!.loadingIndicatorVisibility
99 | .subscribeOn(Schedulers.computation())
100 | .observeOn(AndroidSchedulers.mainThread())
101 | .subscribe(
102 | { isVisible: Boolean -> setLoadingIndicatorVisibility(isVisible) }
103 | ) //onError
104 | { error: Throwable? -> Log.d(TAG, "Error showing loading indicator", error) })
105 |
106 | // Bind Chrome tabs service
107 | viewModel!!.bindTabsService()
108 | }
109 |
110 | override fun unbindViewModel() {
111 | // Unbind tabs service
112 | viewModel?.unbindTabsService()
113 | }
114 |
115 | private fun setupSwipeRefreshLayout() {
116 | refreshLayout.setColorSchemeColors(
117 | ContextCompat.getColor(requireActivity(), R.color.colorPrimary),
118 | ContextCompat.getColor(requireActivity(), R.color.colorAccent),
119 | ContextCompat.getColor(requireActivity(), R.color.colorPrimaryDark)
120 | )
121 | // Set the scrolling view in the custom SwipeRefreshLayout.
122 | refreshLayout.setScrollUpChild(quakesListView)
123 | refreshLayout.setOnRefreshListener { forceUpdate() }
124 | }
125 |
126 | private fun forceUpdate() {
127 | subscription.add(
128 | viewModel!!.getUiModel(true)
129 | .subscribeOn(Schedulers.computation())
130 | .observeOn(AndroidSchedulers.mainThread())
131 | .subscribe(
132 | { model: QuakesUiModel -> updateView(model) }
133 | ) //onError
134 | { error: Throwable? ->
135 | Log.d(
136 | TAG,
137 | "Error loading quakes: " + error?.localizedMessage
138 | )
139 | })
140 | }
141 |
142 | private fun updateView(model: QuakesUiModel) {
143 | val ratesListVisibility = if (model.isQuakesListVisible) View.VISIBLE else View.GONE
144 | val noQuakesViewVisibility = if (model.isNoQuakesViewVisible) View.VISIBLE else View.GONE
145 | quakesLayout.visibility = ratesListVisibility
146 | noQuakesLayout.visibility = noQuakesViewVisibility
147 | if (model.isQuakesListVisible)
148 | showQuakes(model.quakes)
149 |
150 | if (model.isNoQuakesViewVisible && model.noQuakesModel != null)
151 | showNoQuakes(model.noQuakesModel)
152 | }
153 |
154 | private fun showNoQuakes(model: NoQuakesModel?) {
155 | noQuakesFoundTextView.setText(model!!.text)
156 | }
157 |
158 | private fun showQuakes(quakes: List) {
159 | adapter!!.replaceData(quakes.toMutableList())
160 | }
161 |
162 | private fun showSnackbar(@StringRes message: Int) {
163 | Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).show()
164 | }
165 |
166 | private fun setLoadingIndicatorVisibility(isVisible: Boolean) {
167 | // Make sure isRefreshing is called after the layout is done with everything else.
168 | refreshLayout.post { refreshLayout.isRefreshing = isVisible }
169 | }
170 |
171 | companion object {
172 | private val TAG = QuakesFragment::class.java.simpleName
173 | }
174 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'kotlin-android-extensions'
5 |
6 | android {
7 | compileSdkVersion rootProject.ext.compileSdkVersion
8 |
9 | defaultConfig {
10 | applicationId "com.inspiringteam.xchange"
11 | minSdkVersion rootProject.ext.minSdkVersion
12 | targetSdkVersion rootProject.ext.targetSdkVersion
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | multiDexEnabled true
17 | }
18 |
19 | compileOptions {
20 | sourceCompatibility JavaVersion.VERSION_1_8
21 | targetCompatibility JavaVersion.VERSION_1_8
22 | }
23 |
24 | buildTypes {
25 | debug {
26 | // Minifying the variant used for tests is not supported when using Jack.
27 | minifyEnabled false
28 | // Uses new built-in shrinker http://tools.android.com/tech-docs/new-build-system/built-in-shrinker
29 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
30 | testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguardTest-rules.pro'
31 | }
32 |
33 | release {
34 | minifyEnabled true
35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
36 | testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguardTest-rules.pro'
37 | }
38 | }
39 |
40 | // Using gradle plugin 3 you need to specify flavor dimensions.
41 | flavorDimensions 'buildType'
42 |
43 | // If you need to add more flavors, consider using flavor dimensions.
44 | productFlavors {
45 | mock {
46 | dimension 'buildType'
47 | applicationIdSuffix = ".mock"
48 | }
49 | prod {
50 | dimension 'buildType'
51 | }
52 | }
53 |
54 | // Remove mockRelease as it's not needed.
55 | android.variantFilter { variant ->
56 | if (variant.buildType.name.equals('release')
57 | && variant.getFlavors().get(0).name.equals('mock')) {
58 | variant.setIgnore(true)
59 | }
60 | }
61 |
62 | // Always show the result of every unit test, even if it passes.
63 | testOptions.unitTests.all {
64 | testLogging {
65 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError'
66 | }
67 | }
68 | }
69 |
70 | dependencies {
71 | // App's dependencies, including test
72 | implementation "androidx.appcompat:appcompat:$rootProject.androidxVersion"
73 | implementation "androidx.cardview:cardview:1.0.0"
74 | implementation "com.google.android.material:material:$rootProject.supportLibraryVersion"
75 | implementation "androidx.recyclerview:recyclerview:1.2.1"
76 | implementation "androidx.legacy:legacy-support-v4:1.0.0"
77 | implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayout"
78 | implementation "com.google.guava:guava:24.1-jre"
79 |
80 | implementation "androidx.room:room-rxjava2:$rootProject.roomVersion"
81 | implementation "androidx.room:room-runtime:$rootProject.roomVersion"
82 | kapt "androidx.room:room-compiler:$rootProject.roomVersion"
83 | implementation "androidx.room:room-ktx:2.3.0"
84 | androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
85 |
86 | // Dagger dependencies
87 | kapt "com.google.dagger:dagger-compiler:$rootProject.daggerVersion"
88 | annotationProcessor "com.google.dagger:dagger-android-processor:$rootProject.daggerVersion"
89 | implementation "com.google.dagger:dagger:$rootProject.daggerVersion"
90 | implementation "com.google.dagger:dagger-android-support:$rootProject.daggerVersion"
91 | kapt "com.google.dagger:dagger-android-processor:$rootProject.daggerVersion"
92 |
93 | // Android Architecture components dependencies
94 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$arch_version"
95 | implementation "androidx.lifecycle:lifecycle-extensions:$arch_version"
96 | kapt "androidx.lifecycle:lifecycle-compiler:$arch_version"
97 |
98 | // RxJava dependencies
99 | implementation "io.reactivex.rxjava2:rxandroid:$rootProject.rxandroidVersion"
100 | implementation "io.reactivex.rxjava2:rxjava:$rootProject.rxjavaVersion"
101 | implementation "io.reactivex:rxandroid:$rootProject.rx1androidVersion"
102 |
103 | // Retrofit dependencies
104 | implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofitClientVersion"
105 | implementation "com.squareup.retrofit2:adapter-rxjava2:$rootProject.retrofitAdapterRxJavaVersion"
106 | implementation "com.google.code.gson:gson:$rootProject.gsonVersion"
107 | implementation "com.squareup.retrofit2:converter-gson:$rootProject.gsonConverterVersion"
108 |
109 | // Chrome Custom Tabs dependencies
110 | implementation "androidx.browser:browser:$rootProject.supportCustomTabsVersion"
111 |
112 | // Dependencies for local unit tests
113 | testImplementation "junit:junit:$rootProject.ext.junitVersion"
114 | testImplementation "org.mockito:mockito-all:$rootProject.ext.mockitoVersion"
115 | testImplementation "org.hamcrest:hamcrest-all:$rootProject.ext.hamcrestVersion"
116 |
117 | // Android Testing Support Library's runner and rules
118 | androidTestImplementation "androidx.test:runner:$rootProject.ext.runnerVersion"
119 | androidTestImplementation "androidx.test:rules:$rootProject.ext.runnerVersion"
120 |
121 | // Dependencies for Android unit tests
122 | androidTestImplementation "junit:junit:$rootProject.ext.junitVersion"
123 | androidTestImplementation "org.mockito:mockito-core:$rootProject.ext.mockitoVersion"
124 | androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
125 | androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
126 |
127 | // Espresso UI Testing
128 | androidTestImplementation "androidx.test.espresso:espresso-core:$rootProject.espressoVersion"
129 | androidTestImplementation "androidx.test.espresso:espresso-contrib:$rootProject.espressoVersion"
130 | androidTestImplementation "androidx.test.espresso:espresso-intents:$rootProject.espressoVersion"
131 | implementation "androidx.test.espresso:espresso-idling-resource:$rootProject.espressoVersion"
132 |
133 | // Resolve conflicts between main and test APK:
134 | androidTestImplementation "androidx.annotation:annotation:$rootProject.supportLibraryVersion"
135 | androidTestImplementation "androidx.legacy:legacy-support-v4:$rootProject.supportLibraryVersion"
136 | androidTestImplementation "androidx.recyclerview:recyclerview:$rootProject.supportLibraryVersion"
137 | androidTestImplementation "androidx.appcompat:appcompat:$rootProject.androidxVersion"
138 | androidTestImplementation "com.google.android.material:material:$rootProject.supportLibraryVersion"
139 | androidTestImplementation "com.google.code.findbugs:jsr305:3.0.2"
140 |
141 | testImplementation ("androidx.arch.core:core-testing:$arch_testing", {
142 | exclude group: 'com.android.support', module: 'support-compat'
143 | exclude group: 'com.android.support', module: 'support-annotations'
144 | exclude group: 'com.android.support', module: 'support-core-utils'
145 | })
146 |
147 | androidTestImplementation("androidx.arch.core:core-testing:$arch_testing", {
148 | })
149 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
150 | }
151 | repositories {
152 | mavenCentral()
153 | }
154 |
--------------------------------------------------------------------------------
/app/src/test/java/com/inspiringteam/xchange/data/source/QuakesRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.inspiringteam.xchange.data.source
2 |
3 | import com.inspiringteam.xchange.data.models.Quake
4 | import com.inspiringteam.xchange.util.ConnectivityUtils.OnlineChecker
5 | import io.reactivex.Single
6 | import io.reactivex.subscribers.TestSubscriber
7 | import org.junit.Before
8 | import org.junit.Test
9 | import org.mockito.Mock
10 | import org.mockito.Mockito
11 | import org.mockito.MockitoAnnotations
12 | import java.util.*
13 |
14 | /**
15 | * Unit tests for the implementation of the repository
16 | */
17 | class QuakesRepositoryTest {
18 | private val testTime = System.currentTimeMillis()
19 | private val staleTime = (6 * 60 * 1000).toLong()
20 |
21 | private val QUAKES_RECENT: ArrayList = arrayListOf(
22 | Quake(4.5, "Place1", testTime, testTime),
23 | Quake(3.3, "Place2", testTime - 1, testTime)
24 | )
25 | private val QUAKES_STALE: ArrayList = arrayListOf(
26 | Quake(4.5, "Place1", testTime, testTime - staleTime),
27 | Quake(3.3, "Place2", testTime - 1, testTime)
28 | )
29 |
30 | private var mQuakesRepository: QuakesRepository? = null
31 |
32 | private var mQuakesTestSubscriber: TestSubscriber>? = null
33 |
34 | @Mock
35 | private val mQuakesRemoteDataSource: QuakesDataSource? = null
36 |
37 | @Mock
38 | private val mQuakesLocalDataSource: QuakesDataSource? = null
39 |
40 | @Mock
41 | private val mOnlineChecker: OnlineChecker? = null
42 |
43 | @Before
44 | fun setupQuakesRepository() {
45 | // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
46 | // inject the mocks in the test the initMocks method needs to be called.
47 | MockitoAnnotations.initMocks(this)
48 |
49 | // Get a reference to the class under test
50 | mQuakesRepository = QuakesRepository(
51 | mQuakesRemoteDataSource!!,
52 | mQuakesLocalDataSource!!, mOnlineChecker!!
53 | )
54 | mQuakesTestSubscriber = TestSubscriber>()
55 | }
56 |
57 | /**
58 | * Offline Test scenario states:
59 | * As the disk has up-to-date items, upon querying the repository with no internet connection,
60 | * the Local Data Source should be accessed and correct items should be retrieved
61 | */
62 | @Test
63 | fun getQuakesOffline_requestsQuakesFromLocalDataSource() {
64 |
65 | // the local data source has up-to-date data available
66 | ArrangeBuilder()
67 | .withQuakesAvailable(mQuakesLocalDataSource, QUAKES_RECENT)
68 |
69 | // establish a fake internet connection status
70 | Mockito.`when`(mOnlineChecker!!.isOnline()).thenReturn(false)
71 |
72 | // When quakes are requested from the quakes repository
73 | mQuakesRepository!!.getQuakes().toFlowable().subscribe(mQuakesTestSubscriber)
74 |
75 | // Then quakes are loaded from the local data source
76 | Mockito.verify(mQuakesLocalDataSource).getQuakes()
77 | mQuakesTestSubscriber!!.assertValue(QUAKES_RECENT)
78 | }
79 |
80 | /**
81 | * Online Test scenario states:
82 | * As the disk has up-to-date items, upon querying the repository with active internet connection,
83 | * the Local Data Source should be accessed and correct items should be retrieved
84 | */
85 | @Test
86 | fun getQuakesOnline_requestsQuakesFromLocalDataSource_upToDateLocal() {
87 |
88 | // the local data source has up-to-date data available
89 | ArrangeBuilder()
90 | .withQuakesAvailable(mQuakesLocalDataSource, QUAKES_RECENT)
91 |
92 | // establish a fake internet connection status
93 | Mockito.`when`(mOnlineChecker!!.isOnline()).thenReturn(true)
94 |
95 | // When quakes are requested from the quakes repository
96 | mQuakesRepository!!.getQuakes().toFlowable().subscribe(mQuakesTestSubscriber)
97 |
98 | // Then quakes are loaded from the local data source
99 | Mockito.verify(mQuakesLocalDataSource).getQuakes()
100 | mQuakesTestSubscriber!!.assertValue(QUAKES_RECENT)
101 | }
102 |
103 | /**
104 | * Online Test scenario states:
105 | * As the disk has stale items, upon querying the repository with active internet connection,
106 | * the Remote Data Source should be accessed and correct items should be retrieved
107 | */
108 | @Test
109 | fun getQuakesOnline_requestsQuakesFromRemoteDataSource_staleLocal() {
110 |
111 | // the remote data source has fresh data available
112 | ArrangeBuilder()
113 | .withQuakesAvailable(mQuakesRemoteDataSource, QUAKES_RECENT)
114 |
115 | // the local data source has stale data available
116 | ArrangeBuilder()
117 | .withQuakesAvailable(mQuakesLocalDataSource, QUAKES_STALE)
118 |
119 | // establish a fake internet connection status
120 | Mockito.`when`(mOnlineChecker!!.isOnline()).thenReturn(true)
121 |
122 | // When quakes are requested from the quakes repository
123 | mQuakesRepository!!.getQuakes().toFlowable().subscribe(mQuakesTestSubscriber)
124 |
125 | // Both sources should be queried, yet the local source has stale items
126 | // which triggers the call to the remote source
127 | Mockito.verify(mQuakesLocalDataSource).getQuakes()
128 | Mockito.verify(mQuakesLocalDataSource).getQuakes()
129 | mQuakesTestSubscriber!!.assertValue(QUAKES_RECENT)
130 | }
131 |
132 | /**
133 | * Online Test scenario states:
134 | * As the disk has no items, upon querying the repository with active internet connection,
135 | * the Remote Data Source should be accessed and correct items should be retrieved
136 | */
137 | @Test
138 | fun getQuakesOnline_requestsQuakesFromRemoteDataSource_emptyLocal() {
139 |
140 | // the remote data source has fresh data available
141 | ArrangeBuilder()
142 | .withQuakesAvailable((mQuakesRemoteDataSource)!!, QUAKES_RECENT)
143 |
144 | // the local data source has stale data available
145 | ArrangeBuilder()
146 | .withQuakesNotAvailable((mQuakesLocalDataSource)!!)
147 |
148 | // establish a fake internet connection status
149 | Mockito.`when`(mOnlineChecker!!.isOnline()).thenReturn(true)
150 |
151 | // When quakes are requested from the quakes repository
152 | mQuakesRepository!!.getQuakes().toFlowable().subscribe(mQuakesTestSubscriber)
153 |
154 | // Both sources should be queried, yet the local source has no items
155 | // which triggers the call to the remote source
156 | Mockito.verify(mQuakesLocalDataSource).getQuakes()
157 | Mockito.verify(mQuakesLocalDataSource).getQuakes()
158 | mQuakesTestSubscriber!!.assertValue(QUAKES_RECENT)
159 | }
160 |
161 | /**
162 | * Test scenario states:
163 | * Upon get command , both Local Data Source should retrieve the item locally
164 | */
165 | @Test
166 | fun getQuakeFromLocal() {
167 | val tempQuake: Quake = Quake("id", "location")
168 | ArrangeBuilder().withQuakeById((mQuakesLocalDataSource)!!, tempQuake)
169 | mQuakesRepository!!.getQuake(tempQuake.id).toFlowable().subscribe()
170 |
171 | // upon get command, check if only local data source is being called
172 | Mockito.verify(mQuakesLocalDataSource).getQuake(tempQuake.id)
173 | Mockito.verify(mQuakesRemoteDataSource, Mockito.never()).getQuake(tempQuake.id)
174 | }
175 |
176 |
177 | /**
178 | * Test scenario states:
179 | * Upon save command , both Local Data Source and Remote Data Source should save
180 | * the corresponding items taken as parameter
181 | */
182 | @Test
183 | fun saveQuakes() {
184 | mQuakesRepository!!.saveQuakes(QUAKES_RECENT)
185 |
186 | // upon save command, check if both data sources are being called
187 | Mockito.verify(mQuakesLocalDataSource).saveQuakes(QUAKES_RECENT)
188 | Mockito.verify(mQuakesRemoteDataSource).saveQuakes(QUAKES_RECENT)
189 | }
190 |
191 | /**
192 | * Test scenario states:
193 | * Upon save command , both Local Data Source and Remote Data Source should save
194 | * the corresponding item taken as parameter
195 | */
196 | @Test
197 | fun saveQuake() {
198 | mQuakesRepository!!.saveQuake(QUAKES_RECENT.get(0))
199 |
200 | // upon save command, check if both data sources are being called
201 | Mockito.verify(mQuakesLocalDataSource).saveQuake(QUAKES_RECENT.get(0))
202 | Mockito.verify(mQuakesRemoteDataSource).saveQuake(QUAKES_RECENT.get(0))
203 | }
204 |
205 | /**
206 | * Test scenario states:
207 | * Upon delete command , both Local Data Source and Remote Data Source should delete
208 | * all items
209 | */
210 | @Test
211 | fun deleteQuakes() {
212 | mQuakesRepository!!.deleteAllQuakes()
213 |
214 | // upon save command, check if both data sources are being called
215 | Mockito.verify(mQuakesLocalDataSource).deleteAllQuakes()
216 | Mockito.verify(mQuakesRemoteDataSource).deleteAllQuakes()
217 | }
218 |
219 | /**
220 | * Test scenario states:
221 | * Upon delete command , both Local Data Source and Remote Data Source should delete
222 | * the corresponding item
223 | */
224 | @Test
225 | fun deleteQuake() {
226 | mQuakesRepository!!.deleteQuake("id")
227 |
228 | // upon save command, check if both data sources are being called
229 | Mockito.verify(mQuakesLocalDataSource).deleteQuake("id")
230 | Mockito.verify(mQuakesRemoteDataSource).deleteQuake("id")
231 | }
232 |
233 |
234 | internal class ArrangeBuilder constructor() {
235 | fun withQuakesNotAvailable(dataSource: QuakesDataSource): ArrangeBuilder {
236 | Mockito.`when`>(dataSource.getQuakes())
237 | .thenReturn(Single.just(emptyList()))
238 | return this
239 | }
240 |
241 | fun withQuakesAvailable(
242 | dataSource: QuakesDataSource,
243 | quakes: kotlin.collections.MutableList?
244 | ): ArrangeBuilder {
245 | // don't allow the data sources to complete.
246 | Mockito.`when`>(dataSource.getQuakes())
247 | .thenReturn(Single.just(quakes))
248 | return this
249 | }
250 |
251 | fun withQuakeById(dataSource: QuakesDataSource, quake: Quake): ArrangeBuilder {
252 | // don't allow the data sources to complete.
253 | Mockito.`when`(dataSource.getQuake(quake.id)).thenReturn(Single.just(quake))
254 | return this
255 | }
256 | }
257 | }
--------------------------------------------------------------------------------