├── cache ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── org │ │ │ └── buffer │ │ │ └── android │ │ │ └── boilerplate │ │ │ └── cache │ │ │ ├── model │ │ │ └── CachedBufferoo.kt │ │ │ ├── db │ │ │ ├── constants │ │ │ │ └── BufferooConstants.kt │ │ │ ├── mapper │ │ │ │ ├── ModelDbMapper.kt │ │ │ │ └── BufferooMapper.kt │ │ │ ├── Db.kt │ │ │ └── DbOpenHelper.kt │ │ │ ├── mapper │ │ │ ├── EntityMapper.kt │ │ │ └── BufferooEntityMapper.kt │ │ │ ├── PreferencesHelper.kt │ │ │ └── BufferooCacheImpl.kt │ └── test │ │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── cache │ │ ├── test │ │ ├── DefaultConfig.kt │ │ └── factory │ │ │ ├── DataFactory.kt │ │ │ └── BufferooFactory.kt │ │ ├── mapper │ │ └── BufferooEntityMapperTest.kt │ │ ├── db │ │ └── mapper │ │ │ └── BufferooMapperTest.kt │ │ └── BufferooCacheImplTest.kt └── build.gradle ├── data ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── org │ │ │ └── buffer │ │ │ └── android │ │ │ └── boilerplate │ │ │ └── data │ │ │ ├── model │ │ │ └── BufferooEntity.kt │ │ │ ├── mapper │ │ │ ├── Mapper.kt │ │ │ └── BufferooMapper.kt │ │ │ ├── repository │ │ │ ├── BufferooRemote.kt │ │ │ ├── BufferooDataStore.kt │ │ │ └── BufferooCache.kt │ │ │ ├── source │ │ │ ├── BufferooRemoteDataStore.kt │ │ │ ├── BufferooDataStoreFactory.kt │ │ │ └── BufferooCacheDataStore.kt │ │ │ ├── executor │ │ │ └── JobExecutor.kt │ │ │ └── BufferooDataRepository.kt │ └── test │ │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── data │ │ ├── test │ │ └── factory │ │ │ ├── DataFactory.kt │ │ │ └── BufferooFactory.kt │ │ ├── mapper │ │ └── BufferooMapperTest.kt │ │ ├── source │ │ ├── BufferooRemoteDataStoreTest.kt │ │ ├── BufferooCacheDataStoreTest.kt │ │ └── BufferooDataStoreFactoryTest.kt │ │ └── BufferooDataRepositoryTest.kt └── build.gradle ├── domain ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── org │ │ │ └── buffer │ │ │ └── android │ │ │ └── boilerplate │ │ │ └── domain │ │ │ ├── model │ │ │ └── Bufferoo.kt │ │ │ ├── executor │ │ │ ├── ThreadExecutor.kt │ │ │ └── PostExecutionThread.kt │ │ │ ├── interactor │ │ │ ├── BaseSingleObserver.kt │ │ │ ├── browse │ │ │ │ └── GetBufferoos.kt │ │ │ ├── CompletableUseCase.kt │ │ │ └── SingleUseCase.kt │ │ │ └── repository │ │ │ └── BufferooRepository.kt │ └── test │ │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── domain │ │ ├── test │ │ └── factory │ │ │ ├── DataFactory.kt │ │ │ └── BufferooFactory.kt │ │ └── usecase │ │ └── bufferoo │ │ └── GetBufferoosTest.kt └── build.gradle ├── remote ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── org │ │ │ └── buffer │ │ │ └── android │ │ │ └── boilerplate │ │ │ └── remote │ │ │ ├── model │ │ │ └── BufferooModel.kt │ │ │ ├── mapper │ │ │ ├── EntityMapper.kt │ │ │ └── BufferooEntityMapper.kt │ │ │ ├── BufferooService.kt │ │ │ ├── BufferooRemoteImpl.kt │ │ │ └── BufferooServiceFactory.kt │ └── test │ │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── remote │ │ ├── test │ │ └── factory │ │ │ ├── DataFactory.kt │ │ │ └── BufferooFactory.kt │ │ ├── mapper │ │ └── BufferooEntityMapperTest.kt │ │ └── BufferooRemoteImplTest.kt └── build.gradle ├── presentation ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── org │ │ │ └── buffer │ │ │ └── android │ │ │ └── boilerplate │ │ │ └── presentation │ │ │ ├── model │ │ │ └── BufferooView.kt │ │ │ ├── BasePresenter.kt │ │ │ ├── BaseView.kt │ │ │ ├── mapper │ │ │ ├── Mapper.kt │ │ │ └── BufferooMapper.kt │ │ │ └── browse │ │ │ ├── BrowseBufferoosContract.kt │ │ │ └── BrowseBufferoosPresenter.kt │ └── test │ │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── presentation │ │ ├── test │ │ └── factory │ │ │ ├── DataFactory.kt │ │ │ └── BufferooFactory.kt │ │ └── browse │ │ └── BrowseBufferoosPresenterTest.kt └── build.gradle ├── art ├── ui.png ├── data.png ├── architecture.png └── device_screenshot.png ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── mobile-ui ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── dimens.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_browse.xml │ │ │ │ └── item_bufferoo.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── org │ │ │ │ └── buffer │ │ │ │ └── android │ │ │ │ └── boilerplate │ │ │ │ └── ui │ │ │ │ ├── injection │ │ │ │ ├── scopes │ │ │ │ │ ├── PerActivity.kt │ │ │ │ │ └── PerApplication.kt │ │ │ │ ├── component │ │ │ │ │ └── BrowseActivitySubComponent.kt │ │ │ │ ├── module │ │ │ │ │ ├── ActivityBindingModule.kt │ │ │ │ │ ├── BrowseActivityModule.kt │ │ │ │ │ └── ApplicationModule.kt │ │ │ │ └── ApplicationComponent.kt │ │ │ │ ├── model │ │ │ │ └── BufferooViewModel.kt │ │ │ │ ├── mapper │ │ │ │ ├── Mapper.kt │ │ │ │ └── BufferooMapper.kt │ │ │ │ ├── UiThread.kt │ │ │ │ ├── BufferooApplication.kt │ │ │ │ └── browse │ │ │ │ ├── BrowseAdapter.kt │ │ │ │ └── BrowseActivity.kt │ │ └── AndroidManifest.xml │ ├── androidTest │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── org │ │ │ └── buffer │ │ │ └── android │ │ │ └── boilerplate │ │ │ └── ui │ │ │ ├── test │ │ │ ├── factory │ │ │ │ ├── BufferooFactory.kt │ │ │ │ └── DataFactory.kt │ │ │ ├── TestRunner.kt │ │ │ ├── TestApplication.kt │ │ │ └── util │ │ │ │ └── RecyclerViewMatcher.kt │ │ │ ├── injection │ │ │ ├── component │ │ │ │ └── TestApplicationComponent.kt │ │ │ └── module │ │ │ │ └── TestApplicationModule.kt │ │ │ └── browse │ │ │ └── BrowseActivityTest.kt │ └── test │ │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── ui │ │ ├── test │ │ └── factory │ │ │ ├── BufferooFactory.kt │ │ │ └── DataFactory.kt │ │ └── BufferooMapperTest.kt ├── .gitignore └── build.gradle ├── .gitignore ├── licence.txt ├── .travis.yml ├── gradlew.bat ├── gradlew ├── dependencies.gradle └── readme.md /cache/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml -------------------------------------------------------------------------------- /remote/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml -------------------------------------------------------------------------------- /art/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/art/ui.png -------------------------------------------------------------------------------- /art/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/art/data.png -------------------------------------------------------------------------------- /art/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/art/architecture.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':cache', ':remote', ':data', ':domain', ':presentation', ':mobile-ui' 2 | -------------------------------------------------------------------------------- /art/device_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/art/device_screenshot.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /cache/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mobile-ui/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Clean Arch Boilerplate 3 | 4 | -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phodal/android-play/master/mobile-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /cache/src/test/java/org/buffer/android/boilerplate/cache/test/DefaultConfig.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.test 2 | 3 | object DefaultConfig { 4 | //The api level that RoboElectric will use to run the unit tests 5 | const val EMULATE_SDK = 21 6 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/scopes/PerActivity.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.scopes 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class PerActivity -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/scopes/PerApplication.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.scopes 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class PerApplication -------------------------------------------------------------------------------- /mobile-ui/.gitignore: -------------------------------------------------------------------------------- 1 | *.apk 2 | *.ap_ 3 | *.dex 4 | *.class 5 | bin/ 6 | gen/ 7 | out/ 8 | build/ 9 | workspace.xml 10 | .idea 11 | local.properties 12 | ks.properties 13 | .classpath 14 | .project 15 | .DS_Store 16 | lint.xml 17 | protected_strings.xml 18 | .gradle 19 | /dist -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/model/CachedBufferoo.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.model 2 | 3 | /** 4 | * Model used solely for the caching of a bufferroo 5 | */ 6 | data class CachedBufferoo(val name: String, val title: String, val avatar: String) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Aug 11 10:04:50 IST 2017 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-4.1-all.zip 7 | -------------------------------------------------------------------------------- /remote/src/main/java/org/buffer/android/boilerplate/remote/model/BufferooModel.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote.model 2 | 3 | /** 4 | * Representation for a [BufferooModel] fetched from the API 5 | */ 6 | class BufferooModel(val name: String, val title: String, val avatar: String) -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/model/Bufferoo.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.model 2 | 3 | /** 4 | * Representation for a [Bufferoo] fetched from an external layer data source 5 | */ 6 | data class Bufferoo(val name: String, val title: String, val avatar: String) -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/model/BufferooEntity.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.model 2 | 3 | /** 4 | * Representation for a [BufferooEntity] fetched from an external layer data source 5 | */ 6 | data class BufferooEntity(val name: String, val title: String, val avatar: String) -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/model/BufferooViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.model 2 | 3 | /** 4 | * Representation for a [BufferooViewModel] fetched from an external layer data source 5 | */ 6 | class BufferooViewModel(val name: String, val title: String, val avatar: String) -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/java/org/buffer/android/boilerplate/presentation/model/BufferooView.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation.model 2 | 3 | /** 4 | * Representation for a [BufferooView] instance for this layers Model representation 5 | */ 6 | class BufferooView(val name: String, val title: String, val avatar: String) -------------------------------------------------------------------------------- /mobile-ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | /build 6 | .idea/ 7 | *iml 8 | *.iml 9 | */build 10 | gradle.properties 11 | *.apk 12 | *.ap_ 13 | *.dex 14 | *.class 15 | bin/ 16 | gen/ 17 | out/ 18 | build/ 19 | workspace.xml 20 | .idea 21 | ks.properties 22 | .classpath 23 | .project 24 | lint.xml 25 | /dist 26 | *.iml -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/executor/ThreadExecutor.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.executor 2 | 3 | import java.util.concurrent.Executor 4 | 5 | /** 6 | * Executor implementation can be based on different frameworks or techniques of asynchronous 7 | * execution, but every implementation will execute the 8 | */ 9 | interface ThreadExecutor : Executor -------------------------------------------------------------------------------- /presentation/src/main/java/org/buffer/android/boilerplate/presentation/BasePresenter.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation 2 | 3 | /** 4 | * Interface class to act as a base for any class that is to take the role of the IPresenter in the 5 | * Model-BaseView-IPresenter pattern. 6 | */ 7 | interface BasePresenter { 8 | 9 | fun start() 10 | 11 | fun stop() 12 | 13 | } -------------------------------------------------------------------------------- /presentation/src/main/java/org/buffer/android/boilerplate/presentation/BaseView.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation 2 | 3 | /** 4 | * Interface class to act as a base for any class that is to take the role of the BaseView in the Model- 5 | * BaseView-Presenter pattern. 6 | */ 7 | interface BaseView { 8 | 9 | fun setPresenter(presenter: T) 10 | 11 | } -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/db/constants/BufferooConstants.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.db.constants 2 | 3 | import org.buffer.android.boilerplate.cache.db.Db 4 | 5 | /** 6 | * Defines DB queries for the Bufferoos Table 7 | */ 8 | object BufferooConstants { 9 | 10 | internal val QUERY_GET_ALL_BUFFEROOS = "SELECT * FROM " + Db.BufferooTable.TABLE_NAME 11 | 12 | } -------------------------------------------------------------------------------- /domain/src/test/java/org/buffer/android/boilerplate/domain/test/factory/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.test.factory 2 | 3 | /** 4 | * Factory class for data instances 5 | */ 6 | class DataFactory { 7 | 8 | companion object Factory { 9 | 10 | fun randomUuid(): String { 11 | return java.util.UUID.randomUUID().toString() 12 | } 13 | 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /presentation/src/test/java/org/buffer/android/boilerplate/presentation/test/factory/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation.test.factory 2 | 3 | /** 4 | * Factory class for data instances 5 | */ 6 | class DataFactory { 7 | 8 | companion object Factory { 9 | 10 | fun randomUuid(): String { 11 | return java.util.UUID.randomUUID().toString() 12 | } 13 | 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.mapper 2 | 3 | /** 4 | * Interface for model mappers. It provides helper methods that facilitate 5 | * retrieving of models from outer layers 6 | * 7 | * @param the view input type 8 | * @param the view model output type 9 | */ 10 | interface Mapper { 11 | 12 | fun mapToViewModel(type: D): V 13 | 14 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /presentation/src/main/java/org/buffer/android/boilerplate/presentation/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation.mapper 2 | 3 | /** 4 | * Interface for model mappers. It provides helper methods that facilitate 5 | * retrieving of models from outer layers 6 | * 7 | * @param the view model input type 8 | * @param the domain model output type 9 | */ 10 | interface Mapper { 11 | 12 | fun mapToView(type: D): V 13 | 14 | } -------------------------------------------------------------------------------- /remote/src/main/java/org/buffer/android/boilerplate/remote/mapper/EntityMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote.mapper 2 | 3 | /** 4 | * Interface for model mappers. It provides helper methods that facilitate 5 | * retrieving of models from outer data source layers 6 | * 7 | * @param the remote model input type 8 | * @param the entity model output type 9 | */ 10 | interface EntityMapper { 11 | 12 | fun mapFromRemote(type: M): E 13 | 14 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32sp 5 | 24sp 6 | 20sp 7 | 18sp 8 | 16sp 9 | 14sp 10 | 12sp 11 | 10sp 12 | 13 | 14 | -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/component/BrowseActivitySubComponent.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.component 2 | 3 | import dagger.Subcomponent 4 | import dagger.android.AndroidInjector 5 | import org.buffer.android.boilerplate.ui.browse.BrowseActivity 6 | 7 | @Subcomponent 8 | interface BrowseActivitySubComponent : AndroidInjector { 9 | 10 | @Subcomponent.Builder 11 | abstract class Builder : AndroidInjector.Builder() 12 | 13 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.mapper 2 | 3 | /** 4 | * Interface for model mappers. It provides helper methods that facilitate 5 | * retrieving of models from outer data source layers 6 | * 7 | * @param the cached model input type 8 | * @param the remote model input type 9 | * @param the model return type 10 | */ 11 | interface Mapper { 12 | 13 | fun mapFromEntity(type: E): D 14 | 15 | fun mapToEntity(type: D): E 16 | 17 | } -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/BaseSingleObserver.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.interactor 2 | 3 | import io.reactivex.SingleObserver 4 | import io.reactivex.disposables.Disposable 5 | 6 | 7 | /** 8 | * Default [SingleObserver] base class to define 9 | */ 10 | open class BaseSingleObserver : SingleObserver { 11 | 12 | override fun onSubscribe(d: Disposable) { } 13 | 14 | override fun onSuccess(t: T) { } 15 | 16 | override fun onError(exception: Throwable) { } 17 | 18 | } -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/executor/PostExecutionThread.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.executor 2 | 3 | import io.reactivex.Scheduler 4 | 5 | /** 6 | * Thread abstraction created to change the execution context from any thread to any other thread. 7 | * Useful to encapsulate a UI Thread for example, since some job will be done in background, an 8 | * implementation of this interface will change context and update the UI. 9 | */ 10 | interface PostExecutionThread { 11 | val scheduler: Scheduler 12 | } -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/mapper/EntityMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.mapper 2 | 3 | /** 4 | * Interface for model mappers. It provides helper methods that facilitate 5 | * retrieving of models from outer data source layers 6 | * 7 | * @param the cached model input type 8 | * @param the remote model input type 9 | * @param the model return type 10 | */ 11 | interface EntityMapper { 12 | 13 | fun mapFromCached(type: T): V 14 | 15 | fun mapToCached(type: V): T 16 | 17 | } -------------------------------------------------------------------------------- /remote/src/main/java/org/buffer/android/boilerplate/remote/BufferooService.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote 2 | 3 | import io.reactivex.Single 4 | import org.buffer.android.boilerplate.remote.model.BufferooModel 5 | import retrofit2.http.GET 6 | 7 | /** 8 | * Defines the abstract methods used for interacting with the Bufferoo API 9 | */ 10 | interface BufferooService { 11 | 12 | @GET("team.json") 13 | fun getBufferoos(): Single 14 | 15 | class BufferooResponse { 16 | lateinit var team: List 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/repository/BufferooRemote.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.repository 2 | 3 | import io.reactivex.Single 4 | import org.buffer.android.boilerplate.data.model.BufferooEntity 5 | 6 | /** 7 | * Interface defining methods for the caching of Bufferroos. This is to be implemented by the 8 | * cache layer, using this interface as a way of communicating. 9 | */ 10 | interface BufferooRemote { 11 | 12 | /** 13 | * Retrieve a list of Bufferoos, from the cache 14 | */ 15 | fun getBufferoos(): Single> 16 | 17 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/ActivityBindingModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | import org.buffer.android.boilerplate.ui.browse.BrowseActivity 6 | import org.buffer.android.boilerplate.ui.injection.scopes.PerActivity 7 | 8 | @Module 9 | abstract class ActivityBindingModule { 10 | 11 | @PerActivity 12 | @ContributesAndroidInjector(modules = arrayOf(BrowseActivityModule::class)) 13 | abstract fun bindMainActivity(): BrowseActivity 14 | 15 | } -------------------------------------------------------------------------------- /mobile-ui/src/test/java/org/buffer/android/boilerplate/ui/test/factory/BufferooFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test.factory 2 | 3 | import org.buffer.android.boilerplate.presentation.model.BufferooView 4 | import org.buffer.android.boilerplate.ui.test.factory.DataFactory.Factory.randomUuid 5 | 6 | /** 7 | * Factory class for Bufferoo related instances 8 | */ 9 | class BufferooFactory { 10 | 11 | companion object Factory { 12 | 13 | fun makeBufferooView(): BufferooView { 14 | return BufferooView(randomUuid(), randomUuid(), randomUuid()) 15 | } 16 | 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/UiThread.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui 2 | 3 | import io.reactivex.Scheduler 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 6 | import javax.inject.Inject 7 | 8 | /** 9 | * MainThread (UI Thread) implementation based on a [Scheduler] 10 | * which will execute actions on the Android UI thread 11 | */ 12 | class UiThread @Inject internal constructor() : PostExecutionThread { 13 | 14 | override val scheduler: Scheduler 15 | get() = AndroidSchedulers.mainThread() 16 | 17 | } -------------------------------------------------------------------------------- /data/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | sourceCompatibility = 1.7 4 | targetCompatibility = 1.7 5 | 6 | dependencies { 7 | def dataDependencies = rootProject.ext.dataDependencies 8 | def dataTestDependencies = rootProject.ext.dataTestDependencies 9 | 10 | compile project(':domain') 11 | 12 | compile dataDependencies.javaxAnnotation 13 | 14 | implementation dataDependencies.kotlin 15 | implementation dataDependencies.javaxInject 16 | implementation dataDependencies.rxKotlin 17 | 18 | testImplementation dataTestDependencies.junit 19 | testImplementation dataTestDependencies.kotlinJUnit 20 | testImplementation dataTestDependencies.mockito 21 | testImplementation dataTestDependencies.assertj 22 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/repository/BufferooDataStore.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.repository 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Single 5 | import org.buffer.android.boilerplate.data.model.BufferooEntity 6 | 7 | /** 8 | * Interface defining methods for the data operations related to Bufferroos. 9 | * This is to be implemented by external data source layers, setting the requirements for the 10 | * operations that need to be implemented 11 | */ 12 | interface BufferooDataStore { 13 | 14 | fun clearBufferoos(): Completable 15 | 16 | fun saveBufferoos(bufferoos: List): Completable 17 | 18 | fun getBufferoos(): Single> 19 | 20 | } -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/repository/BufferooRepository.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.repository 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Single 5 | import org.buffer.android.boilerplate.domain.model.Bufferoo 6 | 7 | /** 8 | * Interface defining methods for how the data layer can pass data to and from the Domain layer. 9 | * This is to be implemented by the data layer, setting the requirements for the 10 | * operations that need to be implemented 11 | */ 12 | interface BufferooRepository { 13 | 14 | fun clearBufferoos(): Completable 15 | 16 | fun saveBufferoos(bufferoos: List): Completable 17 | 18 | fun getBufferoos(): Single> 19 | 20 | } -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/db/mapper/ModelDbMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.db.mapper 2 | 3 | import android.content.ContentValues 4 | import android.database.Cursor 5 | 6 | /** 7 | * Interface for database model mappers. It provides helper methods that facilitate 8 | * saving/retrieving java models into/from a relational database. 9 | * 10 | * @param the model type 11 | */ 12 | interface ModelDbMapper { 13 | 14 | /** 15 | * Converts the model into ContentValues for the database table 16 | */ 17 | fun toContentValues(model: T): ContentValues 18 | 19 | /** 20 | * Parses the Cursor resulting from a database query into the java model 21 | */ 22 | fun parseCursor(cursor: Cursor): T 23 | 24 | } -------------------------------------------------------------------------------- /presentation/src/main/java/org/buffer/android/boilerplate/presentation/mapper/BufferooMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation.mapper 2 | 3 | import org.buffer.android.boilerplate.domain.model.Bufferoo 4 | import org.buffer.android.boilerplate.presentation.model.BufferooView 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Map a [BufferooView] to and from a [Bufferoo] instance when data is moving between 9 | * this layer and the Domain layer 10 | */ 11 | open class BufferooMapper @Inject constructor(): Mapper { 12 | 13 | /** 14 | * Map a [Bufferoo] instance to a [BufferooView] instance 15 | */ 16 | override fun mapToView(type: Bufferoo): BufferooView { 17 | return BufferooView(type.name, type.title, type.avatar) 18 | } 19 | 20 | 21 | } -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | sourceCompatibility = 1.7 4 | targetCompatibility = 1.7 5 | 6 | configurations.all { 7 | resolutionStrategy { 8 | force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 9 | } 10 | } 11 | 12 | dependencies { 13 | def domainDependencies = rootProject.ext.domainDependencies 14 | def domainTestDependencies = rootProject.ext.domainTestDependencies 15 | 16 | implementation domainDependencies.javaxAnnotation 17 | implementation domainDependencies.javaxInject 18 | implementation domainDependencies.rxKotlin 19 | implementation domainDependencies.kotlin 20 | 21 | testImplementation domainTestDependencies.junit 22 | testImplementation domainTestDependencies.mockito 23 | testImplementation domainTestDependencies.assertj 24 | 25 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/mapper/BufferooMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.mapper 2 | 3 | import org.buffer.android.boilerplate.presentation.model.BufferooView 4 | import org.buffer.android.boilerplate.ui.model.BufferooViewModel 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Map a [BufferooView] to and from a [BufferooViewModel] instance when data is moving between 9 | * this layer and the Domain layer 10 | */ 11 | open class BufferooMapper @Inject constructor(): Mapper { 12 | 13 | /** 14 | * Map a [BufferooView] instance to a [BufferooViewModel] instance 15 | */ 16 | override fun mapToViewModel(type: BufferooView): BufferooViewModel { 17 | return BufferooViewModel(type.name, type.title, type.avatar) 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /remote/src/main/java/org/buffer/android/boilerplate/remote/mapper/BufferooEntityMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote.mapper 2 | 3 | import org.buffer.android.boilerplate.data.model.BufferooEntity 4 | import org.buffer.android.boilerplate.remote.model.BufferooModel 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Map a [BufferooModel] to and from a [BufferooEntity] instance when data is moving between 9 | * this later and the Data layer 10 | */ 11 | open class BufferooEntityMapper @Inject constructor(): EntityMapper { 12 | 13 | /** 14 | * Map an instance of a [BufferooModel] to a [BufferooEntity] model 15 | */ 16 | override fun mapFromRemote(type: BufferooModel): BufferooEntity { 17 | return BufferooEntity(type.name, type.title, type.avatar) 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/test/factory/BufferooFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test.factory.ui 2 | 3 | import org.buffer.android.boilerplate.domain.model.Bufferoo 4 | 5 | /** 6 | * Factory class for Bufferoo related instances 7 | */ 8 | class BufferooFactory { 9 | 10 | companion object Factory { 11 | 12 | fun makeBufferooList(count: Int): List { 13 | val bufferoos = mutableListOf() 14 | repeat(count) { 15 | bufferoos.add(makeBufferooModel()) 16 | } 17 | return bufferoos 18 | } 19 | 20 | fun makeBufferooModel(): Bufferoo { 21 | return Bufferoo(DataFactory.randomUuid(), DataFactory.randomUuid(), 22 | DataFactory.randomUuid()) 23 | } 24 | 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /domain/src/test/java/org/buffer/android/boilerplate/domain/test/factory/BufferooFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.test.factory 2 | 3 | import org.buffer.android.boilerplate.domain.model.Bufferoo 4 | import org.buffer.android.boilerplate.domain.test.factory.DataFactory.Factory.randomUuid 5 | 6 | /** 7 | * Factory class for Bufferoo related instances 8 | */ 9 | class BufferooFactory { 10 | 11 | companion object Factory { 12 | 13 | fun makeBufferooList(count: Int): List { 14 | val bufferoos = mutableListOf() 15 | repeat(count) { 16 | bufferoos.add(makeBufferoo()) 17 | } 18 | return bufferoos 19 | } 20 | 21 | fun makeBufferoo(): Bufferoo { 22 | return Bufferoo(randomUuid(), randomUuid(), randomUuid()) 23 | } 24 | 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /presentation/src/test/java/org/buffer/android/boilerplate/presentation/test/factory/BufferooFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation.test.factory 2 | 3 | import org.buffer.android.boilerplate.domain.model.Bufferoo 4 | import org.buffer.android.boilerplate.presentation.test.factory.DataFactory.Factory.randomUuid 5 | 6 | /** 7 | * Factory class for Bufferoo related instances 8 | */ 9 | class BufferooFactory { 10 | 11 | companion object Factory { 12 | 13 | fun makeBufferooList(count: Int): List { 14 | val bufferoos = mutableListOf() 15 | repeat(count) { 16 | bufferoos.add(makeBufferooModel()) 17 | } 18 | return bufferoos 19 | } 20 | 21 | fun makeBufferooModel(): Bufferoo { 22 | return Bufferoo(randomUuid(), randomUuid(), randomUuid()) 23 | } 24 | 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/test/TestRunner.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.os.Bundle 6 | import android.support.test.runner.AndroidJUnitRunner 7 | import io.reactivex.plugins.RxJavaPlugins 8 | import io.reactivex.schedulers.Schedulers 9 | 10 | class TestRunner : AndroidJUnitRunner() { 11 | 12 | override fun onCreate(arguments: Bundle) { 13 | super.onCreate(arguments) 14 | RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } 15 | } 16 | 17 | @Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class) 18 | override fun newApplication(cl: ClassLoader, className: String, context: Context): Application { 19 | return super.newApplication(cl, TestApplication::class.java.name, context) 20 | } 21 | } -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/db/Db.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.db 2 | 3 | /** 4 | * This class defines the tables found within the application Database. All table 5 | * definitions should contain column names and a sequence for creating the table. 6 | */ 7 | object Db { 8 | 9 | object BufferooTable { 10 | const val TABLE_NAME = "bufferroos" 11 | 12 | const val BUFFEROO_ID = "bufferoo_id" 13 | const val NAME = "name" 14 | const val TITLE = "title" 15 | const val AVATAR = "avatar" 16 | 17 | const val CREATE = 18 | "CREATE TABLE " + TABLE_NAME + " (" + 19 | BUFFEROO_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + 20 | NAME + " TEXT NOT NULL," + 21 | TITLE + " TEXT," + 22 | AVATAR + " TEXT" + 23 | "); " 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /presentation/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | sourceCompatibility = 1.7 4 | targetCompatibility = 1.7 5 | 6 | configurations.all { 7 | resolutionStrategy { 8 | force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 9 | } 10 | } 11 | 12 | dependencies { 13 | def presentationDependencies = rootProject.ext.cacheDependencies 14 | def presentationTestDependencies = rootProject.ext.cacheTestDependencies 15 | 16 | compile presentationDependencies.javaxAnnotation 17 | 18 | implementation presentationDependencies.kotlin 19 | implementation presentationDependencies.javaxInject 20 | implementation presentationDependencies.rxKotlin 21 | 22 | testImplementation presentationTestDependencies.junit 23 | testImplementation presentationTestDependencies.mockito 24 | testImplementation presentationTestDependencies.assertj 25 | testImplementation presentationTestDependencies.robolectric 26 | 27 | compile project(':domain') 28 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/mapper/BufferooMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.mapper 2 | 3 | import org.buffer.android.boilerplate.data.model.BufferooEntity 4 | import org.buffer.android.boilerplate.domain.model.Bufferoo 5 | import javax.inject.Inject 6 | 7 | 8 | /** 9 | * Map a [BufferooEntity] to and from a [Bufferoo] instance when data is moving between 10 | * this later and the Domain layer 11 | */ 12 | open class BufferooMapper @Inject constructor(): Mapper { 13 | 14 | /** 15 | * Map a [BufferooEntity] instance to a [Bufferoo] instance 16 | */ 17 | override fun mapFromEntity(type: BufferooEntity): Bufferoo { 18 | return Bufferoo(type.name, type.title, type.avatar) 19 | } 20 | 21 | /** 22 | * Map a [Bufferoo] instance to a [BufferooEntity] instance 23 | */ 24 | override fun mapToEntity(type: Bufferoo): BufferooEntity { 25 | return BufferooEntity(type.name, type.title, type.avatar) 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /presentation/src/main/java/org/buffer/android/boilerplate/presentation/browse/BrowseBufferoosContract.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation.browse 2 | 3 | import org.buffer.android.boilerplate.presentation.BasePresenter 4 | import org.buffer.android.boilerplate.presentation.BaseView 5 | import org.buffer.android.boilerplate.presentation.model.BufferooView 6 | 7 | /** 8 | * Defines a contract of operations between the Browse Presenter and Browse View 9 | */ 10 | interface BrowseBufferoosContract { 11 | 12 | interface View : BaseView { 13 | 14 | fun showProgress() 15 | 16 | fun hideProgress() 17 | 18 | fun showBufferoos(bufferoos: List) 19 | 20 | fun hideBufferoos() 21 | 22 | fun showErrorState() 23 | 24 | fun hideErrorState() 25 | 26 | fun showEmptyState() 27 | 28 | fun hideEmptyState() 29 | 30 | } 31 | 32 | interface Presenter : BasePresenter { 33 | 34 | fun retrieveBufferoos() 35 | 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /cache/src/test/java/org/buffer/android/boilerplate/cache/test/factory/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.test.factory 2 | 3 | import java.util.concurrent.ThreadLocalRandom 4 | 5 | /** 6 | * Factory class for data instances 7 | */ 8 | class DataFactory { 9 | 10 | companion object Factory { 11 | 12 | fun randomUuid(): String { 13 | return java.util.UUID.randomUUID().toString() 14 | } 15 | 16 | fun randomInt(): Int { 17 | return ThreadLocalRandom.current().nextInt(0, 1000 + 1) 18 | } 19 | 20 | fun randomLong(): Long { 21 | return randomInt().toLong() 22 | } 23 | 24 | fun randomBoolean(): Boolean { 25 | return Math.random() < 0.5 26 | } 27 | 28 | fun makeStringList(count: Int): List { 29 | val items: MutableList = mutableListOf() 30 | repeat(count) { 31 | items.add(randomUuid()) 32 | } 33 | return items 34 | } 35 | 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /data/src/test/java/org/buffer/android/boilerplate/data/test/factory/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.test.factory 2 | 3 | import java.util.concurrent.ThreadLocalRandom 4 | 5 | /** 6 | * Factory class for data instances 7 | */ 8 | class DataFactory { 9 | 10 | companion object Factory { 11 | 12 | fun randomUuid(): String { 13 | return java.util.UUID.randomUUID().toString() 14 | } 15 | 16 | fun randomInt(): Int { 17 | return ThreadLocalRandom.current().nextInt(0, 1000 + 1) 18 | } 19 | 20 | fun randomLong(): Long { 21 | return randomInt().toLong() 22 | } 23 | 24 | fun randomBoolean(): Boolean { 25 | return Math.random() < 0.5 26 | } 27 | 28 | fun makeStringList(count: Int): List { 29 | val items: MutableList = mutableListOf() 30 | repeat(count) { 31 | items.add(randomUuid()) 32 | } 33 | return items 34 | } 35 | 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection 2 | 3 | import android.app.Application 4 | import dagger.BindsInstance 5 | import dagger.Component 6 | import dagger.android.support.AndroidSupportInjectionModule 7 | import org.buffer.android.boilerplate.ui.BufferooApplication 8 | import org.buffer.android.boilerplate.ui.injection.module.ActivityBindingModule 9 | import org.buffer.android.boilerplate.ui.injection.module.ApplicationModule 10 | import org.buffer.android.boilerplate.ui.injection.scopes.PerApplication 11 | 12 | @PerApplication 13 | @Component(modules = arrayOf(ActivityBindingModule::class, ApplicationModule::class, 14 | AndroidSupportInjectionModule::class)) 15 | interface ApplicationComponent { 16 | 17 | @Component.Builder 18 | interface Builder { 19 | @BindsInstance fun application(application: Application): Builder 20 | fun build(): ApplicationComponent 21 | } 22 | 23 | fun inject(app: BufferooApplication) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /mobile-ui/src/test/java/org/buffer/android/boilerplate/ui/test/factory/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test.factory 2 | 3 | import java.util.concurrent.ThreadLocalRandom 4 | 5 | /** 6 | * Factory class for data instances 7 | */ 8 | class DataFactory { 9 | 10 | companion object Factory { 11 | 12 | fun randomUuid(): String { 13 | return java.util.UUID.randomUUID().toString() 14 | } 15 | 16 | fun randomInt(): Int { 17 | return ThreadLocalRandom.current().nextInt(0, 1000 + 1) 18 | } 19 | 20 | fun randomLong(): Long { 21 | return randomInt().toLong() 22 | } 23 | 24 | fun randomBoolean(): Boolean { 25 | return Math.random() < 0.5 26 | } 27 | 28 | fun makeStringList(count: Int): List { 29 | val items: MutableList = mutableListOf() 30 | repeat(count) { 31 | items.add(randomUuid()) 32 | } 33 | return items 34 | } 35 | 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /remote/src/test/java/org/buffer/android/boilerplate/remote/test/factory/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote.test.factory 2 | 3 | import java.util.concurrent.ThreadLocalRandom 4 | 5 | /** 6 | * Factory class for data instances 7 | */ 8 | class DataFactory { 9 | 10 | companion object Factory { 11 | 12 | fun randomUuid(): String { 13 | return java.util.UUID.randomUUID().toString() 14 | } 15 | 16 | fun randomInt(): Int { 17 | return ThreadLocalRandom.current().nextInt(0, 1000 + 1) 18 | } 19 | 20 | fun randomLong(): Long { 21 | return randomInt().toLong() 22 | } 23 | 24 | fun randomBoolean(): Boolean { 25 | return Math.random() < 0.5 26 | } 27 | 28 | fun makeStringList(count: Int): List { 29 | val items: MutableList = mutableListOf() 30 | repeat(count) { 31 | items.add(randomUuid()) 32 | } 33 | return items 34 | } 35 | 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/test/factory/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test.factory.ui 2 | 3 | import java.util.concurrent.ThreadLocalRandom 4 | 5 | /** 6 | * Factory class for data instances 7 | */ 8 | class DataFactory { 9 | 10 | companion object Factory { 11 | 12 | fun randomUuid(): String { 13 | return java.util.UUID.randomUUID().toString() 14 | } 15 | 16 | fun randomInt(): Int { 17 | return ThreadLocalRandom.current().nextInt(0, 1000 + 1) 18 | } 19 | 20 | fun randomLong(): Long { 21 | return randomInt().toLong() 22 | } 23 | 24 | fun randomBoolean(): Boolean { 25 | return Math.random() < 0.5 26 | } 27 | 28 | fun makeStringList(count: Int): List { 29 | val items: MutableList = mutableListOf() 30 | repeat(count) { 31 | items.add(randomUuid()) 32 | } 33 | return items 34 | } 35 | 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /remote/src/test/java/org/buffer/android/boilerplate/remote/mapper/BufferooEntityMapperTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote.mapper 2 | 3 | import org.buffer.android.boilerplate.remote.test.factory.BufferooFactory 4 | import org.junit.Before 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.JUnit4 8 | import kotlin.test.assertEquals 9 | 10 | @RunWith(JUnit4::class) 11 | class BufferooEntityMapperTest { 12 | 13 | private lateinit var bufferooEntityMapper: BufferooEntityMapper 14 | 15 | @Before 16 | fun setUp() { 17 | bufferooEntityMapper = BufferooEntityMapper() 18 | } 19 | 20 | @Test 21 | fun mapFromRemoteMapsData() { 22 | val bufferooModel = BufferooFactory.makeBufferooModel() 23 | val bufferooEntity = bufferooEntityMapper.mapFromRemote(bufferooModel) 24 | 25 | assertEquals(bufferooModel.name, bufferooEntity.name) 26 | assertEquals(bufferooModel.title, bufferooEntity.title) 27 | assertEquals(bufferooModel.avatar, bufferooEntity.avatar) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /mobile-ui/src/test/java/org/buffer/android/boilerplate/ui/BufferooMapperTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui 2 | 3 | import org.buffer.android.boilerplate.ui.mapper.BufferooMapper 4 | import org.buffer.android.boilerplate.ui.test.factory.BufferooFactory 5 | import org.junit.Before 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | import org.junit.runners.JUnit4 9 | import kotlin.test.assertEquals 10 | 11 | @RunWith(JUnit4::class) 12 | class BufferooMapperTest { 13 | 14 | private lateinit var bufferooMapper: BufferooMapper 15 | 16 | @Before 17 | fun setUp() { 18 | bufferooMapper = BufferooMapper() 19 | } 20 | 21 | @Test 22 | fun mapToViewMapsData() { 23 | val bufferooView = BufferooFactory.makeBufferooView() 24 | val bufferooViewModel = bufferooMapper.mapToViewModel(bufferooView) 25 | 26 | assertEquals(bufferooView.name, bufferooViewModel.name) 27 | assertEquals(bufferooView.title, bufferooViewModel.title) 28 | assertEquals(bufferooView.avatar, bufferooViewModel.avatar) 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /remote/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | sourceCompatibility = 1.7 4 | targetCompatibility = 1.7 5 | 6 | dependencies { 7 | def remoteDependencies = rootProject.ext.remoteDependencies 8 | def remoteTestDependencies = rootProject.ext.remoteTestDependencies 9 | 10 | implementation remoteDependencies.javaxAnnotation 11 | 12 | implementation remoteDependencies.kotlin 13 | implementation remoteDependencies.javaxInject 14 | implementation remoteDependencies.rxKotlin 15 | implementation remoteDependencies.gson 16 | implementation remoteDependencies.okHttp 17 | implementation remoteDependencies.okHttpLogger 18 | implementation remoteDependencies.retrofit 19 | implementation remoteDependencies.retrofitConverter 20 | implementation remoteDependencies.retrofitAdapter 21 | 22 | testImplementation remoteTestDependencies.junit 23 | testImplementation remoteTestDependencies.kotlinJUnit 24 | testImplementation remoteTestDependencies.mockito 25 | testImplementation remoteTestDependencies.assertj 26 | 27 | compile project(':data') 28 | } -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/mapper/BufferooEntityMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.mapper 2 | 3 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 4 | import org.buffer.android.boilerplate.data.model.BufferooEntity 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Map a [CachedBufferoo] instance to and from a [BufferooEntity] instance when data is moving between 9 | * this later and the Data layer 10 | */ 11 | class BufferooEntityMapper @Inject constructor(): EntityMapper { 12 | 13 | /** 14 | * Map a [BufferooEntity] instance to a [CachedBufferoo] instance 15 | */ 16 | override fun mapToCached(type: BufferooEntity): CachedBufferoo { 17 | return CachedBufferoo(type.name, type.title, type.avatar) 18 | } 19 | 20 | /** 21 | * Map a [CachedBufferoo] instance to a [BufferooEntity] instance 22 | */ 23 | override fun mapFromCached(type: CachedBufferoo): BufferooEntity { 24 | return BufferooEntity(type.name, type.title, type.avatar) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /licence.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2017] [Buffer inc] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/PreferencesHelper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | import javax.inject.Inject 7 | import javax.inject.Singleton 8 | 9 | /** 10 | * General Preferences Helper class, used for storing preference values using the Preference API 11 | */ 12 | @Singleton 13 | class PreferencesHelper @Inject constructor(context: Context) { 14 | 15 | companion object { 16 | private val PREF_BUFFER_PACKAGE_NAME = "org.buffer.android.boilerplate.preferences" 17 | 18 | private val PREF_KEY_LAST_CACHE = "last_cache" 19 | } 20 | 21 | private val bufferPref: SharedPreferences 22 | 23 | init { 24 | bufferPref = context.getSharedPreferences(PREF_BUFFER_PACKAGE_NAME, Context.MODE_PRIVATE) 25 | } 26 | 27 | /** 28 | * Store and retrieve the last time data was cached 29 | */ 30 | var lastCacheTime: Long 31 | get() = bufferPref.getLong(PREF_KEY_LAST_CACHE, 0) 32 | set(lastCache) = bufferPref.edit().putLong(PREF_KEY_LAST_CACHE, lastCache).apply() 33 | 34 | } 35 | -------------------------------------------------------------------------------- /mobile-ui/src/main/res/layout/activity_browse.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/browse/GetBufferoos.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.interactor.browse 2 | 3 | import io.reactivex.Single 4 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 5 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 6 | import org.buffer.android.boilerplate.domain.interactor.SingleUseCase 7 | import org.buffer.android.boilerplate.domain.model.Bufferoo 8 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 9 | import javax.inject.Inject 10 | 11 | /** 12 | * Use case used for retreiving a [List] of [Bufferoo] instances from the [BufferooRepository] 13 | */ 14 | open class GetBufferoos @Inject constructor(val bufferooRepository: BufferooRepository, 15 | threadExecutor: ThreadExecutor, 16 | postExecutionThread: PostExecutionThread): 17 | SingleUseCase, Void?>(threadExecutor, postExecutionThread) { 18 | 19 | public override fun buildUseCaseObservable(params: Void?): Single> { 20 | return bufferooRepository.getBufferoos() 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/source/BufferooRemoteDataStore.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.source 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Single 5 | import org.buffer.android.boilerplate.data.model.BufferooEntity 6 | import org.buffer.android.boilerplate.data.repository.BufferooDataStore 7 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 8 | import javax.inject.Inject 9 | 10 | /** 11 | * Implementation of the [BufferooDataStore] interface to provide a means of communicating 12 | * with the remote data source 13 | */ 14 | open class BufferooRemoteDataStore @Inject constructor(private val bufferooRemote: BufferooRemote) : 15 | BufferooDataStore { 16 | 17 | override fun clearBufferoos(): Completable { 18 | throw UnsupportedOperationException() 19 | } 20 | 21 | override fun saveBufferoos(bufferoos: List): Completable { 22 | throw UnsupportedOperationException() 23 | } 24 | 25 | /** 26 | * Retrieve a list of [BufferooEntity] instances from the API 27 | */ 28 | override fun getBufferoos(): Single> { 29 | return bufferooRemote.getBufferoos() 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/BufferooApplication.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.support.v4.BuildConfig 6 | import dagger.android.AndroidInjector 7 | import dagger.android.DispatchingAndroidInjector 8 | import dagger.android.HasActivityInjector 9 | import org.buffer.android.boilerplate.ui.injection.DaggerApplicationComponent 10 | import timber.log.Timber 11 | import javax.inject.Inject 12 | 13 | class BufferooApplication : Application(), HasActivityInjector { 14 | 15 | @Inject lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | DaggerApplicationComponent 20 | .builder() 21 | .application(this) 22 | .build() 23 | .inject(this) 24 | setupTimber() 25 | } 26 | 27 | private fun setupTimber() { 28 | if (BuildConfig.DEBUG) { 29 | Timber.plant(Timber.DebugTree()) 30 | } 31 | } 32 | 33 | override fun activityInjector(): AndroidInjector { 34 | return activityDispatchingAndroidInjector 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /remote/src/test/java/org/buffer/android/boilerplate/remote/test/factory/BufferooFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote.test.factory 2 | 3 | import org.buffer.android.boilerplate.remote.test.factory.DataFactory.Factory.randomUuid 4 | import org.buffer.android.boilerplate.remote.BufferooService 5 | import org.buffer.android.boilerplate.remote.model.BufferooModel 6 | 7 | /** 8 | * Factory class for Bufferoo related instances 9 | */ 10 | class BufferooFactory { 11 | 12 | companion object Factory { 13 | 14 | fun makeBufferooResponse(): BufferooService.BufferooResponse { 15 | val bufferooResponse = BufferooService.BufferooResponse() 16 | bufferooResponse.team = makeBufferooModelList(5) 17 | return bufferooResponse 18 | } 19 | 20 | fun makeBufferooModelList(count: Int): List { 21 | val bufferooEntities = mutableListOf() 22 | repeat(count) { 23 | bufferooEntities.add(makeBufferooModel()) 24 | } 25 | return bufferooEntities 26 | } 27 | 28 | fun makeBufferooModel(): BufferooModel { 29 | return BufferooModel(randomUuid(), randomUuid(), randomUuid()) 30 | } 31 | 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /remote/src/main/java/org/buffer/android/boilerplate/remote/BufferooRemoteImpl.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote 2 | 3 | import io.reactivex.Single 4 | import org.buffer.android.boilerplate.data.model.BufferooEntity 5 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 6 | import org.buffer.android.boilerplate.remote.mapper.BufferooEntityMapper 7 | import javax.inject.Inject 8 | 9 | /** 10 | * Remote implementation for retrieving Bufferoo instances. This class implements the 11 | * [BufferooRemote] from the Data layer as it is that layers responsibility for defining the 12 | * operations in which data store implementation layers can carry out. 13 | */ 14 | class BufferooRemoteImpl @Inject constructor(private val bufferooService: BufferooService, 15 | private val entityMapper: BufferooEntityMapper) : 16 | BufferooRemote { 17 | 18 | /** 19 | * Retrieve a list of [BufferooEntity] instances from the [BufferooService]. 20 | */ 21 | override fun getBufferoos(): Single> { 22 | return bufferooService.getBufferoos() 23 | .map { 24 | it.team.map { listItem -> 25 | entityMapper.mapFromRemote(listItem) 26 | } 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/BrowseActivityModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import org.buffer.android.boilerplate.domain.interactor.browse.GetBufferoos 6 | import org.buffer.android.boilerplate.presentation.browse.BrowseBufferoosContract 7 | import org.buffer.android.boilerplate.presentation.browse.BrowseBufferoosPresenter 8 | import org.buffer.android.boilerplate.presentation.mapper.BufferooMapper 9 | import org.buffer.android.boilerplate.ui.browse.BrowseActivity 10 | import org.buffer.android.boilerplate.ui.injection.scopes.PerActivity 11 | 12 | 13 | 14 | /** 15 | * Module used to provide dependencies at an activity-level. 16 | */ 17 | @Module 18 | open class BrowseActivityModule { 19 | 20 | @PerActivity 21 | @Provides 22 | internal fun provideBrowseView(browseActivity: BrowseActivity): BrowseBufferoosContract.View { 23 | return browseActivity 24 | } 25 | 26 | @PerActivity 27 | @Provides 28 | internal fun provideBrowsePresenter(mainView: BrowseBufferoosContract.View, 29 | getBufferoos: GetBufferoos, mapper: BufferooMapper): 30 | BrowseBufferoosContract.Presenter { 31 | return BrowseBufferoosPresenter(mainView, getBufferoos, mapper) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /data/src/test/java/org/buffer/android/boilerplate/data/test/factory/BufferooFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.test.factory 2 | 3 | import org.buffer.android.boilerplate.data.model.BufferooEntity 4 | import org.buffer.android.boilerplate.data.test.factory.DataFactory.Factory.randomUuid 5 | import org.buffer.android.boilerplate.domain.model.Bufferoo 6 | 7 | /** 8 | * Factory class for Bufferoo related instances 9 | */ 10 | class BufferooFactory { 11 | 12 | companion object Factory { 13 | 14 | fun makeBufferooEntity(): BufferooEntity { 15 | return BufferooEntity(randomUuid(), randomUuid(), randomUuid()) 16 | } 17 | 18 | fun makeBufferoo(): Bufferoo { 19 | return Bufferoo(randomUuid(), randomUuid(), randomUuid()) 20 | } 21 | 22 | fun makeBufferooEntityList(count: Int): List { 23 | val bufferooEntities = mutableListOf() 24 | repeat(count) { 25 | bufferooEntities.add(makeBufferooEntity()) 26 | } 27 | return bufferooEntities 28 | } 29 | 30 | fun makeBufferooList(count: Int): List { 31 | val bufferoos = mutableListOf() 32 | repeat(count) { 33 | bufferoos.add(makeBufferoo()) 34 | } 35 | return bufferoos 36 | } 37 | 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/test/TestApplication.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.support.test.InstrumentationRegistry 6 | import dagger.android.AndroidInjector 7 | import dagger.android.DispatchingAndroidInjector 8 | import dagger.android.HasActivityInjector 9 | import org.buffer.android.boilerplate.ui.injection.component.DaggerTestApplicationComponent 10 | import org.buffer.android.boilerplate.ui.injection.component.TestApplicationComponent 11 | import javax.inject.Inject 12 | 13 | class TestApplication: Application(), HasActivityInjector { 14 | 15 | @Inject lateinit var injector: DispatchingAndroidInjector 16 | 17 | private lateinit var appComponent: TestApplicationComponent 18 | 19 | override fun onCreate() { 20 | super.onCreate() 21 | appComponent = DaggerTestApplicationComponent.builder().application(this).build() 22 | appComponent.inject(this) 23 | } 24 | 25 | companion object { 26 | 27 | fun appComponent(): TestApplicationComponent { 28 | return (InstrumentationRegistry.getTargetContext().applicationContext as TestApplication). 29 | appComponent 30 | } 31 | 32 | } 33 | 34 | override fun activityInjector(): AndroidInjector { 35 | return injector 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/res/layout/item_bufferoo.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 25 | 26 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/db/DbOpenHelper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.db 2 | 3 | 4 | import android.content.Context 5 | import android.database.sqlite.SQLiteDatabase 6 | import android.database.sqlite.SQLiteOpenHelper 7 | import android.os.Build 8 | import javax.inject.Inject 9 | 10 | /** 11 | * Standard DB class for configuring the Database and handling migrations during upgrades 12 | */ 13 | class DbOpenHelper @Inject constructor(context: Context) : 14 | SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { 15 | 16 | companion object { 17 | val DATABASE_NAME = "buffer-clean-arch-boilerplate.db" 18 | val DATABASE_VERSION = 1 19 | } 20 | 21 | override fun onConfigure(db: SQLiteDatabase) { 22 | super.onConfigure(db) 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 24 | db.setForeignKeyConstraintsEnabled(true) 25 | } else { 26 | db.execSQL("PRAGMA foreign_keys=ON;") 27 | } 28 | } 29 | 30 | override fun onCreate(db: SQLiteDatabase) { 31 | db.beginTransaction() 32 | try { 33 | db.execSQL(Db.BufferooTable.CREATE) 34 | db.setTransactionSuccessful() 35 | } finally { 36 | db.endTransaction() 37 | } 38 | } 39 | 40 | override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) { 41 | 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/source/BufferooDataStoreFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.source 2 | 3 | import org.buffer.android.boilerplate.data.repository.BufferooCache 4 | import org.buffer.android.boilerplate.data.repository.BufferooDataStore 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Create an instance of a BufferooDataStore 9 | */ 10 | open class BufferooDataStoreFactory @Inject constructor( 11 | private val bufferooCache: BufferooCache, 12 | private val bufferooCacheDataStore: BufferooCacheDataStore, 13 | private val bufferooRemoteDataStore: BufferooRemoteDataStore) { 14 | 15 | /** 16 | * Returns a DataStore based on whether or not there is content in the cache and the cache 17 | * has not expired 18 | */ 19 | open fun retrieveDataStore(): BufferooDataStore { 20 | if (bufferooCache.isCached() && !bufferooCache.isExpired()) { 21 | return retrieveCacheDataStore() 22 | } 23 | return retrieveRemoteDataStore() 24 | } 25 | 26 | /** 27 | * Return an instance of the Remote Data Store 28 | */ 29 | open fun retrieveCacheDataStore(): BufferooDataStore { 30 | return bufferooCacheDataStore 31 | } 32 | 33 | /** 34 | * Return an instance of the Cache Data Store 35 | */ 36 | open fun retrieveRemoteDataStore(): BufferooDataStore { 37 | return bufferooRemoteDataStore 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /cache/src/test/java/org/buffer/android/boilerplate/cache/test/factory/BufferooFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.test.factory 2 | 3 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 4 | import org.buffer.android.boilerplate.data.model.BufferooEntity 5 | import org.buffer.android.boilerplate.cache.test.factory.DataFactory.Factory.randomUuid 6 | 7 | /** 8 | * Factory class for Bufferoo related instances 9 | */ 10 | class BufferooFactory { 11 | 12 | companion object Factory { 13 | 14 | fun makeCachedBufferoo(): CachedBufferoo { 15 | return CachedBufferoo(randomUuid(), randomUuid(), randomUuid()) 16 | } 17 | 18 | fun makeBufferooEntity(): BufferooEntity { 19 | return BufferooEntity(randomUuid(), randomUuid(), randomUuid()) 20 | } 21 | 22 | fun makeBufferooEntityList(count: Int): List { 23 | val bufferooEntities = mutableListOf() 24 | repeat(count) { 25 | bufferooEntities.add(makeBufferooEntity()) 26 | } 27 | return bufferooEntities 28 | } 29 | 30 | fun makeCachedBufferooList(count: Int): List { 31 | val cachedBufferoos = mutableListOf() 32 | repeat(count) { 33 | cachedBufferoos.add(makeCachedBufferoo()) 34 | } 35 | return cachedBufferoos 36 | } 37 | 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: 3 | - oraclejdk8 4 | # Use the Travis Container-Based Infrastructure 5 | 6 | env: 7 | global: 8 | - ANDROID_API_LEVEL=26 9 | - EMULATOR_API_LEVEL=22 10 | - ANDROID_BUILD_TOOLS_VERSION=26.0.0 11 | - ANDROID_ABI=armeabi-v7a 12 | - ANDROID_TAG=google_apis 13 | - ANDROID_TARGET=android-$ANDROID_API_LEVEL 14 | - ADB_INSTALL_TIMEOUT=20 # minutes (2 minutes by default) 15 | 16 | android: 17 | components: 18 | - tools 19 | - platform-tools 20 | - android-$EMULATOR_API_LEVEL 21 | - build-tools-$ANDROID_BUILD_TOOLS_VERSION 22 | - android-$ANDROID_API_LEVEL 23 | # For Google APIs 24 | - addon-google_apis-google-$ANDROID_API_LEVEL 25 | # Google Play Services 26 | - extra-google-google_play_services 27 | # Support library 28 | - extra-android-support 29 | # Latest artifacts in local repository 30 | - extra-google-m2repository 31 | - extra-android-m2repository 32 | # Specify at least one system image 33 | - sys-img-armeabi-v7a-android-$EMULATOR_API_LEVEL 34 | 35 | before_script: 36 | - echo no | android create avd --force -n test -t android-$EMULATOR_API_LEVEL --abi armeabi-v7a 37 | - emulator -avd test -no-audio -no-window & 38 | - android-wait-for-emulator 39 | - adb shell input keyevent 82 & 40 | 41 | script: 42 | - ./gradlew build 43 | - ./gradlew build connectedAndroidTest --stacktrace 44 | 45 | after_success: 46 | - bash <(curl -s https://codecov.io/bash) 47 | -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/db/mapper/BufferooMapper.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.db.mapper 2 | 3 | import android.content.ContentValues 4 | import android.database.Cursor 5 | import org.buffer.android.boilerplate.cache.db.Db 6 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 7 | import javax.inject.Inject 8 | 9 | /** 10 | * Maps a [CachedBufferoo] instance to a database entity. 11 | */ 12 | class BufferooMapper @Inject constructor(): ModelDbMapper { 13 | 14 | /** 15 | * Construct an instance of [ContentValues] using the given [CachedBufferoo] 16 | */ 17 | override fun toContentValues(model: CachedBufferoo): ContentValues { 18 | val values = ContentValues() 19 | values.put(Db.BufferooTable.NAME, model.name) 20 | values.put(Db.BufferooTable.TITLE, model.title) 21 | values.put(Db.BufferooTable.AVATAR, model.avatar) 22 | return values 23 | } 24 | 25 | /** 26 | * Parse the cursor creating a [CachedBufferoo] instance. 27 | */ 28 | override fun parseCursor(cursor: Cursor): CachedBufferoo { 29 | val name = cursor.getString(cursor.getColumnIndexOrThrow(Db.BufferooTable.NAME)) 30 | val title = cursor.getString(cursor.getColumnIndexOrThrow(Db.BufferooTable.TITLE)) 31 | val avatar = cursor.getString(cursor.getColumnIndexOrThrow(Db.BufferooTable.AVATAR)) 32 | return CachedBufferoo(name, title, avatar) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/injection/component/TestApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.component 2 | 3 | import android.app.Application 4 | import dagger.BindsInstance 5 | import dagger.Component 6 | import dagger.android.support.AndroidSupportInjectionModule 7 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 8 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 9 | import org.buffer.android.boilerplate.ui.injection.ApplicationComponent 10 | import org.buffer.android.boilerplate.ui.injection.module.ActivityBindingModule 11 | import org.buffer.android.boilerplate.ui.injection.module.TestApplicationModule 12 | import org.buffer.android.boilerplate.ui.injection.scopes.PerApplication 13 | import org.buffer.android.boilerplate.ui.test.TestApplication 14 | 15 | @Component(modules = arrayOf(TestApplicationModule::class, ActivityBindingModule::class, 16 | AndroidSupportInjectionModule::class)) 17 | @PerApplication 18 | interface TestApplicationComponent : ApplicationComponent { 19 | 20 | fun bufferooRepository(): BufferooRepository 21 | 22 | fun postExecutionThread(): PostExecutionThread 23 | 24 | fun inject(application: TestApplication) 25 | 26 | @Component.Builder 27 | interface Builder { 28 | @BindsInstance 29 | fun application(application: Application): TestApplicationComponent.Builder 30 | 31 | fun build(): TestApplicationComponent 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/source/BufferooCacheDataStore.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.source 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Single 5 | import org.buffer.android.boilerplate.data.model.BufferooEntity 6 | import org.buffer.android.boilerplate.data.repository.BufferooCache 7 | import org.buffer.android.boilerplate.data.repository.BufferooDataStore 8 | import javax.inject.Inject 9 | 10 | /** 11 | * Implementation of the [BufferooDataStore] interface to provide a means of communicating 12 | * with the local data source 13 | */ 14 | open class BufferooCacheDataStore @Inject constructor(private val bufferooCache: BufferooCache) : 15 | BufferooDataStore { 16 | 17 | /** 18 | * Clear all Bufferoos from the cache 19 | */ 20 | override fun clearBufferoos(): Completable { 21 | return bufferooCache.clearBufferoos() 22 | } 23 | 24 | /** 25 | * Save a given [List] of [BufferooEntity] instances to the cache 26 | */ 27 | override fun saveBufferoos(bufferoos: List): Completable { 28 | return bufferooCache.saveBufferoos(bufferoos) 29 | .doOnComplete { 30 | bufferooCache.setLastCacheTime(System.currentTimeMillis()) 31 | } 32 | } 33 | 34 | /** 35 | * Retrieve a list of [BufferooEntity] instance from the cache 36 | */ 37 | override fun getBufferoos(): Single> { 38 | return bufferooCache.getBufferoos() 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/CompletableUseCase.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.interactor 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.disposables.Disposable 5 | import io.reactivex.disposables.Disposables 6 | import io.reactivex.schedulers.Schedulers 7 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 8 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 9 | 10 | /** 11 | * Abstract class for a UseCase that returns an instance of a [Completable]. 12 | */ 13 | abstract class CompletableUseCase protected constructor( 14 | private val threadExecutor: ThreadExecutor, 15 | private val postExecutionThread: PostExecutionThread) { 16 | 17 | private val subscription = Disposables.empty() 18 | 19 | /** 20 | * Builds a [Completable] which will be used when the current [CompletableUseCase] is executed. 21 | */ 22 | protected abstract fun buildUseCaseObservable(params: Params): Completable 23 | 24 | /** 25 | * Executes the current use case. 26 | */ 27 | fun execute(params: Params): Completable { 28 | return this.buildUseCaseObservable(params) 29 | .subscribeOn(Schedulers.from(threadExecutor)) 30 | .observeOn(postExecutionThread.scheduler) 31 | } 32 | 33 | /** 34 | * Unsubscribes from current [Disposable]. 35 | */ 36 | fun unsubscribe() { 37 | if (!subscription.isDisposed) { 38 | subscription.dispose() 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/repository/BufferooCache.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.repository 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Single 5 | import org.buffer.android.boilerplate.data.model.BufferooEntity 6 | 7 | /** 8 | * Interface defining methods for the caching of Bufferroos. This is to be implemented by the 9 | * cache layer, using this interface as a way of communicating. 10 | */ 11 | interface BufferooCache { 12 | 13 | /** 14 | * Clear all Bufferoos from the cache 15 | */ 16 | fun clearBufferoos(): Completable 17 | 18 | /** 19 | * Save a given list of Bufferoos to the cache 20 | */ 21 | fun saveBufferoos(bufferoos: List): Completable 22 | 23 | /** 24 | * Retrieve a list of Bufferoos, from the cache 25 | */ 26 | fun getBufferoos(): Single> 27 | 28 | /** 29 | * Checks if an element (User) exists in the cache. 30 | 31 | * @param userId The id used to look for inside the cache. 32 | * * 33 | * @return true if the element is cached, otherwise false. 34 | */ 35 | fun isCached(): Boolean 36 | 37 | /** 38 | * Checks if an element (User) exists in the cache. 39 | 40 | * @param userId The id used to look for inside the cache. 41 | * * 42 | * @return true if the element is cached, otherwise false. 43 | */ 44 | fun setLastCacheTime(lastCache: Long) 45 | 46 | /** 47 | * Checks if the cache is expired. 48 | 49 | * @return true, the cache is expired, otherwise false. 50 | */ 51 | fun isExpired(): Boolean 52 | 53 | } -------------------------------------------------------------------------------- /data/src/test/java/org/buffer/android/boilerplate/data/mapper/BufferooMapperTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.mapper 2 | 3 | import org.buffer.android.boilerplate.data.model.BufferooEntity 4 | import org.buffer.android.boilerplate.data.test.factory.BufferooFactory 5 | import org.buffer.android.boilerplate.domain.model.Bufferoo 6 | import org.junit.Before 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.JUnit4 10 | import kotlin.test.assertEquals 11 | 12 | @RunWith(JUnit4::class) 13 | class BufferooMapperTest { 14 | 15 | private lateinit var bufferooMapper: BufferooMapper 16 | 17 | @Before 18 | fun setUp() { 19 | bufferooMapper = BufferooMapper() 20 | } 21 | 22 | @Test 23 | fun mapFromEntityMapsData() { 24 | val bufferooEntity = BufferooFactory.makeBufferooEntity() 25 | val bufferoo = bufferooMapper.mapFromEntity(bufferooEntity) 26 | 27 | assertBufferooDataEquality(bufferooEntity, bufferoo) 28 | } 29 | 30 | @Test 31 | fun mapToEntityMapsData() { 32 | val cachedBufferoo = BufferooFactory.makeBufferoo() 33 | val bufferooEntity = bufferooMapper.mapToEntity(cachedBufferoo) 34 | 35 | assertBufferooDataEquality(bufferooEntity, cachedBufferoo) 36 | } 37 | 38 | private fun assertBufferooDataEquality(bufferooEntity: BufferooEntity, 39 | bufferoo: Bufferoo) { 40 | assertEquals(bufferooEntity.name, bufferoo.name) 41 | assertEquals(bufferooEntity.title, bufferoo.title) 42 | assertEquals(bufferooEntity.avatar, bufferoo.avatar) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /cache/src/test/java/org/buffer/android/boilerplate/cache/mapper/BufferooEntityMapperTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.mapper 2 | 3 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 4 | import org.buffer.android.boilerplate.cache.test.factory.BufferooFactory 5 | import org.buffer.android.boilerplate.data.model.BufferooEntity 6 | import org.junit.Before 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.JUnit4 10 | import kotlin.test.assertEquals 11 | 12 | @RunWith(JUnit4::class) 13 | class BufferooEntityMapperTest { 14 | 15 | private lateinit var bufferooEntityMapper: BufferooEntityMapper 16 | 17 | @Before 18 | fun setUp() { 19 | bufferooEntityMapper = BufferooEntityMapper() 20 | } 21 | 22 | @Test 23 | fun mapToCachedMapsData() { 24 | val bufferooEntity = BufferooFactory.makeBufferooEntity() 25 | val cachedBufferoo = bufferooEntityMapper.mapToCached(bufferooEntity) 26 | 27 | assertBufferooDataEquality(bufferooEntity, cachedBufferoo) 28 | } 29 | 30 | @Test 31 | fun mapFromCachedMapsData() { 32 | val cachedBufferoo = BufferooFactory.makeCachedBufferoo() 33 | val bufferooEntity = bufferooEntityMapper.mapFromCached(cachedBufferoo) 34 | 35 | assertBufferooDataEquality(bufferooEntity, cachedBufferoo) 36 | } 37 | 38 | private fun assertBufferooDataEquality(bufferooEntity: BufferooEntity, 39 | cachedBufferoo: CachedBufferoo) { 40 | assertEquals(bufferooEntity.name, cachedBufferoo.name) 41 | assertEquals(bufferooEntity.title, cachedBufferoo.title) 42 | assertEquals(bufferooEntity.avatar, cachedBufferoo.avatar) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/SingleUseCase.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.interactor 2 | 3 | import io.reactivex.Single 4 | import io.reactivex.disposables.CompositeDisposable 5 | import io.reactivex.disposables.Disposable 6 | import io.reactivex.observers.DisposableSingleObserver 7 | import io.reactivex.schedulers.Schedulers 8 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 9 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 10 | 11 | 12 | 13 | /** 14 | * Abstract class for a UseCase that returns an instance of a [Single]. 15 | */ 16 | abstract class SingleUseCase constructor( 17 | private val threadExecutor: ThreadExecutor, 18 | private val postExecutionThread: PostExecutionThread) { 19 | 20 | private val disposables = CompositeDisposable() 21 | 22 | /** 23 | * Builds a [Single] which will be used when the current [SingleUseCase] is executed. 24 | */ 25 | protected abstract fun buildUseCaseObservable(params: Params? = null): Single 26 | 27 | /** 28 | * Executes the current use case. 29 | */ 30 | open fun execute(singleObserver: DisposableSingleObserver, params: Params? = null) { 31 | val single = this.buildUseCaseObservable(params) 32 | .subscribeOn(Schedulers.from(threadExecutor)) 33 | .observeOn(postExecutionThread.scheduler) as Single 34 | addDisposable(single.subscribeWith(singleObserver)) 35 | } 36 | 37 | /** 38 | * Dispose from current [CompositeDisposable]. 39 | */ 40 | fun dispose() { 41 | if (!disposables.isDisposed) disposables.dispose() 42 | } 43 | 44 | /** 45 | * Dispose from current [CompositeDisposable]. 46 | */ 47 | private fun addDisposable(disposable: Disposable) { 48 | disposables.add(disposable) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/browse/BrowseAdapter.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.browse 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import com.bumptech.glide.Glide 10 | import com.bumptech.glide.request.RequestOptions 11 | import org.buffer.android.boilerplate.ui.R 12 | import org.buffer.android.boilerplate.ui.model.BufferooViewModel 13 | import javax.inject.Inject 14 | 15 | class BrowseAdapter @Inject constructor(): RecyclerView.Adapter() { 16 | 17 | var bufferoos: List = arrayListOf() 18 | 19 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 20 | val bufferoo = bufferoos[position] 21 | holder.nameText.text = bufferoo.name 22 | holder.titleText.text = bufferoo.title 23 | 24 | Glide.with(holder.itemView.context) 25 | .load(bufferoo.avatar) 26 | .apply(RequestOptions.circleCropTransform()) 27 | .into(holder.avatarImage) 28 | } 29 | 30 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 31 | val itemView = LayoutInflater 32 | .from(parent.context) 33 | .inflate(R.layout.item_bufferoo, parent, false) 34 | return ViewHolder(itemView) 35 | } 36 | 37 | override fun getItemCount(): Int { 38 | return bufferoos.size 39 | } 40 | 41 | inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 42 | var avatarImage: ImageView 43 | var nameText: TextView 44 | var titleText: TextView 45 | 46 | init { 47 | avatarImage = view.findViewById(R.id.image_avatar) 48 | nameText = view.findViewById(R.id.text_name) 49 | titleText = view.findViewById(R.id.text_title) 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /cache/src/test/java/org/buffer/android/boilerplate/cache/db/mapper/BufferooMapperTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.db.mapper 2 | 3 | import android.database.Cursor 4 | import org.buffer.android.boilerplate.cache.BuildConfig 5 | import org.buffer.android.boilerplate.cache.db.Db 6 | import org.buffer.android.boilerplate.cache.db.DbOpenHelper 7 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 8 | import org.buffer.android.boilerplate.cache.test.DefaultConfig 9 | import org.buffer.android.boilerplate.cache.test.factory.BufferooFactory 10 | import org.junit.Before 11 | import org.junit.Test 12 | import org.junit.runner.RunWith 13 | import org.robolectric.RobolectricTestRunner 14 | import org.robolectric.RuntimeEnvironment 15 | import org.robolectric.annotation.Config 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(RobolectricTestRunner::class) 19 | @Config(constants = BuildConfig::class, sdk = intArrayOf(DefaultConfig.EMULATE_SDK)) 20 | class BufferooMapperTest { 21 | 22 | private lateinit var bufferooMapper: BufferooMapper 23 | private val database = DbOpenHelper(RuntimeEnvironment.application).writableDatabase 24 | 25 | @Before 26 | fun setUp() { 27 | bufferooMapper = BufferooMapper() 28 | } 29 | 30 | @Test 31 | fun parseCursorMapsData() { 32 | val cachedBufferoo = BufferooFactory.makeCachedBufferoo() 33 | insertCachedBufferoo(cachedBufferoo) 34 | 35 | val cursor = retrieveCachedBufferooCursor() 36 | assertEquals(cachedBufferoo, bufferooMapper.parseCursor(cursor)) 37 | } 38 | 39 | private fun retrieveCachedBufferooCursor(): Cursor { 40 | val cursor = database.rawQuery("SELECT * FROM " + Db.BufferooTable.TABLE_NAME, null) 41 | cursor.moveToFirst() 42 | return cursor 43 | } 44 | 45 | private fun insertCachedBufferoo(cachedBufferoo: CachedBufferoo) { 46 | database.insertOrThrow(Db.BufferooTable.TABLE_NAME, null, 47 | bufferooMapper.toContentValues(cachedBufferoo)) 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /presentation/src/main/java/org/buffer/android/boilerplate/presentation/browse/BrowseBufferoosPresenter.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation.browse 2 | 3 | import io.reactivex.observers.DisposableSingleObserver 4 | import org.buffer.android.boilerplate.domain.interactor.SingleUseCase 5 | import org.buffer.android.boilerplate.domain.model.Bufferoo 6 | import org.buffer.android.boilerplate.presentation.mapper.BufferooMapper 7 | import javax.inject.Inject 8 | 9 | class BrowseBufferoosPresenter @Inject constructor(val browseView: BrowseBufferoosContract.View, 10 | val getBufferoosUseCase: SingleUseCase, Void>, 11 | val bufferooMapper: BufferooMapper): 12 | BrowseBufferoosContract.Presenter { 13 | 14 | init { 15 | browseView.setPresenter(this) 16 | } 17 | 18 | override fun start() { 19 | retrieveBufferoos() 20 | } 21 | 22 | override fun stop() { 23 | getBufferoosUseCase.dispose() 24 | } 25 | 26 | override fun retrieveBufferoos() { 27 | getBufferoosUseCase.execute(BufferooSubscriber()) 28 | } 29 | 30 | internal fun handleGetBufferoosSuccess(bufferoos: List) { 31 | browseView.hideErrorState() 32 | if (bufferoos.isNotEmpty()) { 33 | browseView.hideEmptyState() 34 | browseView.showBufferoos(bufferoos.map { bufferooMapper.mapToView(it) }) 35 | } else { 36 | browseView.hideBufferoos() 37 | browseView.showEmptyState() 38 | } 39 | } 40 | 41 | inner class BufferooSubscriber: DisposableSingleObserver>() { 42 | 43 | override fun onSuccess(t: List) { 44 | handleGetBufferoosSuccess(t) 45 | } 46 | 47 | override fun onError(exception: Throwable) { 48 | browseView.hideBufferoos() 49 | browseView.hideEmptyState() 50 | browseView.showErrorState() 51 | } 52 | 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/executor/JobExecutor.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.executor 2 | 3 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 4 | import java.util.concurrent.LinkedBlockingQueue 5 | import java.util.concurrent.ThreadFactory 6 | import java.util.concurrent.ThreadPoolExecutor 7 | import java.util.concurrent.TimeUnit 8 | import javax.inject.Inject 9 | 10 | /** 11 | * Decorated [ThreadPoolExecutor] 12 | */ 13 | open class JobExecutor @Inject constructor(): ThreadExecutor { 14 | 15 | private val workQueue: LinkedBlockingQueue 16 | 17 | private val threadPoolExecutor: ThreadPoolExecutor 18 | 19 | private val threadFactory: ThreadFactory 20 | 21 | init { 22 | this.workQueue = LinkedBlockingQueue() 23 | this.threadFactory = JobThreadFactory() 24 | this.threadPoolExecutor = ThreadPoolExecutor(INITIAL_POOL_SIZE, MAX_POOL_SIZE, 25 | KEEP_ALIVE_TIME.toLong(), KEEP_ALIVE_TIME_UNIT, this.workQueue, this.threadFactory) 26 | } 27 | 28 | override fun execute(runnable: Runnable?) { 29 | if (runnable == null) { 30 | throw IllegalArgumentException("Runnable to execute cannot be null") 31 | } 32 | this.threadPoolExecutor.execute(runnable) 33 | } 34 | 35 | private class JobThreadFactory : ThreadFactory { 36 | private var counter = 0 37 | 38 | override fun newThread(runnable: Runnable): Thread { 39 | return Thread(runnable, THREAD_NAME + counter++) 40 | } 41 | 42 | companion object { 43 | private val THREAD_NAME = "android_" 44 | } 45 | } 46 | 47 | companion object { 48 | 49 | private val INITIAL_POOL_SIZE = 3 50 | private val MAX_POOL_SIZE = 5 51 | 52 | // Sets the amount of time an idle thread waits before terminating 53 | private val KEEP_ALIVE_TIME = 10 54 | 55 | // Sets the Time Unit to seconds 56 | private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS 57 | } 58 | } -------------------------------------------------------------------------------- /data/src/test/java/org/buffer/android/boilerplate/data/source/BufferooRemoteDataStoreTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.source 2 | 3 | import com.nhaarman.mockito_kotlin.mock 4 | import com.nhaarman.mockito_kotlin.whenever 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.data.model.BufferooEntity 7 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 8 | import org.buffer.android.boilerplate.data.test.factory.BufferooFactory 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.junit.runners.JUnit4 13 | 14 | @RunWith(JUnit4::class) 15 | class BufferooRemoteDataStoreTest { 16 | 17 | private lateinit var bufferooRemoteDataStore: BufferooRemoteDataStore 18 | 19 | private lateinit var bufferooRemote: BufferooRemote 20 | 21 | @Before 22 | fun setUp() { 23 | bufferooRemote = mock() 24 | bufferooRemoteDataStore = BufferooRemoteDataStore(bufferooRemote) 25 | } 26 | 27 | // 28 | @Test(expected = UnsupportedOperationException::class) 29 | fun clearBufferoosThrowsException() { 30 | bufferooRemoteDataStore.clearBufferoos().test() 31 | } 32 | // 33 | 34 | // 35 | @Test(expected = UnsupportedOperationException::class) 36 | fun saveBufferoosThrowsException() { 37 | bufferooRemoteDataStore.saveBufferoos(BufferooFactory.makeBufferooEntityList(2)).test() 38 | } 39 | // 40 | 41 | // 42 | @Test 43 | fun getBufferoosCompletes() { 44 | stubBufferooCacheGetBufferoos(Single.just(BufferooFactory.makeBufferooEntityList(2))) 45 | val testObserver = bufferooRemote.getBufferoos().test() 46 | testObserver.assertComplete() 47 | } 48 | // 49 | 50 | // 51 | private fun stubBufferooCacheGetBufferoos(single: Single>) { 52 | whenever(bufferooRemote.getBufferoos()) 53 | .thenReturn(single) 54 | } 55 | // 56 | 57 | } -------------------------------------------------------------------------------- /remote/src/test/java/org/buffer/android/boilerplate/remote/BufferooRemoteImplTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote 2 | 3 | import com.nhaarman.mockito_kotlin.mock 4 | import com.nhaarman.mockito_kotlin.whenever 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.data.model.BufferooEntity 7 | import org.buffer.android.boilerplate.remote.mapper.BufferooEntityMapper 8 | import org.buffer.android.boilerplate.remote.test.factory.BufferooFactory 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.junit.runners.JUnit4 13 | 14 | @RunWith(JUnit4::class) 15 | class BufferooRemoteImplTest { 16 | 17 | private lateinit var entityMapper: BufferooEntityMapper 18 | private lateinit var bufferooService: BufferooService 19 | 20 | private lateinit var bufferooRemoteImpl: BufferooRemoteImpl 21 | 22 | @Before 23 | fun setup() { 24 | entityMapper = mock() 25 | bufferooService = mock() 26 | bufferooRemoteImpl = BufferooRemoteImpl(bufferooService, entityMapper) 27 | } 28 | 29 | // 30 | @Test 31 | fun getBufferoosCompletes() { 32 | stubBufferooServiceGetBufferoos(Single.just(BufferooFactory.makeBufferooResponse())) 33 | val testObserver = bufferooRemoteImpl.getBufferoos().test() 34 | testObserver.assertComplete() 35 | } 36 | 37 | @Test 38 | fun getBufferoosReturnsData() { 39 | val bufferooResponse = BufferooFactory.makeBufferooResponse() 40 | stubBufferooServiceGetBufferoos(Single.just(bufferooResponse)) 41 | val bufferooEntities = mutableListOf() 42 | bufferooResponse.team.forEach { 43 | bufferooEntities.add(entityMapper.mapFromRemote(it)) 44 | } 45 | 46 | val testObserver = bufferooRemoteImpl.getBufferoos().test() 47 | testObserver.assertValue(bufferooEntities) 48 | } 49 | // 50 | 51 | private fun stubBufferooServiceGetBufferoos(single: Single) { 52 | whenever(bufferooService.getBufferoos()) 53 | .thenReturn(single) 54 | } 55 | } -------------------------------------------------------------------------------- /data/src/main/java/org/buffer/android/boilerplate/data/BufferooDataRepository.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Single 5 | import org.buffer.android.boilerplate.data.mapper.BufferooMapper 6 | import org.buffer.android.boilerplate.data.model.BufferooEntity 7 | import org.buffer.android.boilerplate.data.source.BufferooDataStoreFactory 8 | import org.buffer.android.boilerplate.data.source.BufferooRemoteDataStore 9 | import org.buffer.android.boilerplate.domain.model.Bufferoo 10 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 11 | import javax.inject.Inject 12 | 13 | /** 14 | * Provides an implementation of the [BufferooRepository] interface for communicating to and from 15 | * data sources 16 | */ 17 | class BufferooDataRepository @Inject constructor(private val factory: BufferooDataStoreFactory, 18 | private val bufferooMapper: BufferooMapper) : 19 | BufferooRepository { 20 | 21 | override fun clearBufferoos(): Completable { 22 | return factory.retrieveCacheDataStore().clearBufferoos() 23 | } 24 | 25 | override fun saveBufferoos(bufferoos: List): Completable { 26 | val bufferooEntities = bufferoos.map { bufferooMapper.mapToEntity(it) } 27 | return saveBufferooEntities(bufferooEntities) 28 | } 29 | 30 | private fun saveBufferooEntities(bufferoos: List): Completable { 31 | return factory.retrieveCacheDataStore().saveBufferoos(bufferoos) 32 | } 33 | 34 | override fun getBufferoos(): Single> { 35 | val dataStore = factory.retrieveDataStore() 36 | return dataStore.getBufferoos() 37 | .flatMap { 38 | if (dataStore is BufferooRemoteDataStore) { 39 | saveBufferooEntities(it).toSingle { it } 40 | } else { 41 | Single.just(it) 42 | } 43 | } 44 | .map { list -> 45 | list.map { listItem -> 46 | bufferooMapper.mapFromEntity(listItem) 47 | } 48 | } 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/injection/module/TestApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.nhaarman.mockito_kotlin.mock 6 | import dagger.Module 7 | import dagger.Provides 8 | import org.buffer.android.boilerplate.cache.PreferencesHelper 9 | import org.buffer.android.boilerplate.data.executor.JobExecutor 10 | import org.buffer.android.boilerplate.data.repository.BufferooCache 11 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 12 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 13 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 14 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 15 | import org.buffer.android.boilerplate.remote.BufferooService 16 | import org.buffer.android.boilerplate.ui.UiThread 17 | import org.buffer.android.boilerplate.ui.injection.scopes.PerApplication 18 | 19 | @Module 20 | class TestApplicationModule { 21 | 22 | @Provides 23 | @PerApplication 24 | fun provideContext(application: Application): Context { 25 | return application 26 | } 27 | 28 | @Provides 29 | @PerApplication 30 | internal fun providePreferencesHelper(): PreferencesHelper { 31 | return mock() 32 | } 33 | 34 | @Provides 35 | @PerApplication 36 | internal fun provideBufferooRepository(): BufferooRepository { 37 | return mock() 38 | } 39 | 40 | @Provides 41 | @PerApplication 42 | internal fun provideBufferooCache(): BufferooCache { 43 | return mock() 44 | } 45 | 46 | @Provides 47 | @PerApplication 48 | internal fun provideBufferooRemote(): BufferooRemote { 49 | return mock() 50 | } 51 | 52 | @Provides 53 | @PerApplication 54 | internal fun provideThreadExecutor(jobExecutor: JobExecutor): ThreadExecutor { 55 | return jobExecutor 56 | } 57 | 58 | @Provides 59 | @PerApplication 60 | internal fun providePostExecutionThread(uiThread: UiThread): PostExecutionThread { 61 | return uiThread 62 | } 63 | 64 | @Provides 65 | @PerApplication 66 | internal fun provideBufferooService(): BufferooService { 67 | return mock() 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /cache/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | def globalConfiguration = rootProject.extensions.getByName("ext") 6 | 7 | compileSdkVersion globalConfiguration["androidCompileSdkVersion"] 8 | buildToolsVersion globalConfiguration["androidBuildToolsVersion"] 9 | 10 | defaultConfig { 11 | minSdkVersion globalConfiguration["androidMinSdkVersion"] 12 | targetSdkVersion globalConfiguration["androidTargetSdkVersion"] 13 | multiDexEnabled = true 14 | } 15 | 16 | dexOptions { 17 | preDexLibraries = false 18 | dexInProcess = false 19 | javaMaxHeapSize "4g" 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_7 24 | targetCompatibility JavaVersion.VERSION_1_7 25 | } 26 | 27 | packagingOptions { 28 | exclude 'LICENSE.txt' 29 | exclude 'META-INF/DEPENDENCIES' 30 | exclude 'META-INF/ASL2.0' 31 | exclude 'META-INF/NOTICE' 32 | exclude 'META-INF/LICENSE' 33 | } 34 | 35 | lintOptions { 36 | quiet true 37 | abortOnError false 38 | ignoreWarnings true 39 | disable 'InvalidPackage' //Some libraries have issues with this. 40 | disable 'OldTargetApi' //Lint gives this warning but SDK 20 would be Android L Beta. 41 | disable 'IconDensities' //For testing purpose. This is safe to remove. 42 | disable 'IconMissingDensityFolder' //For testing purpose. This is safe to remove. 43 | } 44 | 45 | } 46 | 47 | dependencies { 48 | def cacheDependencies = rootProject.ext.cacheDependencies 49 | def cacheTestDependencies = rootProject.ext.cacheTestDependencies 50 | 51 | compileOnly cacheDependencies.javaxAnnotation 52 | 53 | implementation project(':data') 54 | 55 | implementation cacheDependencies.kotlin 56 | implementation cacheDependencies.javaxInject 57 | implementation cacheDependencies.rxKotlin 58 | implementation cacheDependencies.gson 59 | 60 | testImplementation cacheTestDependencies.junit 61 | testImplementation cacheTestDependencies.kotlinJUnit 62 | testImplementation cacheTestDependencies.mockito 63 | testImplementation cacheTestDependencies.assertj 64 | testImplementation cacheTestDependencies.robolectric 65 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/browse/BrowseActivity.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.browse 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import android.support.v7.widget.LinearLayoutManager 6 | import android.view.View 7 | import dagger.android.AndroidInjection 8 | import kotlinx.android.synthetic.main.activity_browse.* 9 | import org.buffer.android.boilerplate.presentation.browse.BrowseBufferoosContract 10 | import org.buffer.android.boilerplate.presentation.model.BufferooView 11 | import org.buffer.android.boilerplate.ui.R 12 | import org.buffer.android.boilerplate.ui.mapper.BufferooMapper 13 | import javax.inject.Inject 14 | 15 | class BrowseActivity: AppCompatActivity(), BrowseBufferoosContract.View { 16 | 17 | @Inject lateinit var onboardingPresenter: BrowseBufferoosContract.Presenter 18 | @Inject lateinit var browseAdapter: BrowseAdapter 19 | @Inject lateinit var mapper: BufferooMapper 20 | 21 | override fun setPresenter(presenter: BrowseBufferoosContract.Presenter) { 22 | onboardingPresenter = presenter 23 | } 24 | 25 | override fun hideProgress() { 26 | progress.visibility = View.VISIBLE 27 | } 28 | 29 | override fun showProgress() { 30 | progress.visibility = View.GONE 31 | } 32 | 33 | override fun showBufferoos(bufferoos: List) { 34 | browseAdapter.bufferoos = bufferoos.map { mapper.mapToViewModel(it) } 35 | browseAdapter.notifyDataSetChanged() 36 | recycler_browse.visibility = View.VISIBLE 37 | } 38 | 39 | override fun hideBufferoos() { 40 | recycler_browse.visibility = View.VISIBLE 41 | } 42 | 43 | override fun showErrorState() { 44 | } 45 | 46 | override fun hideErrorState() { 47 | } 48 | 49 | override fun showEmptyState() { 50 | } 51 | 52 | override fun hideEmptyState() { 53 | } 54 | 55 | override fun onCreate(savedInstanceState: Bundle?) { 56 | super.onCreate(savedInstanceState) 57 | setContentView(R.layout.activity_browse) 58 | AndroidInjection.inject(this) 59 | setupBrowseRecycler() 60 | } 61 | 62 | override fun onStart() { 63 | super.onStart() 64 | onboardingPresenter.start() 65 | } 66 | 67 | private fun setupBrowseRecycler() { 68 | recycler_browse.layoutManager = LinearLayoutManager(this) 69 | recycler_browse.adapter = browseAdapter 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /domain/src/test/java/org/buffer/android/boilerplate/domain/usecase/bufferoo/GetBufferoosTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.usecase.bufferoo 2 | 3 | import com.nhaarman.mockito_kotlin.mock 4 | import com.nhaarman.mockito_kotlin.verify 5 | import com.nhaarman.mockito_kotlin.whenever 6 | import io.reactivex.Single 7 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 8 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 9 | import org.buffer.android.boilerplate.domain.interactor.browse.GetBufferoos 10 | import org.buffer.android.boilerplate.domain.model.Bufferoo 11 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 12 | import org.buffer.android.boilerplate.domain.test.factory.BufferooFactory 13 | import org.junit.Before 14 | import org.junit.Test 15 | 16 | class GetBufferoosTest { 17 | 18 | private lateinit var getBufferoos: GetBufferoos 19 | 20 | private lateinit var mockThreadExecutor: ThreadExecutor 21 | private lateinit var mockPostExecutionThread: PostExecutionThread 22 | private lateinit var mockBufferooRepository: BufferooRepository 23 | 24 | @Before 25 | fun setUp() { 26 | mockThreadExecutor = mock() 27 | mockPostExecutionThread = mock() 28 | mockBufferooRepository = mock() 29 | getBufferoos = GetBufferoos(mockBufferooRepository, mockThreadExecutor, 30 | mockPostExecutionThread) 31 | } 32 | 33 | @Test 34 | fun buildUseCaseObservableCallsRepository() { 35 | getBufferoos.buildUseCaseObservable(null) 36 | verify(mockBufferooRepository).getBufferoos() 37 | } 38 | 39 | @Test 40 | fun buildUseCaseObservableCompletes() { 41 | stubBufferooRepositoryGetBufferoos(Single.just(BufferooFactory.makeBufferooList(2))) 42 | val testObserver = getBufferoos.buildUseCaseObservable(null).test() 43 | testObserver.assertComplete() 44 | } 45 | 46 | @Test 47 | fun buildUseCaseObservableReturnsData() { 48 | val bufferoos = BufferooFactory.makeBufferooList(2) 49 | stubBufferooRepositoryGetBufferoos(Single.just(bufferoos)) 50 | val testObserver = getBufferoos.buildUseCaseObservable(null).test() 51 | testObserver.assertValue(bufferoos) 52 | } 53 | 54 | private fun stubBufferooRepositoryGetBufferoos(single: Single>) { 55 | whenever(mockBufferooRepository.getBufferoos()) 56 | .thenReturn(single) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /remote/src/main/java/org/buffer/android/boilerplate/remote/BufferooServiceFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.remote 2 | 3 | import com.google.gson.FieldNamingPolicy 4 | import com.google.gson.Gson 5 | import com.google.gson.GsonBuilder 6 | import okhttp3.OkHttpClient 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | import retrofit2.Retrofit 9 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | import java.util.concurrent.TimeUnit 12 | 13 | /** 14 | * Provide "make" methods to create instances of [BufferooService] 15 | * and its related dependencies, such as OkHttpClient, Gson, etc. 16 | */ 17 | object BufferooServiceFactory { 18 | 19 | fun makeBuffeoorService(isDebug: Boolean): BufferooService { 20 | val okHttpClient = makeOkHttpClient( 21 | makeLoggingInterceptor(isDebug)) 22 | return makeBufferooService(okHttpClient, makeGson()) 23 | } 24 | 25 | private fun makeBufferooService(okHttpClient: OkHttpClient, gson: Gson): BufferooService { 26 | val retrofit = Retrofit.Builder() 27 | .baseUrl("https://joe-birch-dsdb.squarespace.com/s/") 28 | .client(okHttpClient) 29 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 30 | .addConverterFactory(GsonConverterFactory.create(gson)) 31 | .build() 32 | return retrofit.create(BufferooService::class.java) 33 | } 34 | 35 | private fun makeOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient { 36 | return OkHttpClient.Builder() 37 | .addInterceptor(httpLoggingInterceptor) 38 | .connectTimeout(120, TimeUnit.SECONDS) 39 | .readTimeout(120, TimeUnit.SECONDS) 40 | .build() 41 | } 42 | 43 | private fun makeGson(): Gson { 44 | return GsonBuilder() 45 | .setLenient() 46 | .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") 47 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 48 | .create() 49 | } 50 | 51 | private fun makeLoggingInterceptor(isDebug: Boolean): HttpLoggingInterceptor { 52 | val logging = HttpLoggingInterceptor() 53 | logging.level = if (isDebug) 54 | HttpLoggingInterceptor.Level.BODY 55 | else 56 | HttpLoggingInterceptor.Level.NONE 57 | return logging 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/test/util/RecyclerViewMatcher.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test.util 2 | 3 | import android.content.res.Resources 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.View 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | import org.hamcrest.TypeSafeMatcher 9 | 10 | 11 | open class RecyclerViewMatcher constructor(var recyclerViewId: Int) { 12 | 13 | fun atPosition(position: Int): Matcher { 14 | return atPositionOnView(position, -1) 15 | } 16 | 17 | fun atPositionOnView(position: Int, targetViewId: Int): Matcher { 18 | 19 | return object : TypeSafeMatcher() { 20 | internal var resources: Resources? = null 21 | internal var childView: View? = null 22 | 23 | override fun describeTo(description: Description) { 24 | var idDescription = Integer.toString(recyclerViewId) 25 | if (this.resources != null) { 26 | try { 27 | idDescription = this.resources!!.getResourceName(recyclerViewId) 28 | } catch (var4: Resources.NotFoundException) { 29 | idDescription = String.format("%s (resource name not found)", 30 | *arrayOf(Integer.valueOf(recyclerViewId))) 31 | } 32 | 33 | } 34 | 35 | description.appendText("with id: " + idDescription) 36 | } 37 | 38 | override fun matchesSafely(view: View): Boolean { 39 | 40 | this.resources = view.resources 41 | 42 | if (childView == null) { 43 | val recyclerView = view.rootView.findViewById(recyclerViewId) 44 | as RecyclerView 45 | if (recyclerView != null && recyclerView.id == recyclerViewId) { 46 | childView = recyclerView.findViewHolderForAdapterPosition(position).itemView 47 | } else { 48 | return false 49 | } 50 | } 51 | 52 | if (targetViewId == -1) { 53 | return view === childView 54 | } else { 55 | val targetView = childView?.findViewById(targetViewId) 56 | return view === targetView 57 | } 58 | 59 | } 60 | } 61 | } 62 | 63 | companion object { 64 | 65 | fun withRecyclerView(recyclerViewId: Int): RecyclerViewMatcher { 66 | return RecyclerViewMatcher(recyclerViewId) 67 | } 68 | 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /data/src/test/java/org/buffer/android/boilerplate/data/source/BufferooCacheDataStoreTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.source 2 | 3 | import com.nhaarman.mockito_kotlin.any 4 | import com.nhaarman.mockito_kotlin.mock 5 | import com.nhaarman.mockito_kotlin.whenever 6 | import io.reactivex.Completable 7 | import io.reactivex.Single 8 | import org.buffer.android.boilerplate.data.model.BufferooEntity 9 | import org.buffer.android.boilerplate.data.repository.BufferooCache 10 | import org.buffer.android.boilerplate.data.test.factory.BufferooFactory 11 | import org.junit.Before 12 | import org.junit.Test 13 | import org.junit.runner.RunWith 14 | import org.junit.runners.JUnit4 15 | 16 | @RunWith(JUnit4::class) 17 | class BufferooCacheDataStoreTest { 18 | 19 | private lateinit var bufferooCacheDataStore: BufferooCacheDataStore 20 | 21 | private lateinit var bufferooCache: BufferooCache 22 | 23 | @Before 24 | fun setUp() { 25 | bufferooCache = mock() 26 | bufferooCacheDataStore = BufferooCacheDataStore(bufferooCache) 27 | } 28 | 29 | // 30 | @Test 31 | fun clearBufferoosCompletes() { 32 | stubBufferooCacheClearBufferoos(Completable.complete()) 33 | val testObserver = bufferooCacheDataStore.clearBufferoos().test() 34 | testObserver.assertComplete() 35 | } 36 | // 37 | 38 | // 39 | @Test 40 | fun saveBufferoosCompletes() { 41 | stubBufferooCacheSaveBufferoos(Completable.complete()) 42 | val testObserver = bufferooCacheDataStore.saveBufferoos( 43 | BufferooFactory.makeBufferooEntityList(2)).test() 44 | testObserver.assertComplete() 45 | } 46 | // 47 | 48 | // 49 | @Test 50 | fun getBufferoosCompletes() { 51 | stubBufferooCacheGetBufferoos(Single.just(BufferooFactory.makeBufferooEntityList(2))) 52 | val testObserver = bufferooCacheDataStore.getBufferoos().test() 53 | testObserver.assertComplete() 54 | } 55 | // 56 | 57 | // 58 | private fun stubBufferooCacheSaveBufferoos(completable: Completable) { 59 | whenever(bufferooCache.saveBufferoos(any())) 60 | .thenReturn(completable) 61 | } 62 | 63 | private fun stubBufferooCacheGetBufferoos(single: Single>) { 64 | whenever(bufferooCache.getBufferoos()) 65 | .thenReturn(single) 66 | } 67 | 68 | private fun stubBufferooCacheClearBufferoos(completable: Completable) { 69 | whenever(bufferooCache.clearBufferoos()) 70 | .thenReturn(completable) 71 | } 72 | // 73 | 74 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/browse/BrowseActivityTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.browse 2 | 3 | import android.support.test.espresso.Espresso.onView 4 | import android.support.test.espresso.assertion.ViewAssertions.matches 5 | import android.support.test.espresso.contrib.RecyclerViewActions 6 | import android.support.test.espresso.matcher.ViewMatchers.* 7 | import android.support.test.rule.ActivityTestRule 8 | import android.support.test.runner.AndroidJUnit4 9 | import android.support.v7.widget.RecyclerView 10 | import com.nhaarman.mockito_kotlin.whenever 11 | import io.reactivex.Single 12 | import org.buffer.android.boilerplate.domain.model.Bufferoo 13 | import org.buffer.android.boilerplate.ui.R 14 | import org.buffer.android.boilerplate.ui.test.TestApplication 15 | import org.buffer.android.boilerplate.ui.test.factory.ui.BufferooFactory 16 | import org.buffer.android.boilerplate.ui.test.util.RecyclerViewMatcher 17 | import org.junit.Rule 18 | import org.junit.Test 19 | import org.junit.runner.RunWith 20 | 21 | 22 | @RunWith(AndroidJUnit4::class) 23 | class BrowseActivityTest { 24 | 25 | @Rule @JvmField 26 | val activity = ActivityTestRule(BrowseActivity::class.java, false, false) 27 | 28 | @Test 29 | fun activityLaunches() { 30 | stubBufferooRepositoryGetBufferoos(Single.just(BufferooFactory.makeBufferooList(2))) 31 | activity.launchActivity(null) 32 | } 33 | 34 | @Test 35 | fun bufferoosDisplay() { 36 | val bufferoos = BufferooFactory.makeBufferooList(1) 37 | stubBufferooRepositoryGetBufferoos(Single.just(bufferoos)) 38 | activity.launchActivity(null) 39 | 40 | checkBufferooDetailsDisplay(bufferoos[0], 0) 41 | } 42 | 43 | @Test 44 | fun bufferoosAreScrollable() { 45 | val bufferoos = BufferooFactory.makeBufferooList(20) 46 | stubBufferooRepositoryGetBufferoos(Single.just(bufferoos)) 47 | activity.launchActivity(null) 48 | 49 | bufferoos.forEachIndexed { index, bufferoo -> 50 | onView(withId(R.id.recycler_browse)).perform(RecyclerViewActions. 51 | scrollToPosition(index)) 52 | checkBufferooDetailsDisplay(bufferoo, index) } 53 | } 54 | 55 | private fun checkBufferooDetailsDisplay(bufferoo: Bufferoo, position: Int) { 56 | onView(RecyclerViewMatcher.withRecyclerView(R.id.recycler_browse).atPosition(position)) 57 | .check(matches(hasDescendant(withText(bufferoo.name)))) 58 | onView(RecyclerViewMatcher.withRecyclerView(R.id.recycler_browse).atPosition(position)) 59 | .check(matches(hasDescendant(withText(bufferoo.title)))) 60 | } 61 | 62 | private fun stubBufferooRepositoryGetBufferoos(single: Single>) { 63 | whenever(TestApplication.appComponent().bufferooRepository().getBufferoos()) 64 | .thenReturn(single) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /data/src/test/java/org/buffer/android/boilerplate/data/source/BufferooDataStoreFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data.source 2 | 3 | import com.nhaarman.mockito_kotlin.mock 4 | import com.nhaarman.mockito_kotlin.whenever 5 | import org.buffer.android.boilerplate.data.repository.BufferooCache 6 | import org.junit.Before 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.JUnit4 10 | 11 | @RunWith(JUnit4::class) 12 | class BufferooDataStoreFactoryTest { 13 | 14 | private lateinit var bufferooDataStoreFactory: BufferooDataStoreFactory 15 | 16 | private lateinit var bufferooCache: BufferooCache 17 | private lateinit var bufferooCacheDataStore: BufferooCacheDataStore 18 | private lateinit var bufferooRemoteDataStore: BufferooRemoteDataStore 19 | 20 | @Before 21 | fun setUp() { 22 | bufferooCache = mock() 23 | bufferooCacheDataStore = mock() 24 | bufferooRemoteDataStore = mock() 25 | bufferooDataStoreFactory = BufferooDataStoreFactory(bufferooCache, 26 | bufferooCacheDataStore, bufferooRemoteDataStore) 27 | } 28 | 29 | // 30 | @Test 31 | fun retrieveDataStoreWhenNotCachedReturnsRemoteDataStore() { 32 | stubBufferooCacheIsCached(false) 33 | val bufferooDataStore = bufferooDataStoreFactory.retrieveDataStore() 34 | assert(bufferooDataStore is BufferooRemoteDataStore) 35 | } 36 | 37 | @Test 38 | fun retrieveDataStoreWhenCacheExpiredReturnsRemoteDataStore() { 39 | stubBufferooCacheIsCached(true) 40 | stubBufferooCacheIsExpired(true) 41 | val bufferooDataStore = bufferooDataStoreFactory.retrieveDataStore() 42 | assert(bufferooDataStore is BufferooRemoteDataStore) 43 | } 44 | 45 | @Test 46 | fun retrieveDataStoreReturnsCacheDataStore() { 47 | stubBufferooCacheIsCached(true) 48 | stubBufferooCacheIsExpired(false) 49 | val bufferooDataStore = bufferooDataStoreFactory.retrieveDataStore() 50 | assert(bufferooDataStore is BufferooCacheDataStore) 51 | } 52 | // 53 | 54 | // 55 | @Test 56 | fun retrieveRemoteDataStoreReturnsRemoteDataStore() { 57 | val bufferooDataStore = bufferooDataStoreFactory.retrieveRemoteDataStore() 58 | assert(bufferooDataStore is BufferooRemoteDataStore) 59 | } 60 | // 61 | 62 | // 63 | @Test 64 | fun retrieveCacheDataStoreReturnsCacheDataStore() { 65 | val bufferooDataStore = bufferooDataStoreFactory.retrieveCacheDataStore() 66 | assert(bufferooDataStore is BufferooCacheDataStore) 67 | } 68 | // 69 | 70 | // 71 | private fun stubBufferooCacheIsCached(isCached: Boolean) { 72 | whenever(bufferooCache.isCached()) 73 | .thenReturn(isCached) 74 | } 75 | 76 | private fun stubBufferooCacheIsExpired(isExpired: Boolean) { 77 | whenever(bufferooCache.isExpired()) 78 | .thenReturn(isExpired) 79 | } 80 | // 81 | 82 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import dagger.Module 6 | import dagger.Provides 7 | import org.buffer.android.boilerplate.cache.BufferooCacheImpl 8 | import org.buffer.android.boilerplate.cache.PreferencesHelper 9 | import org.buffer.android.boilerplate.cache.db.DbOpenHelper 10 | import org.buffer.android.boilerplate.cache.mapper.BufferooEntityMapper 11 | import org.buffer.android.boilerplate.data.BufferooDataRepository 12 | import org.buffer.android.boilerplate.data.executor.JobExecutor 13 | import org.buffer.android.boilerplate.data.mapper.BufferooMapper 14 | import org.buffer.android.boilerplate.data.repository.BufferooCache 15 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 16 | import org.buffer.android.boilerplate.data.source.BufferooDataStoreFactory 17 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 18 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 19 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 20 | import org.buffer.android.boilerplate.remote.BufferooRemoteImpl 21 | import org.buffer.android.boilerplate.remote.BufferooService 22 | import org.buffer.android.boilerplate.remote.BufferooServiceFactory 23 | import org.buffer.android.boilerplate.ui.BuildConfig 24 | import org.buffer.android.boilerplate.ui.UiThread 25 | import org.buffer.android.boilerplate.ui.injection.scopes.PerApplication 26 | 27 | /** 28 | * Module used to provide dependencies at an application-level. 29 | */ 30 | @Module 31 | open class ApplicationModule { 32 | 33 | @Provides 34 | @PerApplication 35 | fun provideContext(application: Application): Context { 36 | return application 37 | } 38 | 39 | @Provides 40 | @PerApplication 41 | internal fun providePreferencesHelper(context: Context): PreferencesHelper { 42 | return PreferencesHelper(context) 43 | } 44 | 45 | 46 | @Provides 47 | @PerApplication 48 | internal fun provideBufferooRepository(factory: BufferooDataStoreFactory, 49 | mapper: BufferooMapper): BufferooRepository { 50 | return BufferooDataRepository(factory, mapper) 51 | } 52 | 53 | @Provides 54 | @PerApplication 55 | internal fun provideBufferooCache(factory: DbOpenHelper, 56 | entityMapper: BufferooEntityMapper, 57 | mapper: org.buffer.android.boilerplate.cache.db.mapper.BufferooMapper, 58 | helper: PreferencesHelper): BufferooCache { 59 | return BufferooCacheImpl(factory, entityMapper, mapper, helper) 60 | } 61 | 62 | @Provides 63 | @PerApplication 64 | internal fun provideBufferooRemote(service: BufferooService, 65 | factory: org.buffer.android.boilerplate.remote.mapper.BufferooEntityMapper): BufferooRemote { 66 | return BufferooRemoteImpl(service, factory) 67 | } 68 | 69 | @Provides 70 | @PerApplication 71 | internal fun provideThreadExecutor(jobExecutor: JobExecutor): ThreadExecutor { 72 | return jobExecutor 73 | } 74 | 75 | @Provides 76 | @PerApplication 77 | internal fun providePostExecutionThread(uiThread: UiThread): PostExecutionThread { 78 | return uiThread 79 | } 80 | 81 | @Provides 82 | @PerApplication 83 | internal fun provideBufferooService(): BufferooService { 84 | return BufferooServiceFactory.makeBuffeoorService(BuildConfig.DEBUG) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cache/src/test/java/org/buffer/android/boilerplate/cache/BufferooCacheImplTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache 2 | 3 | import org.buffer.android.boilerplate.cache.db.Db 4 | import org.buffer.android.boilerplate.cache.db.DbOpenHelper 5 | import org.buffer.android.boilerplate.cache.db.mapper.BufferooMapper 6 | import org.buffer.android.boilerplate.cache.mapper.BufferooEntityMapper 7 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 8 | import org.buffer.android.boilerplate.cache.test.factory.BufferooFactory 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.robolectric.RobolectricTestRunner 13 | import org.robolectric.RuntimeEnvironment 14 | import org.robolectric.annotation.Config 15 | import kotlin.test.assertEquals 16 | 17 | @RunWith(RobolectricTestRunner::class) 18 | @Config(sdk = intArrayOf(21)) 19 | class BufferooCacheImplTest { 20 | 21 | private var entityMapper = BufferooEntityMapper() 22 | private var mapper = BufferooMapper() 23 | private var preferencesHelper = PreferencesHelper(RuntimeEnvironment.application) 24 | 25 | private val databaseHelper = BufferooCacheImpl(DbOpenHelper(RuntimeEnvironment.application), 26 | entityMapper, mapper, preferencesHelper) 27 | 28 | @Before 29 | fun setup() { 30 | databaseHelper.getDatabase().rawQuery("DELETE FROM " + Db.BufferooTable.TABLE_NAME, null) 31 | } 32 | 33 | @Test 34 | fun clearTablesCompletes() { 35 | val testObserver = databaseHelper.clearBufferoos().test() 36 | testObserver.assertComplete() 37 | } 38 | 39 | // 40 | @Test 41 | fun saveBufferoosCompletes() { 42 | val bufferooEntities = BufferooFactory.makeBufferooEntityList(2) 43 | 44 | val testObserver = databaseHelper.saveBufferoos(bufferooEntities).test() 45 | testObserver.assertComplete() 46 | } 47 | 48 | @Test 49 | fun saveBufferoosSavesData() { 50 | val bufferooCount = 2 51 | val bufferooEntities = BufferooFactory.makeBufferooEntityList(bufferooCount) 52 | 53 | databaseHelper.saveBufferoos(bufferooEntities).test() 54 | checkNumRowsInBufferoosTable(bufferooCount) 55 | } 56 | // 57 | 58 | // 59 | @Test 60 | fun getBufferoosCompletes() { 61 | val testObserver = databaseHelper.getBufferoos().test() 62 | testObserver.assertComplete() 63 | } 64 | 65 | @Test 66 | fun getBufferoosReturnsData() { 67 | val bufferooEntities = BufferooFactory.makeBufferooEntityList(2) 68 | val cachedBufferoos = mutableListOf() 69 | bufferooEntities.forEach { 70 | cachedBufferoos.add(entityMapper.mapToCached(it)) 71 | } 72 | insertBufferoos(cachedBufferoos) 73 | 74 | val testObserver = databaseHelper.getBufferoos().test() 75 | testObserver.assertValue(bufferooEntities) 76 | } 77 | // 78 | 79 | private fun insertBufferoos(cachedBufferoos: List) { 80 | val database = databaseHelper.getDatabase() 81 | cachedBufferoos.forEach { 82 | database.insertOrThrow(Db.BufferooTable.TABLE_NAME, null, 83 | mapper.toContentValues(it)) 84 | } 85 | } 86 | 87 | private fun checkNumRowsInBufferoosTable(expectedRows: Int) { 88 | val bufferoosCursor = databaseHelper.getDatabase().query(Db.BufferooTable.TABLE_NAME, 89 | null, null, null, null, null, null) 90 | bufferoosCursor.moveToFirst() 91 | val numberOfRows = bufferoosCursor.count 92 | bufferoosCursor.close() 93 | assertEquals(expectedRows, numberOfRows) 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 22 | 27 | 32 | 37 | 42 | 47 | 52 | 57 | 62 | 67 | 72 | 77 | 82 | 87 | 92 | 97 | 102 | 107 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /presentation/src/test/java/org/buffer/android/boilerplate/presentation/browse/BrowseBufferoosPresenterTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.presentation.browse 2 | 3 | import com.nhaarman.mockito_kotlin.* 4 | import io.reactivex.observers.DisposableSingleObserver 5 | import org.buffer.android.boilerplate.domain.interactor.browse.GetBufferoos 6 | import org.buffer.android.boilerplate.domain.model.Bufferoo 7 | import org.buffer.android.boilerplate.presentation.mapper.BufferooMapper 8 | import org.buffer.android.boilerplate.presentation.test.factory.BufferooFactory 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.junit.runners.JUnit4 13 | 14 | @RunWith(JUnit4::class) 15 | class BrowseBufferoosPresenterTest { 16 | 17 | private lateinit var mockBrowseBufferoosView: BrowseBufferoosContract.View 18 | private lateinit var mockBufferooMapper: BufferooMapper 19 | private lateinit var mockGetBufferoos: GetBufferoos 20 | 21 | private lateinit var browseBufferoosPresenter: BrowseBufferoosPresenter 22 | private lateinit var captor: KArgumentCaptor>> 23 | 24 | @Before 25 | fun setup() { 26 | captor = argumentCaptor>>() 27 | mockBrowseBufferoosView = mock() 28 | mockBufferooMapper = mock() 29 | mockGetBufferoos = mock() 30 | browseBufferoosPresenter = BrowseBufferoosPresenter(mockBrowseBufferoosView, 31 | mockGetBufferoos, mockBufferooMapper) 32 | } 33 | 34 | // 35 | @Test 36 | fun retrieveBufferoosHidesErrorState() { 37 | browseBufferoosPresenter.retrieveBufferoos() 38 | 39 | verify(mockGetBufferoos).execute(captor.capture(), eq(null)) 40 | captor.firstValue.onSuccess(BufferooFactory.makeBufferooList(2)) 41 | verify(mockBrowseBufferoosView).hideErrorState() 42 | } 43 | 44 | @Test 45 | fun retrieveBufferoosHidesEmptyState() { 46 | browseBufferoosPresenter.retrieveBufferoos() 47 | 48 | verify(mockGetBufferoos).execute(captor.capture(), eq(null)) 49 | captor.firstValue.onSuccess(BufferooFactory.makeBufferooList(2)) 50 | verify(mockBrowseBufferoosView).hideEmptyState() 51 | } 52 | 53 | @Test 54 | fun retrieveBufferoosShowsBufferoos() { 55 | val bufferoos = BufferooFactory.makeBufferooList(2) 56 | browseBufferoosPresenter.retrieveBufferoos() 57 | 58 | verify(mockGetBufferoos).execute(captor.capture(), eq(null)) 59 | captor.firstValue.onSuccess(bufferoos) 60 | verify(mockBrowseBufferoosView).showBufferoos( 61 | bufferoos.map { mockBufferooMapper.mapToView(it) }) 62 | } 63 | 64 | @Test 65 | fun retrieveBufferoosShowsEmptyState() { 66 | browseBufferoosPresenter.retrieveBufferoos() 67 | 68 | verify(mockGetBufferoos).execute(captor.capture(), eq(null)) 69 | captor.firstValue.onSuccess(BufferooFactory.makeBufferooList(0)) 70 | verify(mockBrowseBufferoosView).showEmptyState() 71 | } 72 | 73 | @Test 74 | fun retrieveBufferoosHidesBufferoos() { 75 | browseBufferoosPresenter.retrieveBufferoos() 76 | 77 | verify(mockGetBufferoos).execute(captor.capture(), eq(null)) 78 | captor.firstValue.onSuccess(BufferooFactory.makeBufferooList(0)) 79 | verify(mockBrowseBufferoosView).hideBufferoos() 80 | } 81 | 82 | @Test 83 | fun retrieveBufferoosShowsErrorState() { 84 | browseBufferoosPresenter.retrieveBufferoos() 85 | 86 | verify(mockGetBufferoos).execute(captor.capture(), eq(null)) 87 | captor.firstValue.onError(RuntimeException()) 88 | verify(mockBrowseBufferoosView).showErrorState() 89 | } 90 | 91 | @Test 92 | fun retrieveBufferoosHidesBufferoosWhenErrorThrown() { 93 | browseBufferoosPresenter.retrieveBufferoos() 94 | 95 | verify(mockGetBufferoos).execute(captor.capture(), eq(null)) 96 | captor.firstValue.onError(RuntimeException()) 97 | verify(mockBrowseBufferoosView).hideBufferoos() 98 | } 99 | 100 | @Test 101 | fun retrieveBufferoosHidesEmptyStateWhenErrorThrown() { 102 | browseBufferoosPresenter.retrieveBufferoos() 103 | 104 | verify(mockGetBufferoos).execute(captor.capture(), eq(null)) 105 | captor.firstValue.onError(RuntimeException()) 106 | verify(mockBrowseBufferoosView).hideEmptyState() 107 | } 108 | // 109 | 110 | } -------------------------------------------------------------------------------- /mobile-ui/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: 'jacoco-android' 6 | 7 | android { 8 | def globalConfiguration = rootProject.extensions.getByName("ext") 9 | 10 | compileSdkVersion globalConfiguration["androidCompileSdkVersion"] 11 | buildToolsVersion globalConfiguration["androidBuildToolsVersion"] 12 | 13 | defaultConfig { 14 | minSdkVersion globalConfiguration["androidMinSdkVersion"] 15 | targetSdkVersion globalConfiguration["androidTargetSdkVersion"] 16 | multiDexEnabled = true 17 | testInstrumentationRunner "org.buffer.android.boilerplate.ui.test.TestRunner" 18 | } 19 | 20 | buildTypes { 21 | debug { 22 | testCoverageEnabled true 23 | } 24 | } 25 | 26 | dexOptions { 27 | preDexLibraries = false 28 | dexInProcess = false 29 | javaMaxHeapSize "4g" 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | packagingOptions { 38 | exclude 'LICENSE.txt' 39 | exclude 'META-INF/DEPENDENCIES' 40 | exclude 'META-INF/ASL2.0' 41 | exclude 'META-INF/NOTICE' 42 | exclude 'META-INF/LICENSE' 43 | } 44 | 45 | lintOptions { 46 | quiet true 47 | abortOnError false 48 | ignoreWarnings true 49 | } 50 | 51 | } 52 | 53 | kapt { 54 | correctErrorTypes = true 55 | } 56 | 57 | configurations.all { 58 | resolutionStrategy { 59 | force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 60 | } 61 | } 62 | 63 | dependencies { 64 | def mobileUiDependencies = rootProject.ext.mobileUiDependencies 65 | def mobileUiTestDependencies = rootProject.ext.mobileUiTestDependencies 66 | 67 | implementation project(':presentation') 68 | implementation project(':data') 69 | implementation project(':cache') 70 | implementation project(':remote') 71 | 72 | implementation mobileUiDependencies.javaxAnnotation 73 | 74 | implementation mobileUiDependencies.kotlin 75 | implementation mobileUiDependencies.javaxInject 76 | implementation mobileUiDependencies.rxKotlin 77 | implementation mobileUiDependencies.androidAnnotations 78 | implementation mobileUiDependencies.androidSupportV4 79 | implementation mobileUiDependencies.androidSupportV13 80 | implementation mobileUiDependencies.appCompatV7 81 | implementation mobileUiDependencies.supportRecyclerView 82 | implementation mobileUiDependencies.supportDesign 83 | implementation mobileUiDependencies.timber 84 | implementation mobileUiDependencies.rxAndroid 85 | implementation mobileUiDependencies.glide 86 | implementation mobileUiDependencies.dagger 87 | implementation mobileUiDependencies.daggerSupport 88 | 89 | testImplementation mobileUiTestDependencies.kotlinJUnit 90 | 91 | kapt mobileUiDependencies.daggerCompiler 92 | kapt mobileUiDependencies.daggerProcessor 93 | compileOnly mobileUiDependencies.glassfishAnnotation 94 | 95 | // Instrumentation test dependencies 96 | androidTestImplementation mobileUiTestDependencies.junit 97 | androidTestImplementation mobileUiTestDependencies.mockito 98 | androidTestImplementation mobileUiTestDependencies.mockitoAndroid 99 | androidTestImplementation (mobileUiTestDependencies.espressoCore) { 100 | exclude group: 'com.android.support', module: 'support-annotations' 101 | } 102 | androidTestImplementation (mobileUiTestDependencies.androidRunner) { 103 | exclude group: 'com.android.support', module: 'support-annotations' 104 | } 105 | androidTestImplementation (mobileUiTestDependencies.androidRules) { 106 | exclude group: 'com.android.support', module: 'support-annotations' 107 | } 108 | androidTestImplementation (mobileUiTestDependencies.espressoIntents) { 109 | exclude group: 'com.android.support', module: 'support-annotations' 110 | } 111 | androidTestImplementation(mobileUiTestDependencies.espressoContrib) { 112 | exclude module: 'appcompat' 113 | exclude module: 'appcompat-v7' 114 | exclude module: 'support-v4' 115 | exclude module: 'support-v13' 116 | exclude module: 'support-annotations' 117 | exclude module: 'recyclerview-v7' 118 | exclude module: 'design' 119 | } 120 | 121 | kaptTest mobileUiDependencies.daggerCompiler 122 | kaptAndroidTest mobileUiDependencies.daggerCompiler 123 | } 124 | -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/BufferooCacheImpl.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache 2 | 3 | import android.database.sqlite.SQLiteDatabase 4 | import io.reactivex.Completable 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.cache.db.Db 7 | import org.buffer.android.boilerplate.cache.db.DbOpenHelper 8 | import org.buffer.android.boilerplate.cache.db.constants.BufferooConstants 9 | import org.buffer.android.boilerplate.cache.db.mapper.BufferooMapper 10 | import org.buffer.android.boilerplate.cache.mapper.BufferooEntityMapper 11 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 12 | import org.buffer.android.boilerplate.data.model.BufferooEntity 13 | import org.buffer.android.boilerplate.data.repository.BufferooCache 14 | import javax.inject.Inject 15 | 16 | /** 17 | * Cached implementation for retrieving and saving Bufferoo instances. This class implements the 18 | * [BufferooCache] from the Data layer as it is that layers responsibility for defining the 19 | * operations in which data store implementation layers can carry out. 20 | */ 21 | class BufferooCacheImpl @Inject constructor(dbOpenHelper: DbOpenHelper, 22 | private val entityMapper: BufferooEntityMapper, 23 | private val mapper: BufferooMapper, 24 | private val preferencesHelper: PreferencesHelper): 25 | BufferooCache { 26 | 27 | private val EXPIRATION_TIME = (60 * 10 * 1000).toLong() 28 | 29 | private var database: SQLiteDatabase = dbOpenHelper.writableDatabase 30 | 31 | /** 32 | * Retrieve an instance from the database, used for tests 33 | */ 34 | internal fun getDatabase(): SQLiteDatabase { 35 | return database 36 | } 37 | 38 | /** 39 | * Remove all the data from all the tables in the database. 40 | */ 41 | override fun clearBufferoos(): Completable { 42 | return Completable.defer { 43 | database.beginTransaction() 44 | try { 45 | database.delete(Db.BufferooTable.TABLE_NAME, null, null) 46 | database.setTransactionSuccessful() 47 | } finally { 48 | database.endTransaction() 49 | } 50 | Completable.complete() 51 | } 52 | } 53 | 54 | /** 55 | * Save the given list of [BufferooEntity] instances to the database. 56 | */ 57 | override fun saveBufferoos(bufferoos: List): Completable { 58 | return Completable.defer { 59 | database.beginTransaction() 60 | try { 61 | bufferoos.forEach { 62 | saveBufferoo(entityMapper.mapToCached(it)) 63 | } 64 | database.setTransactionSuccessful() 65 | } finally { 66 | database.endTransaction() 67 | } 68 | Completable.complete() 69 | } 70 | } 71 | 72 | /** 73 | * Retrieve a list of [BufferooEntity] instances from the database. 74 | */ 75 | override fun getBufferoos(): Single> { 76 | return Single.defer> { 77 | val updatesCursor = database.rawQuery(BufferooConstants.QUERY_GET_ALL_BUFFEROOS, null) 78 | val bufferoos = mutableListOf() 79 | 80 | while (updatesCursor.moveToNext()) { 81 | val cachedBufferoo = mapper.parseCursor(updatesCursor) 82 | bufferoos.add(entityMapper.mapFromCached(cachedBufferoo)) 83 | } 84 | 85 | updatesCursor.close() 86 | Single.just>(bufferoos) 87 | } 88 | } 89 | 90 | /** 91 | * Helper method for saving a [CachedBufferoo] instance to the database. 92 | */ 93 | private fun saveBufferoo(cachedBufferoo: CachedBufferoo) { 94 | database.insert(Db.BufferooTable.TABLE_NAME, null, mapper.toContentValues(cachedBufferoo)) 95 | } 96 | 97 | /** 98 | * Checked whether there are instances of [CachedBufferoo] stored in the cache 99 | */ 100 | override fun isCached(): Boolean { 101 | return database.rawQuery(BufferooConstants.QUERY_GET_ALL_BUFFEROOS, null).count > 0 102 | } 103 | 104 | /** 105 | * Set a point in time at when the cache was last updated 106 | */ 107 | override fun setLastCacheTime(lastCache: Long) { 108 | preferencesHelper.lastCacheTime = lastCache 109 | } 110 | 111 | /** 112 | * Check whether the current cached data exceeds the defined [EXPIRATION_TIME] time 113 | */ 114 | override fun isExpired(): Boolean { 115 | val currentTime = System.currentTimeMillis() 116 | val lastUpdateTime = this.getLastCacheUpdateTimeMillis() 117 | return currentTime - lastUpdateTime > EXPIRATION_TIME 118 | } 119 | 120 | /** 121 | * Get in millis, the last time the cache was accessed. 122 | */ 123 | private fun getLastCacheUpdateTimeMillis(): Long { 124 | return preferencesHelper.lastCacheTime 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /data/src/test/java/org/buffer/android/boilerplate/data/BufferooDataRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data 2 | 3 | import com.nhaarman.mockito_kotlin.* 4 | import io.reactivex.Completable 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.data.mapper.BufferooMapper 7 | import org.buffer.android.boilerplate.data.model.BufferooEntity 8 | import org.buffer.android.boilerplate.data.repository.BufferooDataStore 9 | import org.buffer.android.boilerplate.data.source.BufferooCacheDataStore 10 | import org.buffer.android.boilerplate.data.source.BufferooDataStoreFactory 11 | import org.buffer.android.boilerplate.data.source.BufferooRemoteDataStore 12 | import org.buffer.android.boilerplate.data.test.factory.BufferooFactory 13 | import org.buffer.android.boilerplate.domain.model.Bufferoo 14 | import org.junit.Before 15 | import org.junit.Test 16 | import org.junit.runner.RunWith 17 | import org.junit.runners.JUnit4 18 | 19 | @RunWith(JUnit4::class) 20 | class BufferooDataRepositoryTest { 21 | 22 | private lateinit var bufferooDataRepository: BufferooDataRepository 23 | 24 | private lateinit var bufferooDataStoreFactory: BufferooDataStoreFactory 25 | private lateinit var bufferooMapper: BufferooMapper 26 | private lateinit var bufferooCacheDataStore: BufferooCacheDataStore 27 | private lateinit var bufferooRemoteDataStore: BufferooRemoteDataStore 28 | 29 | @Before 30 | fun setUp() { 31 | bufferooDataStoreFactory = mock() 32 | bufferooMapper = mock() 33 | bufferooCacheDataStore = mock() 34 | bufferooRemoteDataStore = mock() 35 | bufferooDataRepository = BufferooDataRepository(bufferooDataStoreFactory, bufferooMapper) 36 | stubBufferooDataStoreFactoryRetrieveCacheDataStore() 37 | stubBufferooDataStoreFactoryRetrieveRemoteDataStore() 38 | } 39 | 40 | // 41 | @Test 42 | fun clearBufferoosCompletes() { 43 | stubBufferooCacheClearBufferoos(Completable.complete()) 44 | val testObserver = bufferooDataRepository.clearBufferoos().test() 45 | testObserver.assertComplete() 46 | } 47 | 48 | @Test 49 | fun clearBufferoosCallsCacheDataStore() { 50 | stubBufferooCacheClearBufferoos(Completable.complete()) 51 | bufferooDataRepository.clearBufferoos().test() 52 | verify(bufferooCacheDataStore).clearBufferoos() 53 | } 54 | 55 | @Test 56 | fun clearBufferoosNeverCallsRemoteDataStore() { 57 | stubBufferooCacheClearBufferoos(Completable.complete()) 58 | bufferooDataRepository.clearBufferoos().test() 59 | verify(bufferooRemoteDataStore, never()).clearBufferoos() 60 | } 61 | // 62 | 63 | // 64 | @Test 65 | fun saveBufferoosCompletes() { 66 | stubBufferooCacheSaveBufferoos(Completable.complete()) 67 | val testObserver = bufferooDataRepository.saveBufferoos( 68 | BufferooFactory.makeBufferooList(2)).test() 69 | testObserver.assertComplete() 70 | } 71 | 72 | @Test 73 | fun saveBufferoosCallsCacheDataStore() { 74 | stubBufferooCacheSaveBufferoos(Completable.complete()) 75 | bufferooDataRepository.saveBufferoos(BufferooFactory.makeBufferooList(2)).test() 76 | verify(bufferooCacheDataStore).saveBufferoos(any()) 77 | } 78 | 79 | @Test 80 | fun saveBufferoosNeverCallsRemoteDataStore() { 81 | stubBufferooCacheSaveBufferoos(Completable.complete()) 82 | bufferooDataRepository.saveBufferoos(BufferooFactory.makeBufferooList(2)).test() 83 | verify(bufferooRemoteDataStore, never()).saveBufferoos(any()) 84 | } 85 | // 86 | 87 | // 88 | @Test 89 | fun getBufferoosCompletes() { 90 | stubBufferooDataStoreFactoryRetrieveDataStore(bufferooCacheDataStore) 91 | stubBufferooCacheDataStoreGetBufferoos(Single.just( 92 | BufferooFactory.makeBufferooEntityList(2))) 93 | val testObserver = bufferooDataRepository.getBufferoos().test() 94 | testObserver.assertComplete() 95 | } 96 | 97 | @Test 98 | fun getBufferoosReturnsData() { 99 | stubBufferooDataStoreFactoryRetrieveDataStore(bufferooCacheDataStore) 100 | val bufferoos = BufferooFactory.makeBufferooList(2) 101 | val bufferooEntities = BufferooFactory.makeBufferooEntityList(2) 102 | bufferoos.forEachIndexed { index, bufferoo -> 103 | stubBufferooMapperMapFromEntity(bufferooEntities[index], bufferoo) } 104 | stubBufferooCacheDataStoreGetBufferoos(Single.just(bufferooEntities)) 105 | 106 | val testObserver = bufferooDataRepository.getBufferoos().test() 107 | testObserver.assertValue(bufferoos) 108 | } 109 | 110 | @Test 111 | fun getBufferoosSavesBufferoosWhenFromCacheDataStore() { 112 | stubBufferooDataStoreFactoryRetrieveDataStore(bufferooCacheDataStore) 113 | stubBufferooCacheSaveBufferoos(Completable.complete()) 114 | bufferooDataRepository.saveBufferoos(BufferooFactory.makeBufferooList(2)).test() 115 | verify(bufferooCacheDataStore).saveBufferoos(any()) 116 | } 117 | 118 | @Test 119 | fun getBufferoosNeverSavesBufferoosWhenFromRemoteDataStore() { 120 | stubBufferooDataStoreFactoryRetrieveDataStore(bufferooRemoteDataStore) 121 | stubBufferooCacheSaveBufferoos(Completable.complete()) 122 | bufferooDataRepository.saveBufferoos(BufferooFactory.makeBufferooList(2)).test() 123 | verify(bufferooRemoteDataStore, never()).saveBufferoos(any()) 124 | } 125 | // 126 | 127 | // 128 | private fun stubBufferooCacheSaveBufferoos(completable: Completable) { 129 | whenever(bufferooCacheDataStore.saveBufferoos(any())) 130 | .thenReturn(completable) 131 | } 132 | 133 | private fun stubBufferooCacheDataStoreGetBufferoos(single: Single>) { 134 | whenever(bufferooCacheDataStore.getBufferoos()) 135 | .thenReturn(single) 136 | } 137 | 138 | private fun stubBufferooRemoteDataStoreGetBufferoos(single: Single>) { 139 | whenever(bufferooRemoteDataStore.getBufferoos()) 140 | .thenReturn(single) 141 | } 142 | 143 | private fun stubBufferooCacheClearBufferoos(completable: Completable) { 144 | whenever(bufferooCacheDataStore.clearBufferoos()) 145 | .thenReturn(completable) 146 | } 147 | 148 | private fun stubBufferooDataStoreFactoryRetrieveCacheDataStore() { 149 | whenever(bufferooDataStoreFactory.retrieveCacheDataStore()) 150 | .thenReturn(bufferooCacheDataStore) 151 | } 152 | 153 | private fun stubBufferooDataStoreFactoryRetrieveRemoteDataStore() { 154 | whenever(bufferooDataStoreFactory.retrieveRemoteDataStore()) 155 | .thenReturn(bufferooCacheDataStore) 156 | } 157 | 158 | private fun stubBufferooDataStoreFactoryRetrieveDataStore(dataStore: BufferooDataStore) { 159 | whenever(bufferooDataStoreFactory.retrieveDataStore()) 160 | .thenReturn(dataStore) 161 | } 162 | 163 | private fun stubBufferooMapperMapFromEntity(bufferooEntity: BufferooEntity, 164 | bufferoo: Bufferoo) { 165 | whenever(bufferooMapper.mapFromEntity(bufferooEntity)) 166 | .thenReturn(bufferoo) 167 | } 168 | // 169 | 170 | } -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | 2 | allprojects { 3 | repositories { 4 | jcenter() 5 | } 6 | } 7 | 8 | ext { 9 | //Android 10 | androidBuildToolsVersion = "26.0.0" 11 | androidMinSdkVersion = 15 12 | androidTargetSdkVersion = 26 13 | androidCompileSdkVersion = 26 14 | 15 | //Libraries 16 | kotlinVersion = '1.1.3-2' 17 | butterKnifeVersion = '7.0.1' 18 | recyclerViewVersion = '21.0.3' 19 | rxJavaVersion = '2.0.2' 20 | rxKotlinVersion = '2.1.0' 21 | rxAndroidVersion = '2.0.1' 22 | javaxAnnotationVersion = '1.0' 23 | javaxInjectVersion = '1' 24 | gsonVersion = '2.8.1' 25 | okHttpVersion = '3.8.1' 26 | androidAnnotationsVersion = '21.0.3' 27 | retrofitVersion = '2.3.0' 28 | roomVersion = '1.0.0-alpha6' 29 | supportLibraryVersion = '26.0.1' 30 | timberVersion = '4.5.1' 31 | glideVersion = '4.0.0' 32 | daggerVersion = '2.11' 33 | glassfishAnnotationVersion = '10.0-b28' 34 | 35 | //Testing 36 | robolectricVersion = '3.4.2' 37 | jUnitVersion = '4.12' 38 | assertJVersion = '3.8.0' 39 | mockitoVersion = '1.9.5' 40 | dexmakerVersion = '1.0' 41 | espressoVersion = '3.0.0' 42 | testingSupportLibVersion = '0.1' 43 | mockitoKotlinVersion = '1.5.0' 44 | mockitoAndroidVersion = '2.8.47' 45 | androidSupportRunnerVersion = '1.0.0' 46 | androidSupportRulesVersion = '1.0.0' 47 | dexmakerMockitoversion = '2.2.0' 48 | runnerVersion = '0.5' 49 | 50 | domainDependencies = [ 51 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 52 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 53 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 54 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}" 55 | ] 56 | 57 | domainTestDependencies = [ 58 | junit: "junit:junit:${jUnitVersion}", 59 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 60 | assertj: "org.assertj:assertj-core:${assertJVersion}" 61 | ] 62 | 63 | presentationDependencies = [ 64 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 65 | dagger: "com.google.dagger:dagger:${daggerVersion}", 66 | okHttp: "com.squareup.okhttp3:okhttp:${okHttpVersion}", 67 | okHttpLogger: "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}", 68 | gson: "com.google.code.gson:gson:${gsonVersion}", 69 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 70 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 71 | rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", 72 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 73 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 74 | androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", 75 | retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}", 76 | retrofitConverter: "com.squareup.retrofit2:converter-gson:${retrofitVersion}", 77 | retrofitAdapter: "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" 78 | ] 79 | 80 | presentationTestDependencies = [ 81 | junit: "junit:junit:${jUnitVersion}", 82 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 83 | assertj: "org.assertj:assertj-core:${assertJVersion}", 84 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 85 | robolectric: "org.robolectric:robolectric:${robolectricVersion}" 86 | ] 87 | 88 | dataDependencies = [ 89 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 90 | dagger: "com.google.dagger:dagger:${daggerVersion}", 91 | okHttp: "com.squareup.okhttp3:okhttp:${okHttpVersion}", 92 | okHttpLogger: "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}", 93 | gson: "com.google.code.gson:gson:${gsonVersion}", 94 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 95 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 96 | rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", 97 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 98 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 99 | androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", 100 | retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}", 101 | retrofitConverter: "com.squareup.retrofit2:converter-gson:${retrofitVersion}", 102 | retrofitAdapter: "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" 103 | ] 104 | 105 | dataTestDependencies = [ 106 | junit: "junit:junit:${jUnitVersion}", 107 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 108 | assertj: "org.assertj:assertj-core:${assertJVersion}", 109 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 110 | robolectric: "org.robolectric:robolectric:${robolectricVersion}" 111 | ] 112 | 113 | cacheDependencies = [ 114 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 115 | dagger: "com.google.dagger:dagger:${daggerVersion}", 116 | gson: "com.google.code.gson:gson:${gsonVersion}", 117 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 118 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 119 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 120 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 121 | androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", 122 | roomRuntime: "android.arch.persistence.room:runtime:${roomVersion}", 123 | roomCompiler: "android.arch.persistence.room:compiler:${roomVersion}", 124 | roomRxJava: "android.arch.persistence.room:rxjava2:${roomVersion}", 125 | ] 126 | 127 | cacheTestDependencies = [ 128 | junit: "junit:junit:${jUnitVersion}", 129 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 130 | assertj: "org.assertj:assertj-core:${assertJVersion}", 131 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 132 | robolectric: "org.robolectric:robolectric:${robolectricVersion}", 133 | roomTesting: "android.arch.persistence.room:testing:${roomVersion}", 134 | archTesting: "android.arch.core:core-testing:${roomVersion}", 135 | supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", 136 | supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}" 137 | ] 138 | 139 | remoteDependencies = [ 140 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 141 | dagger: "com.google.dagger:dagger:${daggerVersion}", 142 | gson: "com.google.code.gson:gson:${gsonVersion}", 143 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 144 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 145 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 146 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 147 | androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", 148 | okHttp: "com.squareup.okhttp3:okhttp:${okHttpVersion}", 149 | okHttpLogger: "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}", 150 | retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}", 151 | retrofitConverter: "com.squareup.retrofit2:converter-gson:${retrofitVersion}", 152 | retrofitAdapter: "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" 153 | ] 154 | 155 | remoteTestDependencies = [ 156 | junit: "junit:junit:${jUnitVersion}", 157 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 158 | assertj: "org.assertj:assertj-core:${assertJVersion}", 159 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 160 | supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", 161 | supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}" 162 | ] 163 | 164 | mobileUiDependencies = [ 165 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 166 | dagger: "com.google.dagger:dagger:${daggerVersion}", 167 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 168 | rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", 169 | glide: "com.github.bumptech.glide:glide:${glideVersion}", 170 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 171 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 172 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 173 | androidAnnotations: "com.android.support:support-annotations:${supportLibraryVersion}", 174 | androidSupportV4: "com.android.support:support-v4:${supportLibraryVersion}", 175 | androidSupportV13: "com.android.support:support-v13:${supportLibraryVersion}", 176 | appCompatV7: "com.android.support:appcompat-v7:${supportLibraryVersion}", 177 | supportRecyclerView:"com.android.support:recyclerview-v7:${supportLibraryVersion}", 178 | supportDesign: "com.android.support:design:${supportLibraryVersion}", 179 | timber: "com.jakewharton.timber:timber:${timberVersion}", 180 | daggerSupport: "com.google.dagger:dagger-android-support:${daggerVersion}", 181 | daggerProcessor: "com.google.dagger:dagger-android-processor:${daggerVersion}", 182 | glassfishAnnotation: "org.glassfish:javax.annotation:${glassfishAnnotationVersion}" 183 | ] 184 | 185 | mobileUiTestDependencies = [ 186 | junit: "junit:junit:${jUnitVersion}", 187 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 188 | assertj: "org.assertj:assertj-core:${assertJVersion}", 189 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 190 | supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", 191 | supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}", 192 | mockitoAndroid: "org.mockito:mockito-android:${mockitoAndroidVersion}", 193 | espressoCore: "com.android.support.test.espresso:espresso-core:${espressoVersion}", 194 | espressoIntents: "com.android.support.test.espresso:espresso-intents:${espressoVersion}", 195 | espressoContrib: "com.android.support.test.espresso:espresso-contrib:${espressoVersion}", 196 | androidRunner: "com.android.support.test:runner:${runnerVersion}", 197 | androidRules: "com.android.support.test:rules:${runnerVersion}" 198 | ] 199 | 200 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/bufferapp/android-clean-architecture-boilerplate.svg?branch=master)](https://travis-ci.org/bufferapp/android-clean-architecture-boilerplate) [![codecov](https://codecov.io/gh/bufferapp/android-clean-architecture-boilerplate/branch/master/graph/badge.svg)](https://codecov.io/gh/bufferapp/android-clean-architecture-boilerplate) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/278fa00f492d48a288ab64188d15fb61)](https://www.codacy.com/app/hitherejoe/android-clean-architecture-boilerplate?utm_source=github.com&utm_medium=referral&utm_content=bufferapp/android-clean-architecture-boilerplate&utm_campaign=Badge_Grade) 2 | 3 | # Android Clean Architecture Boilerplate 4 | 5 | Welcome 👋 We hope this boilerplate is not only helpful to other developers, but also that it helps to educate in the area of architecture. We created this boilerplate for a few reasons: 6 | 7 | 1. To experiment with modularisation 8 | 2. To share some approaches to clean architecture, especially as we've been [talking a lot about it](https://academy.realm.io/posts/converting-an-app-to-use-clean-architecture/) 9 | 3. To use as a starting point in future projects where clean architecture feels appropriate 10 | 11 | It is written 100% in Kotlin with both UI and Unit tests - we will also be keeping this up-to-date as libraries change! 12 | 13 | ### Disclaimer 14 | 15 | Note: The use of clean architecture may seem over-complicated for this sample project. However, this allows us to keep the amount of boilerplate code to a minimum and also demonstrate the approach in a simpler form. 16 | 17 | Clean Architecture will not be appropriate for every project, so it is down to you to decide whether or not it fits your needs 🙂 18 | 19 | ## Languages, libraries and tools used 20 | 21 | * [Kotlin](https://kotlinlang.org/) 22 | * Android Support Libraries 23 | * [RxJava2](https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0) 24 | * [Dagger 2 (2.11)](https://github.com/google/dagger) 25 | * [Glide](https://github.com/bumptech/glide) 26 | * [Retrofit](http://square.github.io/retrofit/) 27 | * [OkHttp](http://square.github.io/okhttp/) 28 | * [Gson](https://github.com/google/gson) 29 | * [Timber](https://github.com/JakeWharton/timber) 30 | * [Mockito](http://site.mockito.org/) 31 | * [Espresso](https://developer.android.com/training/testing/espresso/index.html) 32 | * [Robolectric](http://robolectric.org/) 33 | 34 | ## Requirements 35 | 36 | * JDK 1.8 37 | * [Android SDK](https://developer.android.com/studio/index.html) 38 | * Android O ([API 26](https://developer.android.com/preview/api-overview.html)) 39 | * Latest Android SDK Tools and build tools. 40 | 41 | ## Architecture 42 | 43 | The architecture of the project follows the principles of Clean Archicture. Here's how the sample project implements it: 44 | 45 | ![architecture](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/art/architecture.png?raw=true) 46 | 47 | The sample app when run will show you a simple list of all the Bufferoos (Buffer team members!). 48 |

49 | Drawing 50 |

51 | 52 | Let's look at each of the architecture layers and the role each one plays :) 53 | 54 | ![architecture](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/art/ui.png?raw=true) 55 | 56 | ### User Interface 57 | 58 | This layer makes use of the Android Framework and is used to create all of our UI components to display inside of the [Browse Activity](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/9a1308c42c0c882fc724a0e579ee1ce4d454f961/mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/browse/BrowseActivity.kt). The layer receives its data from the Presentation layer and when retrieved, the received models are mapped using the [Bufferoo Mapper](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/9a1308c42c0c882fc724a0e579ee1ce4d454f961/mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/mapper/BufferooMapper.kt) so that the model can be mapped to this layer's interpretation of the Bufferoo instance, which is the [BufferooViewModel](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/9a1308c42c0c882fc724a0e579ee1ce4d454f961/mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/model/BufferooViewModel.kt). The Activity makes use of the [BrowseContract](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/presentation/src/main/java/org/buffer/android/boilerplate/presentation/browse/BrowseBufferoosContract.kt) to enable communication to and from the presenter 59 | 60 | ### Presentation 61 | 62 | This layer's responsibilty is to handle the presentation of the User Interface, but at the same time knows nothing about the user interface itself. This layer has no dependance on the Android Framework, it is a pure Kotlin module. Each Presenter class that is created implements the [Presenter](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/presentation/src/main/java/org/buffer/android/boilerplate/presentation/BasePresenter.kt) interface defined within an instance of a contract - in this case the [BrowseContract](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/presentation/src/main/java/org/buffer/android/boilerplate/presentation/browse/BrowseBufferoosContract.kt), which also contains an interface for the [View](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/presentation/src/main/java/org/buffer/android/boilerplate/presentation/BaseView.kt) interface. 63 | 64 | When a Presenter is constructed, an instance of this View is passed in. This view is then used and the presenter is set for it using the implemented [setPresenter()](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/presentation/src/main/java/org/buffer/android/boilerplate/presentation/browse/BrowseBufferoosPresenter.kt#L15) call. 65 | 66 | The presenters use an instance of a [SingleUseCase](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/SingleUseCase.kt) from the Domain layer to retrieve data. Note here that there is no direct name reference to the UseCase that we are using - we do inject an instance of the [GetBufferoos](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/browse/GetBufferoos.kt) UseCase, however. 67 | 68 | The presenter receives data from the Domain layer in the form of a [Bufferoo](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/presentation/src/main/java/org/buffer/android/boilerplate/presentation/model/BufferooView.kt). These instances are mapped to instance of this layers model, which is a BufferooView using the [BufferooMapper](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/presentation/src/main/java/org/buffer/android/boilerplate/presentation/mapper/BufferooMapper.kt). 69 | 70 | ### Domain 71 | 72 | The domain layer responsibility is to simply contain the UseCase instance used to retrieve data from the Data layer and pass it onto the Presentation layer. In our case, we define a [GetBufferoos](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/browse/GetBufferoos.kt) - this use case handles the subscribing and observing of our request for data from the BufferooRepository interface. This UseCase extends the [SingleUseCase](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/SingleUseCase.kt) base class - therefore we can reference it from outer layers and avoid a direct reference to a specific implementation. 73 | 74 | The layer defines the [Bufferoo](https://github.com/bufferapp/android-clean-architecture-boilerplate/tree/master/domain/src/main/java/org/buffer/android/boilerplate/domain/model) class but no mapper. This is because the Domain layer is our central layer, it knows nothing of the layers outside of it so has no need to map data to any other type of model. 75 | 76 | The Domain layer defines the [BufferooRepository](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/domain/src/main/java/org/buffer/android/boilerplate/domain/repository/BufferooRepository.kt) interface which provides a set of methods for an external layer to implement as the UseCase classes use the interface when requesting data. 77 | 78 | ![architecture](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/art/data.png?raw=true) 79 | 80 | ### Data 81 | 82 | The Data layer is our access point to external data layers and is used to fetch data from multiple sources (the cache and network in our case). It contains an implementation of the BufferooRepository, which is the [BufferooDataRepository](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/BufferooDataRepository.kt). To begin with, this class uses the [BufferooDataStoreFactory](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/source/BufferooDataStoreFactory.kt) to decide which data store class will be used when fetching data - this will be either the [BufferooRemoteDataStore](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/source/BufferooRemoteDataStore.kt) or the [BufferooCacheDataStore](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/source/BufferooCacheDataStore.kt) - both of these classes implement the [BufferooDataStore](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/repository/BufferooDataStore.kt) repository so that our DataStore classes are enforced. 83 | 84 | Each of these DataStore classes also references a corresponding [BufferooCache](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/repository/BufferooCache.kt) and [BufferooRemote](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/repository/BufferooRemote.kt) interface, which is used when requesting data from an external data source module. 85 | 86 | This layers data model is the [BufferooEntity](https://github.com/bufferapp/android-clean-architecture-boilerplate/tree/master/data/src/main/java/org/buffer/android/boilerplate/data/model). Here the [BufferooMapper](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/mapper/BufferooMapper.kt) is used to map data to and from a Bufferoo instance from the domain layer and BufferooEntity instance from this layer as required. 87 | 88 | ### Remote 89 | 90 | The Remote layer handles all communications with remote sources, in our case it makes a simple API call using a Retrofit interface. The [BufferooRemoteImpl](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/remote/src/main/java/org/buffer/android/boilerplate/remote/BufferooRemoteImpl.kt) class implements the [BufferooRemote](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/data/src/main/java/org/buffer/android/boilerplate/data/repository/BufferooRemote.kt) interface from the Data layer and uses the [BufferooService](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/remote/src/main/java/org/buffer/android/boilerplate/remote/BufferooService.kt) to retrieve data from the API. 91 | 92 | The API returns us instances of a [BufferooModel](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/remote/src/main/java/org/buffer/android/boilerplate/remote/model/BufferooModel.kt) and these are mapped to BufferooEntity instance from the Data layer using the [BufferooEntityMapper](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/remote/src/main/java/org/buffer/android/boilerplate/remote/mapper/BufferooEntityMapper.kt) class. 93 | 94 | ### Cache 95 | 96 | The Cache layer handles all communication with the local database which is used to cache data. 97 | 98 | The data model for this layer is the [CachedBufferoo](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/cache/src/main/java/org/buffer/android/boilerplate/cache/model/CachedBufferoo.kt) and this is mapped to and from a BufferooEntity instance from the Data layer using the [BufferooEntityMapper](https://github.com/bufferapp/android-clean-architecture-boilerplate/blob/master/cache/src/main/java/org/buffer/android/boilerplate/cache/mapper/BufferooEntityMapper.kt) class. 99 | 100 | ## Conclusion 101 | 102 | We will be happy to answer any questions that you may have on this approach, and if you want to lend a hand with the boilerplate then please feel free to submit an issue and/or pull request 🙂 103 | 104 | Again to note, use Clean Architecture where appropriate. This is example can appear as over-architectured for what it is - but it is an example only. The same can be said for individual models for each layer, this decision is down to you. In this example, the data used for ever model is exactly the same, so some may argue that "hey, maybe we don't need to map between the presentation and user-interface layer". Or maybe you don't want to modularise your data layer into data/remote/cache and want to just have it in a single 'data' module. That decision is down to you and the the project that you are working on 🙌🏻 105 | 106 | ## Thanks 107 | 108 | A special thanks to the authors involved with these two repositories, they were a great resource during our learning! 109 | 110 | - https://github.com/android10/Android-CleanArchitecture 111 | 112 | - https://github.com/googlesamples/android-architecture 113 | --------------------------------------------------------------------------------