├── .gitignore ├── .travis.yml ├── art ├── architecture.png ├── data.png ├── device_screenshot.png └── ui.png ├── build.gradle ├── cache ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── cache │ │ ├── BufferooCacheImpl.kt │ │ ├── PreferencesHelper.kt │ │ ├── dao │ │ └── CachedBufferooDao.kt │ │ ├── db │ │ ├── BufferoosDatabase.kt │ │ └── constants │ │ │ └── BufferooConstants.kt │ │ ├── mapper │ │ ├── BufferooEntityMapper.kt │ │ └── EntityMapper.kt │ │ └── model │ │ └── CachedBufferoo.kt │ └── test │ └── java │ └── org │ └── buffer │ └── android │ └── boilerplate │ └── cache │ ├── BufferooCacheImplTest.kt │ ├── dao │ └── CachedBufferooDaoTest.kt │ ├── mapper │ └── BufferooEntityMapperTest.kt │ └── test │ ├── DefaultConfig.kt │ └── factory │ ├── BufferooFactory.kt │ └── DataFactory.kt ├── data ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── data │ │ ├── BufferooDataRepository.kt │ │ ├── executor │ │ └── JobExecutor.kt │ │ ├── mapper │ │ ├── BufferooMapper.kt │ │ └── Mapper.kt │ │ ├── model │ │ └── BufferooEntity.kt │ │ ├── repository │ │ ├── BufferooCache.kt │ │ ├── BufferooDataStore.kt │ │ └── BufferooRemote.kt │ │ └── source │ │ ├── BufferooCacheDataStore.kt │ │ ├── BufferooDataStoreFactory.kt │ │ └── BufferooRemoteDataStore.kt │ └── test │ └── java │ └── org │ └── buffer │ └── android │ └── boilerplate │ └── data │ ├── BufferooDataRepositoryTest.kt │ ├── mapper │ └── BufferooMapperTest.kt │ ├── source │ ├── BufferooCacheDataStoreTest.kt │ ├── BufferooDataStoreFactoryTest.kt │ └── BufferooRemoteDataStoreTest.kt │ └── test │ └── factory │ ├── BufferooFactory.kt │ └── DataFactory.kt ├── dependencies.gradle ├── domain ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── domain │ │ ├── executor │ │ ├── PostExecutionThread.kt │ │ └── ThreadExecutor.kt │ │ ├── interactor │ │ ├── BaseFlowableObserver.kt │ │ ├── BaseSingleObserver.kt │ │ ├── CompletableUseCase.kt │ │ ├── FlowableUseCase.kt │ │ └── browse │ │ │ └── GetBufferoos.kt │ │ ├── model │ │ └── Bufferoo.kt │ │ └── repository │ │ └── BufferooRepository.kt │ └── test │ └── java │ └── org │ └── buffer │ └── android │ └── boilerplate │ └── domain │ ├── test │ └── factory │ │ ├── BufferooFactory.kt │ │ └── DataFactory.kt │ └── usecase │ └── bufferoo │ └── GetBufferoosTest.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── licence.txt ├── mobile-ui ├── .gitignore ├── build.gradle └── src │ ├── androidTest │ ├── AndroidManifest.xml │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── ui │ │ ├── browse │ │ └── BrowseActivityTest.kt │ │ ├── injection │ │ ├── component │ │ │ └── TestApplicationComponent.kt │ │ └── module │ │ │ ├── TestApplicationModule.kt │ │ │ ├── TestCacheModule.kt │ │ │ ├── TestDataModule.kt │ │ │ └── TestRemoteModule.kt │ │ └── test │ │ ├── TestApplication.kt │ │ ├── TestRunner.kt │ │ └── util │ │ ├── BufferooFactory.kt │ │ ├── DataFactory.kt │ │ └── RecyclerViewMatcher.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ └── buffer │ │ │ └── android │ │ │ └── boilerplate │ │ │ └── ui │ │ │ ├── BufferooApplication.kt │ │ │ ├── UiThread.kt │ │ │ ├── browse │ │ │ ├── BrowseActivity.kt │ │ │ └── BrowseAdapter.kt │ │ │ ├── injection │ │ │ ├── ApplicationComponent.kt │ │ │ ├── module │ │ │ │ ├── ApplicationModule.kt │ │ │ │ ├── CacheModule.kt │ │ │ │ ├── DataModule.kt │ │ │ │ ├── DomainModule.kt │ │ │ │ ├── PresentationModule.kt │ │ │ │ ├── RemoteModule.kt │ │ │ │ └── UiModule.kt │ │ │ └── scopes │ │ │ │ ├── PerActivity.kt │ │ │ │ └── PerApplication.kt │ │ │ ├── mapper │ │ │ ├── BufferooMapper.kt │ │ │ └── Mapper.kt │ │ │ ├── model │ │ │ └── BufferooViewModel.kt │ │ │ └── widget │ │ │ ├── empty │ │ │ ├── EmptyListener.kt │ │ │ └── EmptyView.kt │ │ │ └── error │ │ │ ├── ErrorListener.kt │ │ │ └── ErrorView.kt │ └── res │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_browse.xml │ │ ├── item_bufferoo.xml │ │ ├── view_empty.xml │ │ └── view_error.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── org │ └── buffer │ └── android │ └── boilerplate │ └── ui │ ├── BufferooMapperTest.kt │ └── test │ └── factory │ ├── BufferooFactory.kt │ └── DataFactory.kt ├── presentation ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── presentation │ │ ├── ViewModelFactory.kt │ │ ├── browse │ │ └── BrowseBufferoosViewModel.kt │ │ ├── data │ │ ├── Resource.kt │ │ └── ResourceState.kt │ │ ├── mapper │ │ ├── BufferooMapper.kt │ │ └── Mapper.kt │ │ └── model │ │ └── BufferooView.kt │ └── test │ └── java │ └── org │ └── buffer │ └── android │ └── boilerplate │ └── presentation │ ├── browse │ └── BrowseBufferoosViewModelTest.kt │ └── test │ └── factory │ ├── BufferooFactory.kt │ └── DataFactory.kt ├── readme.md ├── remote ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── buffer │ │ └── android │ │ └── boilerplate │ │ └── remote │ │ ├── BufferooRemoteImpl.kt │ │ ├── BufferooService.kt │ │ ├── BufferooServiceFactory.kt │ │ ├── mapper │ │ ├── BufferooEntityMapper.kt │ │ └── EntityMapper.kt │ │ └── model │ │ └── BufferooModel.kt │ └── test │ └── java │ └── org │ └── buffer │ └── android │ └── boilerplate │ └── remote │ ├── BufferooRemoteImplTest.kt │ ├── mapper │ └── BufferooEntityMapperTest.kt │ └── test │ └── factory │ ├── BufferooFactory.kt │ └── DataFactory.kt └── settings.gradle /.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 -------------------------------------------------------------------------------- /.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=25 9 | - ANDROID_API_LEVEL_22=22 10 | - ANDROID_BUILD_TOOLS_VERSION=25.0.3 11 | - ANDROID_ABI=armeabi-v7a 12 | - ANDROID_TAG=google_apis 13 | - ANDROID_TARGET=android-25 14 | - ADB_INSTALL_TIMEOUT=20 # minutes (2 minutes by default) 15 | 16 | android: 17 | components: 18 | - tools 19 | - platform-tools 20 | - android-$ANDROID_API_LEVEL_22 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 | - android-sdk-license-.+ 33 | - '.+' 34 | # Specify at least one system image 35 | - sys-img-armeabi-v7a-google_apis-$ANDROID_API_LEVEL 36 | - sys-img-armeabi-v7a-android-$ANDROID_API_LEVEL_22 37 | 38 | # list of directories to Cache 39 | 40 | before_install: 41 | - mkdir "$ANDROID_HOME/licenses" || true 42 | - echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" 43 | - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" 44 | 45 | licenses: 46 | - 'android-sdk-preview-license-.+' 47 | - 'android-sdk-license-.+' 48 | - 'google-gdk-license-.+' 49 | 50 | before_script: 51 | - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a 52 | - emulator -avd test -no-audio -no-window & 53 | - android-wait-for-emulator 54 | - adb shell input keyevent 82 & 55 | 56 | script: 57 | - ./gradlew build 58 | - ./gradlew build connectedAndroidTest --stacktrace 59 | 60 | after_success: 61 | - bash <(curl -s https://codecov.io/bash) 62 | -------------------------------------------------------------------------------- /art/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/clean-architecture-components-boilerplate/09f746223d7c852ac5ac26fbd9118e4237b55a9b/art/architecture.png -------------------------------------------------------------------------------- /art/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/clean-architecture-components-boilerplate/09f746223d7c852ac5ac26fbd9118e4237b55a9b/art/data.png -------------------------------------------------------------------------------- /art/device_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/clean-architecture-components-boilerplate/09f746223d7c852ac5ac26fbd9118e4237b55a9b/art/device_screenshot.png -------------------------------------------------------------------------------- /art/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/clean-architecture-components-boilerplate/09f746223d7c852ac5ac26fbd9118e4237b55a9b/art/ui.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.1.51' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.0.0-rc1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | classpath "com.dicedmelon.gradle:jacoco-android:0.1.1" 13 | 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | apply from: 'dependencies.gradle' 21 | 22 | allprojects { 23 | repositories { 24 | google() 25 | jcenter() 26 | } 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml -------------------------------------------------------------------------------- /cache/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | def globalConfiguration = rootProject.extensions.getByName("ext") 7 | 8 | compileSdkVersion globalConfiguration["androidCompileSdkVersion"] 9 | buildToolsVersion globalConfiguration["androidBuildToolsVersion"] 10 | 11 | defaultConfig { 12 | minSdkVersion globalConfiguration["androidMinSdkVersion"] 13 | targetSdkVersion globalConfiguration["androidTargetSdkVersion"] 14 | multiDexEnabled = true 15 | } 16 | 17 | dexOptions { 18 | preDexLibraries = false 19 | dexInProcess = false 20 | javaMaxHeapSize "4g" 21 | } 22 | 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_1_7 25 | targetCompatibility JavaVersion.VERSION_1_7 26 | } 27 | 28 | packagingOptions { 29 | exclude 'LICENSE.txt' 30 | exclude 'META-INF/DEPENDENCIES' 31 | exclude 'META-INF/ASL2.0' 32 | exclude 'META-INF/NOTICE' 33 | exclude 'META-INF/LICENSE' 34 | } 35 | 36 | lintOptions { 37 | quiet true 38 | abortOnError false 39 | ignoreWarnings true 40 | disable 'InvalidPackage' //Some libraries have issues with this. 41 | disable 'OldTargetApi' //Lint gives this warning but SDK 20 would be Android L Beta. 42 | disable 'IconDensities' //For testing purpose. This is safe to remove. 43 | disable 'IconMissingDensityFolder' //For testing purpose. This is safe to remove. 44 | } 45 | 46 | } 47 | 48 | dependencies { 49 | def cacheDependencies = rootProject.ext.cacheDependencies 50 | def cacheTestDependencies = rootProject.ext.cacheTestDependencies 51 | 52 | compileOnly cacheDependencies.javaxAnnotation 53 | 54 | implementation project(':data') 55 | 56 | implementation cacheDependencies.kotlin 57 | implementation cacheDependencies.javaxInject 58 | implementation cacheDependencies.rxKotlin 59 | implementation cacheDependencies.gson 60 | implementation cacheDependencies.roomRuntime 61 | kapt cacheDependencies.roomCompiler 62 | 63 | testImplementation cacheTestDependencies.junit 64 | testImplementation cacheTestDependencies.kotlinJUnit 65 | testImplementation cacheTestDependencies.mockito 66 | testImplementation cacheTestDependencies.assertj 67 | testImplementation cacheTestDependencies.robolectric 68 | testImplementation cacheTestDependencies.archTesting 69 | testImplementation cacheTestDependencies.roomTesting 70 | } -------------------------------------------------------------------------------- /cache/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/BufferooCacheImpl.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Flowable 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.cache.db.BufferoosDatabase 7 | import org.buffer.android.boilerplate.cache.mapper.BufferooEntityMapper 8 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 9 | import org.buffer.android.boilerplate.data.model.BufferooEntity 10 | import org.buffer.android.boilerplate.data.repository.BufferooCache 11 | import javax.inject.Inject 12 | 13 | /** 14 | * Cached implementation for retrieving and saving Bufferoo instances. This class implements the 15 | * [BufferooCache] from the Data layer as it is that layers responsibility for defining the 16 | * operations in which data store implementation layers can carry out. 17 | */ 18 | class BufferooCacheImpl @Inject constructor(val bufferoosDatabase: BufferoosDatabase, 19 | private val entityMapper: BufferooEntityMapper, 20 | private val preferencesHelper: PreferencesHelper) : 21 | BufferooCache { 22 | 23 | private val EXPIRATION_TIME = (60 * 10 * 1000).toLong() 24 | 25 | /** 26 | * Retrieve an instance from the database, used for tests. 27 | */ 28 | internal fun getDatabase(): BufferoosDatabase { 29 | return bufferoosDatabase 30 | } 31 | 32 | /** 33 | * Remove all the data from all the tables in the database. 34 | */ 35 | override fun clearBufferoos(): Completable { 36 | return Completable.defer { 37 | bufferoosDatabase.cachedBufferooDao().clearBufferoos() 38 | Completable.complete() 39 | } 40 | } 41 | 42 | /** 43 | * Save the given list of [BufferooEntity] instances to the database. 44 | */ 45 | override fun saveBufferoos(bufferoos: List): Completable { 46 | return Completable.defer { 47 | bufferoos.forEach { 48 | bufferoosDatabase.cachedBufferooDao().insertBufferoo( 49 | entityMapper.mapToCached(it)) 50 | } 51 | Completable.complete() 52 | } 53 | } 54 | 55 | /** 56 | * Retrieve a list of [BufferooEntity] instances from the database. 57 | */ 58 | override fun getBufferoos(): Flowable> { 59 | return Flowable.defer { 60 | Flowable.just(bufferoosDatabase.cachedBufferooDao().getBufferoos()) 61 | }.map { 62 | it.map { entityMapper.mapFromCached(it) } 63 | } 64 | } 65 | 66 | /** 67 | * Check whether there are instances of [CachedBufferoo] stored in the cache. 68 | */ 69 | override fun isCached(): Single { 70 | return Single.defer { 71 | Single.just(bufferoosDatabase.cachedBufferooDao().getBufferoos().isNotEmpty()) 72 | } 73 | } 74 | 75 | /** 76 | * Set a point in time at when the cache was last updated. 77 | */ 78 | override fun setLastCacheTime(lastCache: Long) { 79 | preferencesHelper.lastCacheTime = lastCache 80 | } 81 | 82 | /** 83 | * Check whether the current cached data exceeds the defined [EXPIRATION_TIME] time. 84 | */ 85 | override fun isExpired(): Boolean { 86 | val currentTime = System.currentTimeMillis() 87 | val lastUpdateTime = this.getLastCacheUpdateTimeMillis() 88 | return currentTime - lastUpdateTime > EXPIRATION_TIME 89 | } 90 | 91 | /** 92 | * Get in millis, the last time the cache was accessed. 93 | */ 94 | private fun getLastCacheUpdateTimeMillis(): Long { 95 | return preferencesHelper.lastCacheTime 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /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 | open 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 | -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/dao/CachedBufferooDao.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.dao 2 | 3 | import android.arch.persistence.room.Dao 4 | import android.arch.persistence.room.Insert 5 | import android.arch.persistence.room.OnConflictStrategy 6 | import android.arch.persistence.room.Query 7 | import org.buffer.android.boilerplate.cache.db.constants.BufferooConstants 8 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 9 | 10 | @Dao 11 | abstract class CachedBufferooDao { 12 | 13 | @Query(BufferooConstants.QUERY_BUFFEROOS) 14 | abstract fun getBufferoos(): List 15 | 16 | @Query(BufferooConstants.DELETE_ALL_BUFFEROOS) 17 | abstract fun clearBufferoos() 18 | 19 | @Insert(onConflict = OnConflictStrategy.REPLACE) 20 | abstract fun insertBufferoo(cachedBufferoo: CachedBufferoo) 21 | 22 | } -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/db/BufferoosDatabase.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.db 2 | 3 | import android.arch.persistence.room.Database 4 | import android.arch.persistence.room.Room 5 | import android.arch.persistence.room.RoomDatabase 6 | import android.content.Context 7 | import org.buffer.android.boilerplate.cache.dao.CachedBufferooDao 8 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 9 | import javax.inject.Inject 10 | 11 | @Database(entities = arrayOf(CachedBufferoo::class), version = 1) 12 | abstract class BufferoosDatabase @Inject constructor() : RoomDatabase() { 13 | 14 | abstract fun cachedBufferooDao(): CachedBufferooDao 15 | 16 | private var INSTANCE: BufferoosDatabase? = null 17 | 18 | private val sLock = Any() 19 | 20 | fun getInstance(context: Context): BufferoosDatabase { 21 | if (INSTANCE == null) { 22 | synchronized(sLock) { 23 | if (INSTANCE == null) { 24 | INSTANCE = Room.databaseBuilder(context.applicationContext, 25 | BufferoosDatabase::class.java, "bufferoos.db") 26 | .build() 27 | } 28 | return INSTANCE!! 29 | } 30 | } 31 | return INSTANCE!! 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/db/constants/BufferooConstants.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.db.constants 2 | 3 | /** 4 | * Defines constants for the Bufferoos Table 5 | */ 6 | object BufferooConstants { 7 | 8 | const val TABLE_NAME = "bufferoos" 9 | 10 | const val QUERY_BUFFEROOS = "SELECT * FROM" + " " + TABLE_NAME 11 | 12 | const val DELETE_ALL_BUFFEROOS = "DELETE FROM" + " " + TABLE_NAME 13 | 14 | } -------------------------------------------------------------------------------- /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 | open class BufferooEntityMapper @Inject constructor(): 12 | EntityMapper { 13 | 14 | /** 15 | * Map a [BufferooEntity] instance to a [CachedBufferoo] instance 16 | */ 17 | override fun mapToCached(type: BufferooEntity): CachedBufferoo { 18 | return CachedBufferoo(type.id, type.name, type.title, type.avatar) 19 | } 20 | 21 | /** 22 | * Map a [CachedBufferoo] instance to a [BufferooEntity] instance 23 | */ 24 | override fun mapFromCached(type: CachedBufferoo): BufferooEntity { 25 | return BufferooEntity(type.id, type.name, type.title, type.avatar) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /cache/src/main/java/org/buffer/android/boilerplate/cache/model/CachedBufferoo.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.model 2 | 3 | import android.arch.persistence.room.Entity 4 | import android.arch.persistence.room.PrimaryKey 5 | import org.buffer.android.boilerplate.cache.db.constants.BufferooConstants 6 | 7 | /** 8 | * Model used solely for the caching of a bufferroo 9 | */ 10 | @Entity(tableName = BufferooConstants.TABLE_NAME) 11 | data class CachedBufferoo( 12 | 13 | @PrimaryKey 14 | var id: Long, 15 | val name: String, 16 | val title: String, 17 | val avatar: String 18 | 19 | ) -------------------------------------------------------------------------------- /cache/src/test/java/org/buffer/android/boilerplate/cache/BufferooCacheImplTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache 2 | 3 | import android.arch.persistence.room.Room 4 | import org.buffer.android.boilerplate.cache.db.BufferoosDatabase 5 | import org.buffer.android.boilerplate.cache.mapper.BufferooEntityMapper 6 | import org.buffer.android.boilerplate.cache.model.CachedBufferoo 7 | import org.buffer.android.boilerplate.cache.test.factory.BufferooFactory 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.RobolectricTestRunner 11 | import org.robolectric.RuntimeEnvironment 12 | import org.robolectric.annotation.Config 13 | import kotlin.test.assertEquals 14 | 15 | @RunWith(RobolectricTestRunner::class) 16 | @Config(sdk = intArrayOf(21)) 17 | class BufferooCacheImplTest { 18 | 19 | private var bufferoosDatabase = Room.inMemoryDatabaseBuilder(RuntimeEnvironment.application, 20 | BufferoosDatabase::class.java).allowMainThreadQueries().build() 21 | private var entityMapper = BufferooEntityMapper() 22 | private var preferencesHelper = PreferencesHelper(RuntimeEnvironment.application) 23 | 24 | 25 | private val databaseHelper = BufferooCacheImpl(bufferoosDatabase, 26 | entityMapper, preferencesHelper) 27 | 28 | @Test 29 | fun clearTablesCompletes() { 30 | val testObserver = databaseHelper.clearBufferoos().test() 31 | testObserver.assertComplete() 32 | } 33 | 34 | // 35 | @Test 36 | fun saveBufferoosCompletes() { 37 | val bufferooEntities = BufferooFactory.makeBufferooEntityList(2) 38 | 39 | val testObserver = databaseHelper.saveBufferoos(bufferooEntities).test() 40 | testObserver.assertComplete() 41 | } 42 | 43 | @Test 44 | fun saveBufferoosSavesData() { 45 | val bufferooCount = 2 46 | val bufferooEntities = BufferooFactory.makeBufferooEntityList(bufferooCount) 47 | 48 | databaseHelper.saveBufferoos(bufferooEntities).test() 49 | checkNumRowsInBufferoosTable(bufferooCount) 50 | } 51 | // 52 | 53 | // 54 | @Test 55 | fun getBufferoosCompletes() { 56 | val testObserver = databaseHelper.getBufferoos().test() 57 | testObserver.assertComplete() 58 | } 59 | 60 | @Test 61 | fun getBufferoosReturnsData() { 62 | val bufferooEntities = BufferooFactory.makeBufferooEntityList(2) 63 | val cachedBufferoos = mutableListOf() 64 | bufferooEntities.forEach { 65 | cachedBufferoos.add(entityMapper.mapToCached(it)) 66 | } 67 | insertBufferoos(cachedBufferoos) 68 | 69 | val testObserver = databaseHelper.getBufferoos().test() 70 | // testObserver.assertValue(bufferooEntities) 71 | } 72 | // 73 | 74 | private fun insertBufferoos(cachedBufferoos: List) { 75 | cachedBufferoos.forEach { 76 | bufferoosDatabase.cachedBufferooDao().insertBufferoo(it) 77 | } 78 | } 79 | 80 | private fun checkNumRowsInBufferoosTable(expectedRows: Int) { 81 | val numberOfRows = bufferoosDatabase.cachedBufferooDao().getBufferoos().size 82 | assertEquals(expectedRows, numberOfRows) 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /cache/src/test/java/org/buffer/android/boilerplate/cache/dao/CachedBufferooDaoTest.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.cache.dao 2 | 3 | import android.arch.persistence.room.Room 4 | import org.buffer.android.boilerplate.cache.db.BufferoosDatabase 5 | import org.buffer.android.boilerplate.cache.test.factory.BufferooFactory 6 | import org.junit.After 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.robolectric.RobolectricTestRunner 11 | import org.robolectric.RuntimeEnvironment 12 | 13 | @RunWith(RobolectricTestRunner::class) 14 | open class CachedBufferooDaoTest { 15 | 16 | private lateinit var bufferoosDatabase: BufferoosDatabase 17 | 18 | @Before 19 | fun initDb() { 20 | bufferoosDatabase = Room.inMemoryDatabaseBuilder( 21 | RuntimeEnvironment.application.baseContext, 22 | BufferoosDatabase::class.java) 23 | .allowMainThreadQueries() 24 | .build() 25 | } 26 | 27 | @After 28 | fun closeDb() { 29 | bufferoosDatabase.close() 30 | } 31 | 32 | @Test 33 | fun insertBufferoosSavesData() { 34 | val cachedBufferoo = BufferooFactory.makeCachedBufferoo() 35 | bufferoosDatabase.cachedBufferooDao().insertBufferoo(cachedBufferoo) 36 | 37 | val bufferoos = bufferoosDatabase.cachedBufferooDao().getBufferoos() 38 | assert(bufferoos.isNotEmpty()) 39 | } 40 | 41 | @Test 42 | fun getBufferoosRetrievesData() { 43 | val cachedBufferoos = BufferooFactory.makeCachedBufferooList(5) 44 | 45 | cachedBufferoos.forEach { 46 | bufferoosDatabase.cachedBufferooDao().insertBufferoo(it) } 47 | 48 | val retrievedBufferoos = bufferoosDatabase.cachedBufferooDao().getBufferoos() 49 | assert(retrievedBufferoos == cachedBufferoos.sortedWith(compareBy({ it.id }, { it.id }))) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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.cache.test.factory.DataFactory.Factory.randomLong 5 | import org.buffer.android.boilerplate.cache.test.factory.DataFactory.Factory.randomUuid 6 | import org.buffer.android.boilerplate.data.model.BufferooEntity 7 | 8 | /** 9 | * Factory class for Bufferoo related instances 10 | */ 11 | class BufferooFactory { 12 | 13 | companion object Factory { 14 | 15 | fun makeCachedBufferoo(): CachedBufferoo { 16 | return CachedBufferoo(randomLong(), randomUuid(), randomUuid(), randomUuid()) 17 | } 18 | 19 | fun makeBufferooEntity(): BufferooEntity { 20 | return BufferooEntity(randomLong(), randomUuid(), randomUuid(), randomUuid()) 21 | } 22 | 23 | fun makeBufferooEntityList(count: Int): List { 24 | val bufferooEntities = mutableListOf() 25 | repeat(count) { 26 | bufferooEntities.add(makeBufferooEntity()) 27 | } 28 | return bufferooEntities 29 | } 30 | 31 | fun makeCachedBufferooList(count: Int): List { 32 | val cachedBufferoos = mutableListOf() 33 | repeat(count) { 34 | cachedBufferoos.add(makeCachedBufferoo()) 35 | } 36 | return cachedBufferoos 37 | } 38 | 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /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/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml -------------------------------------------------------------------------------- /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/BufferooDataRepository.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.data 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Flowable 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.domain.model.Bufferoo 9 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 10 | import javax.inject.Inject 11 | 12 | /** 13 | * Provides an implementation of the [BufferooRepository] interface for communicating to and from 14 | * data sources 15 | */ 16 | class BufferooDataRepository @Inject constructor(private val factory: BufferooDataStoreFactory, 17 | private val bufferooMapper: BufferooMapper) : 18 | BufferooRepository { 19 | 20 | override fun clearBufferoos(): Completable { 21 | return factory.retrieveCacheDataStore().clearBufferoos() 22 | } 23 | 24 | override fun saveBufferoos(bufferoos: List): Completable { 25 | val bufferooEntities = mutableListOf() 26 | bufferoos.map { bufferooEntities.add(bufferooMapper.mapToEntity(it)) } 27 | return factory.retrieveCacheDataStore().saveBufferoos(bufferooEntities) 28 | } 29 | 30 | override fun getBufferoos(): Flowable> { 31 | return factory.retrieveCacheDataStore().isCached() 32 | .flatMapPublisher { 33 | factory.retrieveDataStore(it).getBufferoos() 34 | } 35 | .flatMap { 36 | Flowable.just(it.map { bufferooMapper.mapFromEntity(it) }) 37 | } 38 | .flatMap { 39 | saveBufferoos(it).toSingle { it }.toFlowable() 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /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/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.id, 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.id, type.name, type.title, type.avatar) 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 id: Long, val name: String, val title: String, val avatar: String) -------------------------------------------------------------------------------- /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.Flowable 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.data.model.BufferooEntity 7 | 8 | /** 9 | * Interface defining methods for the caching of Bufferroos. This is to be implemented by the 10 | * cache layer, using this interface as a way of communicating. 11 | */ 12 | interface BufferooCache { 13 | 14 | /** 15 | * Clear all Bufferoos from the cache. 16 | */ 17 | fun clearBufferoos(): Completable 18 | 19 | /** 20 | * Save a given list of Bufferoos to the cache. 21 | */ 22 | fun saveBufferoos(bufferoos: List): Completable 23 | 24 | /** 25 | * Retrieve a list of Bufferoos, from the cache. 26 | */ 27 | fun getBufferoos(): Flowable> 28 | 29 | /** 30 | * Check whether there is a list of Bufferoos stored in the cache. 31 | * 32 | * @return true if the list is cached, otherwise false 33 | */ 34 | fun isCached(): Single 35 | 36 | /** 37 | * Set a point in time at when the cache was last updated. 38 | * 39 | * @param lastCache the point in time at when the cache was last updated 40 | */ 41 | fun setLastCacheTime(lastCache: Long) 42 | 43 | /** 44 | * Check if the cache is expired. 45 | * 46 | * @return true if the cache is expired, otherwise false 47 | */ 48 | fun isExpired(): Boolean 49 | 50 | } -------------------------------------------------------------------------------- /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.Flowable 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.data.model.BufferooEntity 7 | 8 | /** 9 | * Interface defining methods for the data operations related to Bufferroos. 10 | * This is to be implemented by external data source layers, setting the requirements for the 11 | * operations that need to be implemented 12 | */ 13 | interface BufferooDataStore { 14 | 15 | fun clearBufferoos(): Completable 16 | 17 | fun saveBufferoos(bufferoos: List): Completable 18 | 19 | fun getBufferoos(): Flowable> 20 | 21 | fun isCached(): Single 22 | 23 | } -------------------------------------------------------------------------------- /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.Flowable 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(): Flowable> 16 | 17 | } -------------------------------------------------------------------------------- /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.Flowable 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.data.model.BufferooEntity 7 | import org.buffer.android.boilerplate.data.repository.BufferooCache 8 | import org.buffer.android.boilerplate.data.repository.BufferooDataStore 9 | import javax.inject.Inject 10 | 11 | /** 12 | * Implementation of the [BufferooDataStore] interface to provide a means of communicating 13 | * with the local data source 14 | */ 15 | open class BufferooCacheDataStore @Inject constructor(private val bufferooCache: BufferooCache) : 16 | BufferooDataStore { 17 | 18 | /** 19 | * Clear all Bufferoos from the cache 20 | */ 21 | override fun clearBufferoos(): Completable { 22 | return bufferooCache.clearBufferoos() 23 | } 24 | 25 | /** 26 | * Save a given [List] of [BufferooEntity] instances to the cache 27 | */ 28 | override fun saveBufferoos(bufferoos: List): Completable { 29 | return bufferooCache.saveBufferoos(bufferoos) 30 | .doOnComplete { 31 | bufferooCache.setLastCacheTime(System.currentTimeMillis()) 32 | } 33 | } 34 | 35 | /** 36 | * Retrieve a list of [BufferooEntity] instance from the cache 37 | */ 38 | override fun getBufferoos(): Flowable> { 39 | return bufferooCache.getBufferoos() 40 | } 41 | 42 | /** 43 | * Retrieve a list of [BufferooEntity] instance from the cache 44 | */ 45 | override fun isCached(): Single { 46 | return bufferooCache.isCached() 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /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(isCached: Boolean): BufferooDataStore { 20 | if (isCached && !bufferooCache.isExpired()) { 21 | return retrieveCacheDataStore() 22 | } 23 | return retrieveRemoteDataStore() 24 | } 25 | 26 | /** 27 | * Return an instance of the Cache Data Store 28 | */ 29 | open fun retrieveCacheDataStore(): BufferooDataStore { 30 | return bufferooCacheDataStore 31 | } 32 | 33 | /** 34 | * Return an instance of the Remote Data Store 35 | */ 36 | open fun retrieveRemoteDataStore(): BufferooDataStore { 37 | return bufferooRemoteDataStore 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /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.Flowable 5 | import io.reactivex.Single 6 | import org.buffer.android.boilerplate.data.model.BufferooEntity 7 | import org.buffer.android.boilerplate.data.repository.BufferooDataStore 8 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 9 | import javax.inject.Inject 10 | 11 | /** 12 | * Implementation of the [BufferooDataStore] interface to provide a means of communicating 13 | * with the remote data source 14 | */ 15 | open class BufferooRemoteDataStore @Inject constructor(private val bufferooRemote: BufferooRemote) : 16 | BufferooDataStore { 17 | 18 | override fun clearBufferoos(): Completable { 19 | throw UnsupportedOperationException() 20 | } 21 | 22 | override fun saveBufferoos(bufferoos: List): Completable { 23 | throw UnsupportedOperationException() 24 | } 25 | 26 | /** 27 | * Retrieve a list of [BufferooEntity] instances from the API 28 | */ 29 | override fun getBufferoos(): Flowable> { 30 | return bufferooRemote.getBufferoos() 31 | } 32 | 33 | override fun isCached(): Single { 34 | throw UnsupportedOperationException() 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /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.Flowable 6 | import io.reactivex.Single 7 | import org.buffer.android.boilerplate.data.mapper.BufferooMapper 8 | import org.buffer.android.boilerplate.data.model.BufferooEntity 9 | import org.buffer.android.boilerplate.data.repository.BufferooDataStore 10 | import org.buffer.android.boilerplate.data.source.BufferooCacheDataStore 11 | import org.buffer.android.boilerplate.data.source.BufferooDataStoreFactory 12 | import org.buffer.android.boilerplate.data.source.BufferooRemoteDataStore 13 | import org.buffer.android.boilerplate.data.test.factory.BufferooFactory 14 | import org.buffer.android.boilerplate.domain.model.Bufferoo 15 | import org.junit.Before 16 | import org.junit.Test 17 | import org.junit.runner.RunWith 18 | import org.junit.runners.JUnit4 19 | 20 | @RunWith(JUnit4::class) 21 | class BufferooDataRepositoryTest { 22 | 23 | private lateinit var bufferooDataRepository: BufferooDataRepository 24 | 25 | private lateinit var bufferooDataStoreFactory: BufferooDataStoreFactory 26 | private lateinit var bufferooMapper: BufferooMapper 27 | private lateinit var bufferooCacheDataStore: BufferooCacheDataStore 28 | private lateinit var bufferooRemoteDataStore: BufferooRemoteDataStore 29 | 30 | @Before 31 | fun setUp() { 32 | bufferooDataStoreFactory = mock() 33 | bufferooMapper = mock() 34 | bufferooCacheDataStore = mock() 35 | bufferooRemoteDataStore = mock() 36 | bufferooDataRepository = BufferooDataRepository(bufferooDataStoreFactory, bufferooMapper) 37 | stubBufferooDataStoreFactoryRetrieveCacheDataStore() 38 | stubBufferooDataStoreFactoryRetrieveRemoteDataStore() 39 | } 40 | 41 | // 42 | @Test 43 | fun clearBufferoosCompletes() { 44 | stubBufferooCacheClearBufferoos(Completable.complete()) 45 | val testObserver = bufferooDataRepository.clearBufferoos().test() 46 | testObserver.assertComplete() 47 | } 48 | 49 | @Test 50 | fun clearBufferoosCallsCacheDataStore() { 51 | stubBufferooCacheClearBufferoos(Completable.complete()) 52 | bufferooDataRepository.clearBufferoos().test() 53 | verify(bufferooCacheDataStore).clearBufferoos() 54 | } 55 | 56 | @Test 57 | fun clearBufferoosNeverCallsRemoteDataStore() { 58 | stubBufferooCacheClearBufferoos(Completable.complete()) 59 | bufferooDataRepository.clearBufferoos().test() 60 | verify(bufferooRemoteDataStore, never()).clearBufferoos() 61 | } 62 | // 63 | 64 | // 65 | @Test 66 | fun saveBufferoosCompletes() { 67 | stubBufferooCacheSaveBufferoos(Completable.complete()) 68 | val testObserver = bufferooDataRepository.saveBufferoos( 69 | BufferooFactory.makeBufferooList(2)).test() 70 | testObserver.assertComplete() 71 | } 72 | 73 | @Test 74 | fun saveBufferoosCallsCacheDataStore() { 75 | stubBufferooCacheSaveBufferoos(Completable.complete()) 76 | bufferooDataRepository.saveBufferoos(BufferooFactory.makeBufferooList(2)).test() 77 | verify(bufferooCacheDataStore).saveBufferoos(any()) 78 | } 79 | 80 | @Test 81 | fun saveBufferoosNeverCallsRemoteDataStore() { 82 | stubBufferooCacheSaveBufferoos(Completable.complete()) 83 | bufferooDataRepository.saveBufferoos(BufferooFactory.makeBufferooList(2)).test() 84 | verify(bufferooRemoteDataStore, never()).saveBufferoos(any()) 85 | } 86 | // 87 | 88 | // 89 | @Test 90 | fun getBufferoosCompletes() { 91 | stubBufferooCacheDataStoreIsCached(Single.just(true)) 92 | stubBufferooDataStoreFactoryRetrieveDataStore(bufferooCacheDataStore) 93 | stubBufferooCacheDataStoreGetBufferoos(Flowable.just( 94 | BufferooFactory.makeBufferooEntityList(2))) 95 | stubBufferooCacheSaveBufferoos(Completable.complete()) 96 | val testObserver = bufferooDataRepository.getBufferoos().test() 97 | testObserver.assertComplete() 98 | } 99 | 100 | @Test 101 | fun getBufferoosReturnsData() { 102 | stubBufferooCacheDataStoreIsCached(Single.just(true)) 103 | stubBufferooDataStoreFactoryRetrieveDataStore(bufferooCacheDataStore) 104 | stubBufferooCacheSaveBufferoos(Completable.complete()) 105 | val bufferoos = BufferooFactory.makeBufferooList(2) 106 | val bufferooEntities = BufferooFactory.makeBufferooEntityList(2) 107 | bufferoos.forEachIndexed { index, bufferoo -> 108 | stubBufferooMapperMapFromEntity(bufferooEntities[index], bufferoo) } 109 | stubBufferooCacheDataStoreGetBufferoos(Flowable.just(bufferooEntities)) 110 | 111 | val testObserver = bufferooDataRepository.getBufferoos().test() 112 | testObserver.assertValue(bufferoos) 113 | } 114 | 115 | @Test 116 | fun getBufferoosSavesBufferoosWhenFromCacheDataStore() { 117 | stubBufferooDataStoreFactoryRetrieveDataStore(bufferooCacheDataStore) 118 | stubBufferooCacheSaveBufferoos(Completable.complete()) 119 | bufferooDataRepository.saveBufferoos(BufferooFactory.makeBufferooList(2)).test() 120 | verify(bufferooCacheDataStore).saveBufferoos(any()) 121 | } 122 | 123 | @Test 124 | fun getBufferoosNeverSavesBufferoosWhenFromRemoteDataStore() { 125 | stubBufferooDataStoreFactoryRetrieveDataStore(bufferooRemoteDataStore) 126 | stubBufferooCacheSaveBufferoos(Completable.complete()) 127 | bufferooDataRepository.saveBufferoos(BufferooFactory.makeBufferooList(2)).test() 128 | verify(bufferooRemoteDataStore, never()).saveBufferoos(any()) 129 | } 130 | // 131 | 132 | // 133 | private fun stubBufferooCacheSaveBufferoos(completable: Completable) { 134 | whenever(bufferooCacheDataStore.saveBufferoos(any())) 135 | .thenReturn(completable) 136 | } 137 | 138 | private fun stubBufferooCacheDataStoreIsCached(single: Single) { 139 | whenever(bufferooCacheDataStore.isCached()) 140 | .thenReturn(single) 141 | } 142 | 143 | private fun stubBufferooCacheDataStoreGetBufferoos(single: Flowable>) { 144 | whenever(bufferooCacheDataStore.getBufferoos()) 145 | .thenReturn(single) 146 | } 147 | 148 | private fun stubBufferooRemoteDataStoreGetBufferoos(single: Flowable>) { 149 | whenever(bufferooRemoteDataStore.getBufferoos()) 150 | .thenReturn(single) 151 | } 152 | 153 | private fun stubBufferooCacheClearBufferoos(completable: Completable) { 154 | whenever(bufferooCacheDataStore.clearBufferoos()) 155 | .thenReturn(completable) 156 | } 157 | 158 | private fun stubBufferooDataStoreFactoryRetrieveCacheDataStore() { 159 | whenever(bufferooDataStoreFactory.retrieveCacheDataStore()) 160 | .thenReturn(bufferooCacheDataStore) 161 | } 162 | 163 | private fun stubBufferooDataStoreFactoryRetrieveRemoteDataStore() { 164 | whenever(bufferooDataStoreFactory.retrieveRemoteDataStore()) 165 | .thenReturn(bufferooCacheDataStore) 166 | } 167 | 168 | private fun stubBufferooDataStoreFactoryRetrieveDataStore(dataStore: BufferooDataStore) { 169 | whenever(bufferooDataStoreFactory.retrieveDataStore(any())) 170 | .thenReturn(dataStore) 171 | } 172 | 173 | private fun stubBufferooMapperMapFromEntity(bufferooEntity: BufferooEntity, 174 | bufferoo: Bufferoo) { 175 | whenever(bufferooMapper.mapFromEntity(bufferooEntity)) 176 | .thenReturn(bufferoo) 177 | } 178 | // 179 | 180 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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.Flowable 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(Flowable.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: Flowable>) { 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 | } -------------------------------------------------------------------------------- /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 io.reactivex.Single 6 | import org.buffer.android.boilerplate.data.repository.BufferooCache 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.junit.runners.JUnit4 11 | 12 | @RunWith(JUnit4::class) 13 | class BufferooDataStoreFactoryTest { 14 | 15 | private lateinit var bufferooDataStoreFactory: BufferooDataStoreFactory 16 | 17 | private lateinit var bufferooCache: BufferooCache 18 | private lateinit var bufferooCacheDataStore: BufferooCacheDataStore 19 | private lateinit var bufferooRemoteDataStore: BufferooRemoteDataStore 20 | 21 | @Before 22 | fun setUp() { 23 | bufferooCache = mock() 24 | bufferooCacheDataStore = mock() 25 | bufferooRemoteDataStore = mock() 26 | bufferooDataStoreFactory = BufferooDataStoreFactory(bufferooCache, 27 | bufferooCacheDataStore, bufferooRemoteDataStore) 28 | } 29 | 30 | // 31 | @Test 32 | fun retrieveDataStoreWhenNotCachedReturnsRemoteDataStore() { 33 | stubBufferooCacheIsCached(Single.just(false)) 34 | val bufferooDataStore = bufferooDataStoreFactory.retrieveDataStore(false) 35 | assert(bufferooDataStore is BufferooRemoteDataStore) 36 | } 37 | 38 | @Test 39 | fun retrieveDataStoreWhenCacheExpiredReturnsRemoteDataStore() { 40 | stubBufferooCacheIsCached(Single.just(true)) 41 | stubBufferooCacheIsExpired(true) 42 | val bufferooDataStore = bufferooDataStoreFactory.retrieveDataStore(true) 43 | assert(bufferooDataStore is BufferooRemoteDataStore) 44 | } 45 | 46 | @Test 47 | fun retrieveDataStoreReturnsCacheDataStore() { 48 | stubBufferooCacheIsCached(Single.just(true)) 49 | stubBufferooCacheIsExpired(false) 50 | val bufferooDataStore = bufferooDataStoreFactory.retrieveDataStore(true) 51 | assert(bufferooDataStore is BufferooCacheDataStore) 52 | } 53 | // 54 | 55 | // 56 | @Test 57 | fun retrieveRemoteDataStoreReturnsRemoteDataStore() { 58 | val bufferooDataStore = bufferooDataStoreFactory.retrieveRemoteDataStore() 59 | assert(bufferooDataStore is BufferooRemoteDataStore) 60 | } 61 | // 62 | 63 | // 64 | @Test 65 | fun retrieveCacheDataStoreReturnsCacheDataStore() { 66 | val bufferooDataStore = bufferooDataStoreFactory.retrieveCacheDataStore() 67 | assert(bufferooDataStore is BufferooCacheDataStore) 68 | } 69 | // 70 | 71 | // 72 | private fun stubBufferooCacheIsCached(single: Single) { 73 | whenever(bufferooCache.isCached()) 74 | .thenReturn(single) 75 | } 76 | 77 | private fun stubBufferooCacheIsExpired(isExpired: Boolean) { 78 | whenever(bufferooCache.isExpired()) 79 | .thenReturn(isExpired) 80 | } 81 | // 82 | 83 | } -------------------------------------------------------------------------------- /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.Flowable 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(Flowable.just(BufferooFactory.makeBufferooEntityList(2))) 45 | val testObserver = bufferooRemote.getBufferoos().test() 46 | testObserver.assertComplete() 47 | } 48 | // 49 | 50 | // 51 | private fun stubBufferooCacheGetBufferoos(single: Flowable>) { 52 | whenever(bufferooRemote.getBufferoos()) 53 | .thenReturn(single) 54 | } 55 | // 56 | 57 | } -------------------------------------------------------------------------------- /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.randomLong 5 | import org.buffer.android.boilerplate.data.test.factory.DataFactory.Factory.randomUuid 6 | import org.buffer.android.boilerplate.domain.model.Bufferoo 7 | 8 | /** 9 | * Factory class for Bufferoo related instances 10 | */ 11 | class BufferooFactory { 12 | 13 | companion object Factory { 14 | 15 | fun makeBufferooEntity(): BufferooEntity { 16 | return BufferooEntity(randomLong(), randomUuid(), randomUuid(), randomUuid()) 17 | } 18 | 19 | fun makeBufferoo(): Bufferoo { 20 | return Bufferoo(randomLong(), randomUuid(), randomUuid(), randomUuid()) 21 | } 22 | 23 | fun makeBufferooEntityList(count: Int): List { 24 | val bufferooEntities = mutableListOf() 25 | repeat(count) { 26 | bufferooEntities.add(makeBufferooEntity()) 27 | } 28 | return bufferooEntities 29 | } 30 | 31 | fun makeBufferooList(count: Int): List { 32 | val bufferoos = mutableListOf() 33 | repeat(count) { 34 | bufferoos.add(makeBufferoo()) 35 | } 36 | return bufferoos 37 | } 38 | 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | 2 | allprojects { 3 | repositories { 4 | jcenter() 5 | } 6 | } 7 | 8 | ext { 9 | //Android 10 | androidBuildToolsVersion = "26.0.2" 11 | androidMinSdkVersion = 15 12 | androidTargetSdkVersion = 26 13 | androidCompileSdkVersion = 26 14 | 15 | //Libraries 16 | kotlinVersion = '1.1.4-3' 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-alpha9-1' 29 | supportLibraryVersion = '26.1.0' 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 | archRuntime: "android.arch.lifecycle:runtime:${roomVersion}", 79 | archExtensions: "android.arch.lifecycle:extensions:${roomVersion}", 80 | archCompiler: "android.arch.lifecycle:compiler:${roomVersion}", 81 | ] 82 | 83 | presentationTestDependencies = [ 84 | junit: "junit:junit:${jUnitVersion}", 85 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 86 | assertj: "org.assertj:assertj-core:${assertJVersion}", 87 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 88 | robolectric: "org.robolectric:robolectric:${robolectricVersion}" 89 | ] 90 | 91 | dataDependencies = [ 92 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 93 | dagger: "com.google.dagger:dagger:${daggerVersion}", 94 | okHttp: "com.squareup.okhttp3:okhttp:${okHttpVersion}", 95 | okHttpLogger: "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}", 96 | gson: "com.google.code.gson:gson:${gsonVersion}", 97 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 98 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 99 | rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", 100 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 101 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 102 | androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", 103 | retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}", 104 | retrofitConverter: "com.squareup.retrofit2:converter-gson:${retrofitVersion}", 105 | retrofitAdapter: "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" 106 | ] 107 | 108 | dataTestDependencies = [ 109 | junit: "junit:junit:${jUnitVersion}", 110 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 111 | assertj: "org.assertj:assertj-core:${assertJVersion}", 112 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 113 | robolectric: "org.robolectric:robolectric:${robolectricVersion}" 114 | ] 115 | 116 | cacheDependencies = [ 117 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 118 | dagger: "com.google.dagger:dagger:${daggerVersion}", 119 | gson: "com.google.code.gson:gson:${gsonVersion}", 120 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 121 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 122 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 123 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 124 | androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", 125 | roomRuntime: "android.arch.persistence.room:runtime:${roomVersion}", 126 | roomCompiler: "android.arch.persistence.room:compiler:${roomVersion}", 127 | roomRxJava: "android.arch.persistence.room:rxjava2:${roomVersion}" 128 | ] 129 | 130 | cacheTestDependencies = [ 131 | junit: "junit:junit:${jUnitVersion}", 132 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 133 | assertj: "org.assertj:assertj-core:${assertJVersion}", 134 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 135 | robolectric: "org.robolectric:robolectric:${robolectricVersion}", 136 | roomTesting: "android.arch.persistence.room:testing:${roomVersion}", 137 | archTesting: "android.arch.core:core-testing:${roomVersion}", 138 | supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", 139 | supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}" 140 | ] 141 | 142 | remoteDependencies = [ 143 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 144 | dagger: "com.google.dagger:dagger:${daggerVersion}", 145 | gson: "com.google.code.gson:gson:${gsonVersion}", 146 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 147 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 148 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 149 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 150 | androidAnnotations: "com.android.support:support-annotations:${androidAnnotationsVersion}", 151 | okHttp: "com.squareup.okhttp3:okhttp:${okHttpVersion}", 152 | okHttpLogger: "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}", 153 | retrofit: "com.squareup.retrofit2:retrofit:${retrofitVersion}", 154 | retrofitConverter: "com.squareup.retrofit2:converter-gson:${retrofitVersion}", 155 | retrofitAdapter: "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}" 156 | ] 157 | 158 | remoteTestDependencies = [ 159 | junit: "junit:junit:${jUnitVersion}", 160 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 161 | assertj: "org.assertj:assertj-core:${assertJVersion}", 162 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 163 | supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", 164 | supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}" 165 | ] 166 | 167 | mobileUiDependencies = [ 168 | daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", 169 | dagger: "com.google.dagger:dagger:${daggerVersion}", 170 | rxKotlin: "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 171 | rxAndroid: "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", 172 | glide: "com.github.bumptech.glide:glide:${glideVersion}", 173 | kotlin: "org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}", 174 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 175 | javaxInject: "javax.inject:javax.inject:${javaxInjectVersion}", 176 | androidAnnotations: "com.android.support:support-annotations:${supportLibraryVersion}", 177 | androidSupportV4: "com.android.support:support-v4:${supportLibraryVersion}", 178 | androidSupportV13: "com.android.support:support-v13:${supportLibraryVersion}", 179 | appCompatV7: "com.android.support:appcompat-v7:${supportLibraryVersion}", 180 | supportRecyclerView:"com.android.support:recyclerview-v7:${supportLibraryVersion}", 181 | supportDesign: "com.android.support:design:${supportLibraryVersion}", 182 | timber: "com.jakewharton.timber:timber:${timberVersion}", 183 | daggerSupport: "com.google.dagger:dagger-android-support:${daggerVersion}", 184 | daggerProcessor: "com.google.dagger:dagger-android-processor:${daggerVersion}", 185 | glassfishAnnotation: "org.glassfish:javax.annotation:${glassfishAnnotationVersion}", 186 | roomRuntime: "android.arch.persistence.room:runtime:${roomVersion}", 187 | roomCompiler: "android.arch.persistence.room:compiler:${roomVersion}", 188 | roomRxJava: "android.arch.persistence.room:rxjava2:${roomVersion}", 189 | ] 190 | 191 | mobileUiTestDependencies = [ 192 | junit: "junit:junit:${jUnitVersion}", 193 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 194 | assertj: "org.assertj:assertj-core:${assertJVersion}", 195 | mockito: "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 196 | supportRunner: "com.android.support.test:runner:${androidSupportRunnerVersion}", 197 | supportRules: "com.android.support.test:rules:${androidSupportRulesVersion}", 198 | mockitoAndroid: "org.mockito:mockito-android:${mockitoAndroidVersion}", 199 | espressoCore: "com.android.support.test.espresso:espresso-core:${espressoVersion}", 200 | espressoIntents: "com.android.support.test.espresso:espresso-intents:${espressoVersion}", 201 | espressoContrib: "com.android.support.test.espresso:espresso-contrib:${espressoVersion}", 202 | androidRunner: "com.android.support.test:runner:${runnerVersion}", 203 | androidRules: "com.android.support.test:rules:${runnerVersion}" 204 | ] 205 | 206 | } -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *iml 3 | *.iml -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | sourceCompatibility = 1.8 4 | targetCompatibility = 1.8 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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/BaseFlowableObserver.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.interactor 2 | 3 | import io.reactivex.SingleObserver 4 | import io.reactivex.disposables.Disposable 5 | 6 | /** 7 | * Default [SingleObserver] base class to define 8 | */ 9 | open class BaseFlowableObserver : SingleObserver { 10 | 11 | override fun onSubscribe(d: Disposable) { } 12 | 13 | override fun onSuccess(t: T) { } 14 | 15 | override fun onError(exception: Throwable) { } 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/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 | } -------------------------------------------------------------------------------- /domain/src/main/java/org/buffer/android/boilerplate/domain/interactor/FlowableUseCase.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.interactor 2 | 3 | import io.reactivex.Flowable 4 | import io.reactivex.Single 5 | import io.reactivex.disposables.CompositeDisposable 6 | import io.reactivex.disposables.Disposable 7 | import io.reactivex.schedulers.Schedulers 8 | import io.reactivex.subscribers.DisposableSubscriber 9 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 10 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 11 | 12 | 13 | 14 | /** 15 | * Abstract class for a UseCase that returns an instance of a [Single]. 16 | */ 17 | abstract class FlowableUseCase constructor( 18 | private val threadExecutor: ThreadExecutor, 19 | private val postExecutionThread: PostExecutionThread) { 20 | 21 | private val disposables = CompositeDisposable() 22 | 23 | /** 24 | * Builds a [Single] which will be used when the current [FlowableUseCase] is executed. 25 | */ 26 | protected abstract fun buildUseCaseObservable(params: Params? = null): Flowable 27 | 28 | /** 29 | * Executes the current use case. 30 | */ 31 | open fun execute(observer: DisposableSubscriber, params: Params? = null) { 32 | val observable = this.buildUseCaseObservable(params) 33 | .subscribeOn(Schedulers.from(threadExecutor)) 34 | .observeOn(postExecutionThread.scheduler) as Flowable 35 | addDisposable(observable.subscribeWith(observer)) 36 | } 37 | 38 | /** 39 | * Dispose from current [CompositeDisposable]. 40 | */ 41 | fun dispose() { 42 | if (!disposables.isDisposed) disposables.dispose() 43 | } 44 | 45 | /** 46 | * Dispose from current [CompositeDisposable]. 47 | */ 48 | private fun addDisposable(disposable: Disposable) { 49 | disposables.add(disposable) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /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.Flowable 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.FlowableUseCase 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 | FlowableUseCase, Void?>(threadExecutor, postExecutionThread) { 18 | 19 | public override fun buildUseCaseObservable(params: Void?): Flowable> { 20 | return bufferooRepository.getBufferoos() 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /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 id: Long, val name: String, val title: String, val avatar: String) -------------------------------------------------------------------------------- /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.Flowable 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(): Flowable> 19 | 20 | } -------------------------------------------------------------------------------- /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.randomLong 5 | import org.buffer.android.boilerplate.domain.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 makeBufferooList(count: Int): List { 15 | val bufferoos = mutableListOf() 16 | repeat(count) { 17 | bufferoos.add(makeBufferoo()) 18 | } 19 | return bufferoos 20 | } 21 | 22 | fun makeBufferoo(): Bufferoo { 23 | return Bufferoo(randomLong(), randomUuid(), randomUuid(), randomUuid()) 24 | } 25 | 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /domain/src/test/java/org/buffer/android/boilerplate/domain/test/factory/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.domain.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 randomInt(): Int { 13 | return ThreadLocalRandom.current().nextInt(0, 1000 + 1) 14 | } 15 | 16 | fun randomLong(): Long { 17 | return randomInt().toLong() 18 | } 19 | 20 | fun randomUuid(): String { 21 | return java.util.UUID.randomUUID().toString() 22 | } 23 | 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /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.Flowable 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(Flowable.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(Flowable.just(bufferoos)) 50 | val testObserver = getBufferoos.buildUseCaseObservable(null).test() 51 | testObserver.assertValue(bufferoos) 52 | } 53 | 54 | private fun stubBufferooRepositoryGetBufferoos(single: Flowable>) { 55 | whenever(mockBufferooRepository.getBufferoos()) 56 | .thenReturn(single) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufferapp/clean-architecture-components-boilerplate/09f746223d7c852ac5ac26fbd9118e4237b55a9b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | compile presentationDependencies.archRuntime 90 | compile presentationDependencies.archExtensions 91 | compile "android.arch.persistence.room:rxjava2:1.0.0-alpha9-1" 92 | kapt presentationDependencies.archCompiler 93 | 94 | testImplementation mobileUiTestDependencies.kotlinJUnit 95 | 96 | kapt mobileUiDependencies.daggerCompiler 97 | kapt mobileUiDependencies.daggerProcessor 98 | compileOnly mobileUiDependencies.glassfishAnnotation 99 | 100 | // Instrumentation test dependencies 101 | androidTestImplementation mobileUiTestDependencies.junit 102 | androidTestImplementation mobileUiTestDependencies.mockito 103 | androidTestImplementation mobileUiTestDependencies.mockitoAndroid 104 | androidTestImplementation (mobileUiTestDependencies.espressoCore) { 105 | exclude group: 'com.android.support', module: 'support-annotations' 106 | } 107 | androidTestImplementation (mobileUiTestDependencies.androidRunner) { 108 | exclude group: 'com.android.support', module: 'support-annotations' 109 | } 110 | androidTestImplementation (mobileUiTestDependencies.androidRules) { 111 | exclude group: 'com.android.support', module: 'support-annotations' 112 | } 113 | androidTestImplementation (mobileUiTestDependencies.espressoIntents) { 114 | exclude group: 'com.android.support', module: 'support-annotations' 115 | } 116 | androidTestImplementation(mobileUiTestDependencies.espressoContrib) { 117 | exclude module: 'appcompat' 118 | exclude module: 'appcompat-v7' 119 | exclude module: 'support-v4' 120 | exclude module: 'support-v13' 121 | exclude module: 'support-annotations' 122 | exclude module: 'recyclerview-v7' 123 | exclude module: 'design' 124 | } 125 | 126 | kaptTest mobileUiDependencies.daggerCompiler 127 | kaptAndroidTest mobileUiDependencies.daggerCompiler 128 | } 129 | -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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.Flowable 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.util.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(Flowable.just(BufferooFactory.makeBufferooList(2))) 31 | activity.launchActivity(null) 32 | } 33 | 34 | @Test 35 | fun bufferoosDisplay() { 36 | val bufferoos = BufferooFactory.makeBufferooList(1) 37 | stubBufferooRepositoryGetBufferoos(Flowable.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(Flowable.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: Flowable>) { 63 | whenever(TestApplication.appComponent().bufferooRepository().getBufferoos()) 64 | .thenReturn(single) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /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.* 11 | import org.buffer.android.boilerplate.ui.injection.scopes.PerApplication 12 | import org.buffer.android.boilerplate.ui.test.TestApplication 13 | import javax.inject.Singleton 14 | 15 | @Singleton 16 | @Component(modules = arrayOf( 17 | TestApplicationModule::class, 18 | AndroidSupportInjectionModule::class, 19 | TestCacheModule::class, 20 | TestRemoteModule::class, 21 | TestDataModule::class, 22 | PresentationModule::class, 23 | UiModule::class)) 24 | interface TestApplicationComponent : ApplicationComponent { 25 | 26 | fun bufferooRepository(): BufferooRepository 27 | 28 | fun postExecutionThread(): PostExecutionThread 29 | 30 | fun inject(application: TestApplication) 31 | 32 | @Component.Builder 33 | interface Builder { 34 | @BindsInstance 35 | fun application(application: Application): TestApplicationComponent.Builder 36 | 37 | fun build(): TestApplicationComponent 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /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.Binds 7 | import dagger.Module 8 | import dagger.Provides 9 | import org.buffer.android.boilerplate.cache.PreferencesHelper 10 | import org.buffer.android.boilerplate.data.executor.JobExecutor 11 | import org.buffer.android.boilerplate.data.repository.BufferooCache 12 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 13 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 14 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 15 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 16 | import org.buffer.android.boilerplate.remote.BufferooService 17 | import org.buffer.android.boilerplate.ui.UiThread 18 | import org.buffer.android.boilerplate.ui.injection.scopes.PerApplication 19 | 20 | @Module 21 | abstract class TestApplicationModule { 22 | 23 | @Binds 24 | abstract fun bindContext(application: Application): Context 25 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/injection/module/TestCacheModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import android.app.Application 4 | import android.arch.persistence.room.Room 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.cache.db.BufferoosDatabase 10 | import org.buffer.android.boilerplate.data.repository.BufferooCache 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | object TestCacheModule { 15 | 16 | @Provides 17 | @JvmStatic 18 | fun provideBufferoosDatabase(application: Application): BufferoosDatabase { 19 | return Room.databaseBuilder( 20 | application.applicationContext, 21 | BufferoosDatabase::class.java, "bufferoos.db") 22 | .build() 23 | } 24 | 25 | @Provides 26 | @JvmStatic 27 | @Singleton 28 | fun provideBufferooCache(): BufferooCache { 29 | return mock() 30 | } 31 | 32 | @Provides 33 | @JvmStatic 34 | @Singleton 35 | fun providePreferencesHelper(): PreferencesHelper { 36 | return mock() 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/injection/module/TestDataModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import com.nhaarman.mockito_kotlin.mock 4 | import dagger.Binds 5 | import dagger.Module 6 | import dagger.Provides 7 | import org.buffer.android.boilerplate.data.executor.JobExecutor 8 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 9 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | abstract class TestDataModule { 14 | 15 | @Module 16 | companion object { 17 | 18 | @Provides 19 | @JvmStatic 20 | @Singleton 21 | fun provideBufferooRepository(): BufferooRepository { 22 | return mock() 23 | } 24 | } 25 | 26 | @Binds 27 | abstract fun bindThreadExecutor(jobExecutor: JobExecutor): ThreadExecutor 28 | } 29 | -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/injection/module/TestRemoteModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import com.nhaarman.mockito_kotlin.mock 4 | import dagger.Module 5 | import dagger.Provides 6 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 7 | import org.buffer.android.boilerplate.remote.BufferooService 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | object TestRemoteModule { 12 | 13 | @Provides 14 | @JvmStatic 15 | @Singleton 16 | fun provideBufferooRemote(): BufferooRemote { 17 | return mock() 18 | } 19 | 20 | @Provides 21 | @JvmStatic 22 | @Singleton 23 | fun provideBufferooService(): BufferooService { 24 | return mock() 25 | } 26 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/test/util/BufferooFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test.util 2 | 3 | import org.buffer.android.boilerplate.domain.model.Bufferoo 4 | import org.buffer.android.boilerplate.presentation.model.BufferooView 5 | 6 | /** 7 | * Factory class for Bufferoo related instances 8 | */ 9 | object BufferooFactory { 10 | 11 | fun makeBufferooView(): BufferooView { 12 | return BufferooView(DataFactory.randomUuid(), DataFactory.randomUuid(), 13 | DataFactory.randomUuid()) 14 | } 15 | 16 | fun makeBufferooList(count: Int): List { 17 | val bufferoos = mutableListOf() 18 | repeat(count) { 19 | bufferoos.add(BufferooFactory.makeBufferooModel()) 20 | } 21 | return bufferoos 22 | } 23 | 24 | fun makeBufferooModel(): Bufferoo { 25 | return Bufferoo(DataFactory.randomLong(), DataFactory.randomUuid(), 26 | DataFactory.randomUuid(), DataFactory.randomUuid()) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /mobile-ui/src/androidTest/java/org/buffer/android/boilerplate/ui/test/util/DataFactory.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.test.util 2 | 3 | import java.util.concurrent.ThreadLocalRandom 4 | 5 | /** 6 | * Factory class for data instances 7 | */ 8 | object DataFactory { 9 | 10 | fun randomUuid(): String { 11 | return java.util.UUID.randomUUID().toString() 12 | } 13 | 14 | fun randomInt(): Int { 15 | return ThreadLocalRandom.current().nextInt(0, 1000 + 1) 16 | } 17 | 18 | fun randomLong(): Long { 19 | return randomInt().toLong() 20 | } 21 | 22 | fun randomBoolean(): Boolean { 23 | return Math.random() < 0.5 24 | } 25 | 26 | fun makeStringList(count: Int): List { 27 | val items: MutableList = mutableListOf() 28 | repeat(count) { 29 | items.add(randomUuid()) 30 | } 31 | return items 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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.arch.lifecycle.Observer 4 | import android.arch.lifecycle.ViewModelProviders 5 | import android.os.Bundle 6 | import android.support.v7.app.AppCompatActivity 7 | import android.support.v7.widget.LinearLayoutManager 8 | import android.view.View 9 | import dagger.android.AndroidInjection 10 | import kotlinx.android.synthetic.main.activity_browse.* 11 | import org.buffer.android.boilerplate.presentation.ViewModelFactory 12 | import org.buffer.android.boilerplate.presentation.browse.BrowseBufferoosViewModel 13 | import org.buffer.android.boilerplate.presentation.data.ResourceState 14 | import org.buffer.android.boilerplate.presentation.model.BufferooView 15 | import org.buffer.android.boilerplate.ui.R 16 | import org.buffer.android.boilerplate.ui.mapper.BufferooMapper 17 | import org.buffer.android.boilerplate.ui.widget.empty.EmptyListener 18 | import org.buffer.android.boilerplate.ui.widget.error.ErrorListener 19 | import javax.inject.Inject 20 | 21 | class BrowseActivity : AppCompatActivity() { 22 | 23 | @Inject lateinit var browseAdapter: BrowseAdapter 24 | @Inject lateinit var mapper: BufferooMapper 25 | @Inject lateinit var viewModelFactory: ViewModelFactory 26 | private lateinit var browseBufferoosViewModel: BrowseBufferoosViewModel 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_browse) 31 | AndroidInjection.inject(this) 32 | 33 | browseBufferoosViewModel = ViewModelProviders.of(this, viewModelFactory) 34 | .get(BrowseBufferoosViewModel::class.java) 35 | 36 | setupBrowseRecycler() 37 | setupViewListeners() 38 | } 39 | 40 | override fun onStart() { 41 | super.onStart() 42 | browseBufferoosViewModel.getBufferoos().observe(this, Observer { 43 | if (it != null) this.handleDataState(it.status, it.data, it.message) 44 | }) 45 | } 46 | 47 | private fun setupBrowseRecycler() { 48 | recycler_browse.layoutManager = LinearLayoutManager(this) 49 | recycler_browse.adapter = browseAdapter 50 | } 51 | 52 | private fun handleDataState(resourceState: ResourceState, data: List?, 53 | message: String?) { 54 | when (resourceState) { 55 | ResourceState.LOADING -> setupScreenForLoadingState() 56 | ResourceState.SUCCESS -> setupScreenForSuccess(data) 57 | ResourceState.ERROR -> setupScreenForError(message) 58 | } 59 | } 60 | 61 | private fun setupScreenForLoadingState() { 62 | progress.visibility = View.VISIBLE 63 | recycler_browse.visibility = View.GONE 64 | view_empty.visibility = View.GONE 65 | view_error.visibility = View.GONE 66 | } 67 | 68 | private fun setupScreenForSuccess(data: List?) { 69 | view_error.visibility = View.GONE 70 | progress.visibility = View.GONE 71 | if (data != null && data.isNotEmpty()) { 72 | updateListView(data) 73 | recycler_browse.visibility = View.VISIBLE 74 | } else { 75 | view_empty.visibility = View.VISIBLE 76 | } 77 | } 78 | 79 | private fun updateListView(bufferoos: List) { 80 | browseAdapter.bufferoos = bufferoos.map { mapper.mapToViewModel(it) } 81 | browseAdapter.notifyDataSetChanged() 82 | } 83 | 84 | private fun setupScreenForError(message: String?) { 85 | progress.visibility = View.GONE 86 | recycler_browse.visibility = View.GONE 87 | view_empty.visibility = View.GONE 88 | view_error.visibility = View.VISIBLE 89 | } 90 | 91 | private fun setupViewListeners() { 92 | view_empty.emptyListener = emptyListener 93 | view_error.errorListener = errorListener 94 | } 95 | 96 | private val emptyListener = object : EmptyListener { 97 | override fun onCheckAgainClicked() { 98 | browseBufferoosViewModel.fetchBufferoos() 99 | } 100 | } 101 | 102 | private val errorListener = object : ErrorListener { 103 | override fun onTryAgainClicked() { 104 | browseBufferoosViewModel.fetchBufferoos() 105 | } 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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.* 9 | import javax.inject.Singleton 10 | 11 | @Singleton 12 | @Component(modules = arrayOf( 13 | ApplicationModule::class, 14 | AndroidSupportInjectionModule::class, 15 | CacheModule::class, 16 | DataModule::class, 17 | DomainModule::class, 18 | PresentationModule::class, 19 | RemoteModule::class, 20 | UiModule::class) 21 | ) 22 | interface ApplicationComponent { 23 | 24 | @Component.Builder 25 | interface Builder { 26 | @BindsInstance 27 | fun application(application: Application): Builder 28 | 29 | fun build(): ApplicationComponent 30 | } 31 | 32 | fun inject(app: BufferooApplication) 33 | 34 | } 35 | -------------------------------------------------------------------------------- /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.arch.lifecycle.ViewModelProvider 5 | import android.arch.persistence.room.Room 6 | import android.content.Context 7 | import dagger.Binds 8 | import dagger.Module 9 | import dagger.Provides 10 | import org.buffer.android.boilerplate.cache.BufferooCacheImpl 11 | import org.buffer.android.boilerplate.cache.PreferencesHelper 12 | import org.buffer.android.boilerplate.cache.db.BufferoosDatabase 13 | import org.buffer.android.boilerplate.cache.mapper.BufferooEntityMapper 14 | import org.buffer.android.boilerplate.data.BufferooDataRepository 15 | import org.buffer.android.boilerplate.data.executor.JobExecutor 16 | import org.buffer.android.boilerplate.data.mapper.BufferooMapper 17 | import org.buffer.android.boilerplate.data.repository.BufferooCache 18 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 19 | import org.buffer.android.boilerplate.data.source.BufferooDataStoreFactory 20 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 21 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 22 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 23 | import org.buffer.android.boilerplate.remote.BufferooRemoteImpl 24 | import org.buffer.android.boilerplate.remote.BufferooService 25 | import org.buffer.android.boilerplate.remote.BufferooServiceFactory 26 | import org.buffer.android.boilerplate.ui.BuildConfig 27 | import org.buffer.android.boilerplate.ui.UiThread 28 | import org.buffer.android.boilerplate.ui.injection.scopes.PerApplication 29 | 30 | /** 31 | * Module used to provide dependencies at an application-level. 32 | */ 33 | @Module 34 | abstract class ApplicationModule { 35 | 36 | @Binds 37 | abstract fun bindContext(application: Application): Context 38 | } 39 | -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/CacheModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import android.app.Application 4 | import android.arch.persistence.room.Room 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.Provides 8 | import org.buffer.android.boilerplate.cache.BufferooCacheImpl 9 | import org.buffer.android.boilerplate.cache.db.BufferoosDatabase 10 | import org.buffer.android.boilerplate.data.repository.BufferooCache 11 | 12 | /** 13 | * Module that provides all dependencies from the cache package/layer. 14 | */ 15 | @Module 16 | abstract class CacheModule { 17 | 18 | /** 19 | * This companion object annotated as a module is necessary in order to provide dependencies 20 | * statically in case the wrapping module is an abstract class (to use binding) 21 | */ 22 | @Module 23 | companion object { 24 | 25 | @Provides 26 | @JvmStatic 27 | fun provideBufferoosDatabase(application: Application): BufferoosDatabase { 28 | return Room.databaseBuilder( 29 | application.applicationContext, 30 | BufferoosDatabase::class.java, "bufferoos.db") 31 | .build() 32 | } 33 | } 34 | 35 | @Binds 36 | abstract fun bindBufferooCache(bufferooCacheImpl: BufferooCacheImpl): BufferooCache 37 | } 38 | -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/DataModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import dagger.Binds 4 | import dagger.Module 5 | import org.buffer.android.boilerplate.data.BufferooDataRepository 6 | import org.buffer.android.boilerplate.data.executor.JobExecutor 7 | import org.buffer.android.boilerplate.domain.executor.ThreadExecutor 8 | import org.buffer.android.boilerplate.domain.repository.BufferooRepository 9 | 10 | @Module 11 | abstract class DataModule { 12 | 13 | @Binds 14 | abstract fun bindBufferooRepository(bufferooDataRepository: BufferooDataRepository): BufferooRepository 15 | 16 | @Binds 17 | abstract fun bindThreadExecutor(jobExecutor: JobExecutor): ThreadExecutor 18 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/DomainModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import dagger.Module 4 | 5 | /** 6 | * Module that provides all dependencies from the domain package/layer. 7 | */ 8 | @Module 9 | abstract class DomainModule -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/PresentationModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import android.arch.lifecycle.ViewModel 4 | import android.arch.lifecycle.ViewModelProvider 5 | import dagger.Binds 6 | import dagger.MapKey 7 | import dagger.Module 8 | import dagger.multibindings.IntoMap 9 | import org.buffer.android.boilerplate.presentation.ViewModelFactory 10 | import org.buffer.android.boilerplate.presentation.browse.BrowseBufferoosViewModel 11 | import kotlin.reflect.KClass 12 | 13 | /** 14 | * Annotation class to identify view models by classname. 15 | */ 16 | @MustBeDocumented 17 | @Target(AnnotationTarget.FUNCTION) 18 | @Retention(AnnotationRetention.RUNTIME) 19 | @MapKey 20 | annotation class ViewModelKey(val value: KClass) 21 | 22 | /** 23 | * Module that provides all dependencies from the presentation package/layer. 24 | */ 25 | @Module 26 | abstract class PresentationModule { 27 | @Binds 28 | @IntoMap 29 | @ViewModelKey(BrowseBufferoosViewModel::class) 30 | abstract fun bindBrowseBufferoosViewModel(viewModel: BrowseBufferoosViewModel): ViewModel 31 | 32 | @Binds 33 | abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory 34 | } 35 | 36 | -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/RemoteModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import dagger.Binds 4 | import dagger.Module 5 | import dagger.Provides 6 | import org.buffer.android.boilerplate.data.repository.BufferooRemote 7 | import org.buffer.android.boilerplate.remote.BufferooRemoteImpl 8 | import org.buffer.android.boilerplate.remote.BufferooService 9 | import org.buffer.android.boilerplate.remote.BufferooServiceFactory 10 | import org.buffer.android.boilerplate.ui.BuildConfig 11 | 12 | /** 13 | * Module that provides all dependencies from the repository package/layer. 14 | */ 15 | @Module 16 | abstract class RemoteModule { 17 | 18 | /** 19 | * This companion object annotated as a module is necessary in order to provide dependencies 20 | * statically in case the wrapping module is an abstract class (to use binding) 21 | */ 22 | @Module 23 | companion object { 24 | @Provides 25 | @JvmStatic 26 | fun provideBufferooService(): BufferooService { 27 | return BufferooServiceFactory.makeBuffeoorService(BuildConfig.DEBUG) 28 | } 29 | } 30 | 31 | @Binds 32 | abstract fun bindBufferooRemote(bufferooRemoteImpl: BufferooRemoteImpl): BufferooRemote 33 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/injection/module/UiModule.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.injection.module 2 | 3 | import dagger.Binds 4 | import dagger.Module 5 | import dagger.android.ContributesAndroidInjector 6 | import org.buffer.android.boilerplate.domain.executor.PostExecutionThread 7 | import org.buffer.android.boilerplate.ui.UiThread 8 | import org.buffer.android.boilerplate.ui.browse.BrowseActivity 9 | 10 | /** 11 | * Module that provides all dependencies from the mobile-ui package/layer and injects all activities. 12 | */ 13 | @Module 14 | abstract class UiModule { 15 | 16 | @Binds 17 | abstract fun bindPostExecutionThread(uiThread: UiThread): PostExecutionThread 18 | 19 | @ContributesAndroidInjector 20 | abstract fun contributeMainActivity(): BrowseActivity 21 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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/java/org/buffer/android/boilerplate/ui/widget/empty/EmptyListener.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.widget.empty 2 | 3 | interface EmptyListener { 4 | 5 | fun onCheckAgainClicked() 6 | 7 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/widget/empty/EmptyView.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.widget.empty 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | import android.widget.RelativeLayout 7 | import kotlinx.android.synthetic.main.view_empty.view.* 8 | import org.buffer.android.boilerplate.ui.R 9 | 10 | /** 11 | * Widget used to display an empty state to the user 12 | */ 13 | class EmptyView: RelativeLayout { 14 | 15 | var emptyListener: EmptyListener? = null 16 | 17 | constructor(context: Context) : super(context) { 18 | init() 19 | } 20 | 21 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 22 | init() 23 | } 24 | 25 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : 26 | super(context, attrs, defStyleAttr) { 27 | init() 28 | } 29 | 30 | private fun init() { 31 | LayoutInflater.from(context).inflate(R.layout.view_empty, this) 32 | button_check_again.setOnClickListener { emptyListener?.onCheckAgainClicked() } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/widget/error/ErrorListener.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.widget.error 2 | 3 | interface ErrorListener { 4 | 5 | fun onTryAgainClicked() 6 | 7 | } -------------------------------------------------------------------------------- /mobile-ui/src/main/java/org/buffer/android/boilerplate/ui/widget/error/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package org.buffer.android.boilerplate.ui.widget.error 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | import android.widget.RelativeLayout 7 | import kotlinx.android.synthetic.main.view_error.view.* 8 | import org.buffer.android.boilerplate.ui.R 9 | 10 | /** 11 | * Widget used to display an empty state to the user 12 | */ 13 | class ErrorView : RelativeLayout { 14 | 15 | var errorListener: ErrorListener? = null 16 | 17 | constructor(context: Context) : super(context) { 18 | init() 19 | } 20 | 21 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 22 | init() 23 | } 24 | 25 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : 26 | super(context, attrs, defStyleAttr) { 27 | init() 28 | } 29 | 30 | private fun init() { 31 | LayoutInflater.from(context).inflate(R.layout.view_error, this) 32 | button_try_again.setOnClickListener { errorListener?.onTryAgainClicked() } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mobile-ui/src/main/res/layout/activity_browse.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 20 | 21 | 26 | 27 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /mobile-ui/src/main/res/layout/item_bufferoo.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 25 | 26 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /mobile-ui/src/main/res/layout/view_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 |