()
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/base/BaseView.java:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.base;
2 |
3 | import android.support.annotation.NonNull;
4 |
5 | public interface BaseView> {
6 | void attachPresenter(@NonNull P presenter);
7 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Oct 10 14:15:31 CEST 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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/base/BasePresenter.java:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.base;
2 |
3 | import android.support.annotation.NonNull;
4 |
5 | public interface BasePresenter extends BasePresenterLifecycle {
6 | void attachView(@NonNull V view);
7 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/executor/SchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.executor
2 |
3 |
4 | import io.reactivex.Scheduler
5 |
6 | interface SchedulerProvider {
7 | fun ui(): Scheduler
8 | fun io(): Scheduler
9 | fun computation(): Scheduler
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/bus/event/BaseEventBus.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.bus.event
2 |
3 | import com.petrulak.cleankotlin.platform.bus.BaseBus
4 | import com.petrulak.cleankotlin.platform.bus.event.events.BaseEvent
5 |
6 |
7 | open class BaseEventBus : BaseBus>()
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/RemoteSource.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source
2 |
3 | import com.petrulak.cleankotlin.data.source.remote.model.WeatherDto
4 | import io.reactivex.Single
5 |
6 | interface RemoteSource {
7 | fun getWeatherForCity(city: String): Single
8 | }
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/definition/GetWeatherUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor.definition
2 |
3 | import com.petrulak.cleankotlin.domain.model.Weather
4 | import io.reactivex.Flowable
5 |
6 | interface GetWeatherUseCase {
7 |
8 | fun execute(city: String): Flowable
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example1/ExampleParcelable.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example1
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Parcelable
5 | import kotlinx.android.parcel.Parcelize
6 |
7 | @Parcelize
8 | @SuppressLint("ParcelCreator")
9 | class ExampleParcelable(val message: String) : Parcelable
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/definition/GetWeatherLocallyUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor.definition
2 |
3 | import com.petrulak.cleankotlin.domain.model.Weather
4 | import io.reactivex.Flowable
5 |
6 | interface GetWeatherLocallyUseCase {
7 |
8 | fun execute(city: String): Flowable
9 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/definition/GetWeatherRemotelyUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor.definition
2 |
3 | import com.petrulak.cleankotlin.domain.model.Weather
4 | import io.reactivex.Single
5 |
6 | interface GetWeatherRemotelyUseCase {
7 |
8 | fun execute(city: String): Single
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/extensions/Global.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.extensions
2 |
3 | /**
4 | * Since we don't need our objects to be thread safe most of the time, we can use `LazyThreadSafetyMode.NONE` which has lower
5 | * overhead.
6 | */
7 | fun lazyFast(operation: () -> T): Lazy = lazy(LazyThreadSafetyMode.NONE) {
8 | operation()
9 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/local/model/WeatherEntity.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.local.model
2 |
3 | import android.arch.persistence.room.Entity
4 | import android.arch.persistence.room.PrimaryKey
5 |
6 | @Entity
7 | data class WeatherEntity(
8 | @PrimaryKey()
9 | val uid: Long,
10 | val name: String,
11 | val visibility: Int
12 | )
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/bus/event/events/FragmentSyncEvent.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.bus.event.events
2 |
3 | import com.petrulak.cleankotlin.platform.bus.event.BaseEventBus
4 |
5 | class FragmentSyncEvent : BaseEventBus() {
6 |
7 | companion object {
8 | val ACTION_SYNC_ON = "SYNC_ON"
9 | val ACTION_SYNC_OFF = "SYNC_OFF"
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/scope/ViewScope.java:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.scope;
2 |
3 | import java.lang.annotation.Retention;
4 | import java.lang.annotation.RetentionPolicy;
5 |
6 | import javax.inject.Scope;
7 |
8 | /**
9 | * A component that has this scope will exist as long as a view does.
10 | */
11 | @Scope
12 | @Retention(RetentionPolicy.RUNTIME)
13 | public @interface ViewScope {
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/analytics/AnalyticsManager.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.analytics
2 |
3 | interface AnalyticsManager {
4 |
5 | fun setUser(email: String)
6 |
7 | fun setCustomAttributes(attributes: HashMap)
8 |
9 | fun trackEvent(event: String)
10 |
11 | fun trackEvent(event: String, map: HashMap)
12 |
13 | fun clear()
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/bus/event/EventBus.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.bus.event
2 |
3 | import com.petrulak.cleankotlin.platform.bus.event.events.FragmentSyncEvent
4 | import com.petrulak.cleankotlin.platform.bus.event.events.WeatherDummyEvent
5 |
6 |
7 | class EventBus {
8 | val fragmentSyncEvent = FragmentSyncEvent()
9 | val weatherDummyEvent = WeatherDummyEvent()
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/bus/event/events/WeatherDummyEvent.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.bus.event.events
2 |
3 | import com.petrulak.cleankotlin.domain.model.Weather
4 | import com.petrulak.cleankotlin.platform.bus.event.BaseEventBus
5 |
6 | class WeatherDummyEvent : BaseEventBus() {
7 |
8 | companion object {
9 | val ACTION_HELLO = "ACTION_HELLO"
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/base/BasePresenterImpl.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.base
2 |
3 | import io.reactivex.disposables.CompositeDisposable
4 |
5 |
6 | open class BasePresenterImpl : BasePresenterLifecycle {
7 |
8 | val disposables = CompositeDisposable()
9 |
10 | override fun start() {
11 | }
12 |
13 | override fun stop() {
14 | disposables.clear()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example2/ViewState.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example2
2 |
3 | import com.petrulak.cleankotlin.domain.model.Weather
4 |
5 |
6 | sealed class ViewState {
7 | class Init : ViewState()
8 | class Loading : ViewState()
9 | class Success(val item: Weather) : ViewState()
10 | class LoadingFinished : ViewState()
11 | class Error(val error: Throwable) : ViewState()
12 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/remote/WeatherApiClient.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.remote
2 |
3 | import com.petrulak.cleankotlin.data.source.remote.model.WeatherDto
4 | import io.reactivex.Single
5 | import retrofit2.http.GET
6 | import retrofit2.http.Query
7 |
8 |
9 | interface WeatherApiClient {
10 | @GET("data/2.5/weather")
11 | fun getWeatherForCity(@Query("q") city: String): Single
12 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/LocalSource.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source
2 |
3 | import com.petrulak.cleankotlin.data.source.local.model.WeatherEntity
4 | import com.petrulak.cleankotlin.domain.model.Weather
5 | import io.reactivex.Completable
6 | import io.reactivex.Flowable
7 |
8 | interface LocalSource {
9 | fun getWeatherForCity(name: String): Flowable
10 | fun save(weather: Weather): Completable
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/bus/BaseBus.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.bus
2 |
3 | import com.jakewharton.rxrelay2.PublishRelay
4 | import io.reactivex.BackpressureStrategy
5 | import io.reactivex.Flowable
6 |
7 |
8 | open class BaseBus {
9 |
10 | protected val bus = PublishRelay.create()
11 |
12 | val flowable: Flowable get() = bus.toFlowable(BackpressureStrategy.LATEST)
13 |
14 | fun emmit(event: T) = bus.accept(event)
15 | }
--------------------------------------------------------------------------------
/config.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | config = [
3 | package : "com.petrulak.cleankotlin",
4 | buildToolsVersion : "26.0.2",
5 | compileSdkVersion : 26,
6 | minSdkVersion : 24,
7 | targetSdkVersion : 25,
8 | versionCode : 1,
9 | versionName : "1.0",
10 | testInstrumentationRunner: "com.petrulak.cleankotlin.MockAppTestRunner"
11 | ]
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/bus/event/events/BaseEvent.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.bus.event.events
2 |
3 | open class BaseEvent {
4 |
5 | val eventType: String
6 | val payload: T?
7 |
8 | constructor(eventType: String, payload: T) {
9 | this.eventType = eventType
10 | this.payload = payload
11 | }
12 |
13 | constructor(eventType: String) {
14 | this.eventType = eventType
15 | payload = null
16 | }
17 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/local/dao/BaseDao.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.local.dao
2 |
3 | import android.arch.persistence.room.Delete
4 | import android.arch.persistence.room.Insert
5 | import android.arch.persistence.room.Update
6 |
7 |
8 | interface BaseDao {
9 |
10 | @Insert
11 | fun insert(obj: T)
12 |
13 | @Insert
14 | fun insert(vararg obj: T)
15 |
16 | @Update
17 | fun update(obj: T)
18 |
19 | @Delete
20 | fun delete(obj: T)
21 | }
--------------------------------------------------------------------------------
/data/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | data
3 |
4 | hr
5 | min
6 | hrs
7 |
8 |
9 | - %s hr
10 | - %s hrs
11 |
12 |
13 |
14 | -
15 | - %s min
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/local/WeatherDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.local
2 |
3 | import android.arch.persistence.room.Database
4 | import android.arch.persistence.room.RoomDatabase
5 | import com.petrulak.cleankotlin.data.source.local.dao.WeatherDao
6 | import com.petrulak.cleankotlin.data.source.local.model.WeatherEntity
7 |
8 |
9 | @Database(entities = arrayOf(WeatherEntity::class), version = 1)
10 | abstract class WeatherDatabase : RoomDatabase() {
11 | abstract fun weatherDao(): WeatherDao
12 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/di/component/ApplicationMockComponent.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.component
2 |
3 | import com.petrulak.cleankotlin.di.module.ApplicationMockModule
4 | import com.petrulak.cleankotlin.di.module.InteractorMockModule
5 | import dagger.Component
6 | import javax.inject.Singleton
7 |
8 | @Singleton
9 | @Component(
10 | modules = arrayOf(
11 | ApplicationMockModule::class,
12 | InteractorMockModule::class
13 | )
14 | )
15 | interface ApplicationMockComponent : ApplicationComponent {
16 |
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example2/Example2Contract.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example2
2 |
3 | import com.petrulak.cleankotlin.ui.base.BasePresenter
4 | import com.petrulak.cleankotlin.ui.base.BaseView
5 |
6 | interface Example2Contract {
7 | interface View : BaseView {
8 | override fun attachPresenter(presenter: Presenter)
9 | var viewState: ViewState
10 | }
11 |
12 | interface Presenter : BasePresenter {
13 | override fun attachView(view: View)
14 | fun refresh(city: String)
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/navigation/Navigator.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.navigation
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.support.v7.app.AppCompatActivity
6 | import com.petrulak.cleankotlin.ui.base.BaseActivity
7 |
8 | class Navigator {
9 |
10 | fun navigate(source: BaseActivity, target: Class, bundle: Bundle? = null) {
11 | val intent = Intent(source, target)
12 | bundle?.let { intent.putExtras(it) }
13 | source.startActivity(intent)
14 | }
15 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/repository/WeatherRepository.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.repository
2 |
3 | import com.petrulak.cleankotlin.domain.model.Weather
4 | import io.reactivex.Completable
5 | import io.reactivex.Flowable
6 | import io.reactivex.Single
7 |
8 | interface WeatherRepository {
9 | fun getWeatherForCity(city: String): Flowable
10 | fun getWeatherForCityRemotely(city: String): Single
11 | fun getWeatherForCityLocally(name: String): Flowable
12 | fun save(weather: Weather): Completable
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/MockAppTestRunner.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin
2 |
3 |
4 | import android.app.Application
5 | import android.content.Context
6 | import android.support.test.runner.AndroidJUnitRunner
7 |
8 | class MockAppTestRunner : AndroidJUnitRunner() {
9 | @Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class)
10 | override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
11 | return super.newApplication(cl, MockApp::class.java.name, context)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/logging/ErrorReportingTree.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.logging
2 |
3 | import android.util.Log
4 | import timber.log.Timber
5 |
6 | class ErrorReportingTree : Timber.Tree() {
7 |
8 | override fun log(priority: Int, tag: String?, message: String?, t: Throwable?) {
9 | if (priority == Log.VERBOSE || priority == Log.DEBUG) {
10 | return
11 | }
12 |
13 | // Crashlytics.log(priority, tag, message)
14 |
15 | if (t != null) {
16 | // Crashlytics.logException(t)
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 |
4 | def conf = rootProject.ext.config
5 |
6 | android {
7 |
8 | compileSdkVersion conf.compileSdkVersion
9 | buildToolsVersion conf.buildToolsVersion
10 |
11 | defaultConfig {
12 | minSdkVersion conf.minSdkVersion
13 | targetSdkVersion conf.targetSdkVersion
14 | versionCode conf.versionCode
15 | }
16 | }
17 |
18 | dependencies {
19 | compile Deps.rxJava
20 | compile Deps.rxAndroid
21 | compile Deps.dagger
22 | compile Deps.timber
23 | compile Deps.kotlin_stdLib
24 | }
25 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/executor/SchedulerProviderImpl.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.executor
2 |
3 | import io.reactivex.Scheduler
4 | import io.reactivex.android.schedulers.AndroidSchedulers
5 | import io.reactivex.schedulers.Schedulers
6 |
7 | class SchedulerProviderImpl : SchedulerProvider {
8 |
9 | override fun ui(): Scheduler {
10 | return AndroidSchedulers.mainThread()
11 | }
12 |
13 | override fun io(): Scheduler {
14 | return Schedulers.io()
15 | }
16 |
17 | override fun computation(): Scheduler {
18 | return Schedulers.computation()
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example1/fragment/Example1Contract.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example1.fragment
2 |
3 | import com.petrulak.cleankotlin.domain.model.Weather
4 | import com.petrulak.cleankotlin.ui.base.BasePresenter
5 | import com.petrulak.cleankotlin.ui.base.BaseView
6 |
7 | interface Example1Contract {
8 | interface View : BaseView {
9 | override fun attachPresenter(presenter: Presenter)
10 | fun showWeather(data: Weather)
11 | }
12 |
13 | interface Presenter : BasePresenter {
14 | override fun attachView(view: View)
15 | fun refresh()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example3/fragment/Example3Contract.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example3.fragment
2 |
3 | import com.petrulak.cleankotlin.domain.model.Weather
4 | import com.petrulak.cleankotlin.ui.base.BasePresenter
5 | import com.petrulak.cleankotlin.ui.base.BaseView
6 |
7 | interface Example3Contract {
8 | interface View : BaseView {
9 | override fun attachPresenter(presenter: Presenter)
10 | fun showWeather(data: Weather)
11 | }
12 |
13 | interface Presenter : BasePresenter {
14 | override fun attachView(view: View)
15 | fun refresh()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: android
3 | android:
4 | components:
5 | - tools
6 | - platform-tools
7 |
8 | # The SDK version used to compile your project
9 | - android-26
10 |
11 | # The BuildTools version used by your project
12 | - build-tools-26.0.2
13 |
14 | # Support library
15 | - extra-android-support
16 | - extra-android-m2repository
17 | - extra-google-m2repository
18 | before_script:
19 | - echo no | android create avd --force -n test -t android-26 --abi armeabi-v7a
20 | - emulator -avd test -no-skin -no-audio -no-window &
21 | - android-wait-for-emulator
22 | - adb shell input keyevent 82 &
23 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/MockApp.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin
2 |
3 | import com.petrulak.cleankotlin.di.component.ApplicationComponent
4 | import com.petrulak.cleankotlin.di.component.DaggerApplicationMockComponent
5 | import com.petrulak.cleankotlin.di.module.ApplicationMockModule
6 | import com.petrulak.cleankotlin.di.module.InteractorMockModule
7 |
8 | class MockApp : App() {
9 |
10 | override fun initializeAppComponent(): ApplicationComponent {
11 | return DaggerApplicationMockComponent.builder()
12 | .applicationMockModule(ApplicationMockModule(this))
13 | .interactorMockModule(InteractorMockModule())
14 | .build()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/di/module/PresenterMockModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import com.petrulak.cleankotlin.di.scope.ViewScope
4 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
5 | import com.petrulak.cleankotlin.ui.example2.Example2Contract
6 | import com.petrulak.cleankotlin.ui.example2.Example2Presenter
7 | import dagger.Module
8 | import dagger.Provides
9 |
10 | @Module
11 | class PresenterMockModule {
12 |
13 | @ViewScope
14 | @Provides
15 | internal fun example2Presenter(remotelyUseCaseGet: GetWeatherRemotelyUseCase): Example2Contract.Presenter {
16 | return Example2Presenter(remotelyUseCaseGet)
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/di/component/ViewMockComponent.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.component
2 |
3 | import com.petrulak.cleankotlin.di.module.PresenterMockModule
4 | import com.petrulak.cleankotlin.di.scope.ViewScope
5 | import com.petrulak.cleankotlin.ui.example2.Example2ActivityTest
6 | import com.petrulak.cleankotlin.ui.example2.Example2PresenterTest
7 | import dagger.Component
8 |
9 | @ViewScope
10 | @Component(
11 | dependencies = arrayOf(
12 | ApplicationMockComponent::class
13 | ),
14 | modules = arrayOf(PresenterMockModule::class)
15 | )
16 | interface ViewMockComponent {
17 |
18 | fun inject(item: Example2ActivityTest)
19 | fun inject(item: Example2PresenterTest)
20 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/local/dao/WeatherDao.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.local.dao
2 |
3 | import android.arch.persistence.room.Dao
4 | import android.arch.persistence.room.Query
5 | import com.petrulak.cleankotlin.data.source.local.model.WeatherEntity
6 | import io.reactivex.Flowable
7 |
8 | @Dao
9 | interface WeatherDao : BaseDao {
10 |
11 | @Query("SELECT * FROM WeatherEntity")
12 | fun getAll(): Flowable>
13 |
14 | @Query("SELECT * FROM WeatherEntity where name = :name")
15 | fun getByName(name: String): Flowable
16 |
17 | @Query("DELETE from WeatherEntity where uid =:id")
18 | fun deleteWeatherById(id: Long): Int
19 |
20 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/remote/WeatherRemoteSource.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.remote
2 |
3 |
4 | import com.petrulak.cleankotlin.data.source.RemoteSource
5 | import com.petrulak.cleankotlin.data.source.remote.model.WeatherDto
6 | import io.reactivex.Single
7 | import retrofit2.Retrofit
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class WeatherRemoteSource
13 | @Inject
14 | constructor(retrofit: Retrofit) : RemoteSource {
15 |
16 | private val client: WeatherApiClient = retrofit.create(WeatherApiClient::class.java)
17 |
18 | override fun getWeatherForCity(city: String): Single {
19 | return client.getWeatherForCity(city)
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/base/SingleInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor.base
2 |
3 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
4 | import io.reactivex.Single
5 |
6 | abstract class SingleInteractor(
7 | private val schedulerProvider: SchedulerProvider
8 | ) : BaseInteractor() {
9 |
10 | abstract fun buildUseCase(parameters: Parameters): Single
11 |
12 | fun execute(onNext: (T) -> Unit, onError: (Throwable) -> Unit = {}, params: Parameters) {
13 | val single = buildUseCase(params).subscribeOn(schedulerProvider.io()).observeOn(schedulerProvider.ui())
14 | val disposable = single.subscribeWith(getDisposableSingleObserver(onNext, onError))
15 | disposables.add(disposable)
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/base/FlowableInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor.base
2 |
3 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
4 | import io.reactivex.Flowable
5 |
6 | abstract class FlowableInteractor(
7 | private val schedulerProvider: SchedulerProvider
8 | ) : BaseInteractor() {
9 |
10 | abstract fun buildUseCase(parameters: Parameters): Flowable
11 |
12 | fun execute(onNext: (T) -> Unit, onError: (Throwable) -> Unit = {}, params: Parameters) {
13 | val flowable = buildUseCase(params).subscribeOn(schedulerProvider.io()).observeOn(schedulerProvider.ui())
14 | val disposable = flowable.subscribeWith(getDisposableSubscriber(onNext, onError))
15 | disposables.add(disposable)
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
19 |
20 |
25 |
26 |
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/remote/RequestInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.remote
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.Response
5 |
6 | /**
7 | * Intercepts every request and adds `appid` into query parameters.
8 | */
9 | class RequestInterceptor(private val appId: String) : Interceptor {
10 |
11 | override fun intercept(chain: Interceptor.Chain): Response {
12 | val original = chain.request()
13 | val originalHttpUrl = original.url()
14 |
15 | val url = originalHttpUrl
16 | .newBuilder()
17 | .addQueryParameter("appid", appId)
18 | .build()
19 |
20 | val requestBuilder = original.newBuilder().url(url)
21 | val request = requestBuilder.build()
22 |
23 | return chain.proceed(request)
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/analytics/MixpanelAnalyticsManager.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.analytics
2 |
3 | import com.mixpanel.android.mpmetrics.MixpanelAPI
4 |
5 |
6 | class MixpanelAnalyticsManager(val mixpanelAPI: MixpanelAPI) : AnalyticsManager {
7 |
8 | override fun trackEvent(event: String) {
9 | mixpanelAPI.track(event)
10 | }
11 |
12 | override fun trackEvent(event: String, map: HashMap) {
13 | mixpanelAPI.trackMap(event, map)
14 | }
15 |
16 | override fun setUser(email: String) {
17 | mixpanelAPI.identify(email)
18 | }
19 |
20 | override fun setCustomAttributes(attributes: HashMap) {
21 | mixpanelAPI.registerSuperPropertiesMap(attributes)
22 | }
23 |
24 | override fun clear() {
25 | mixpanelAPI.reset()
26 | }
27 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/base/CompletableInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor.base
2 |
3 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
4 | import io.reactivex.Completable
5 |
6 | abstract class CompletableInteractor(
7 | private val schedulerProvider: SchedulerProvider
8 | ) : BaseInteractor() {
9 |
10 | abstract fun buildUseCase(parameters: Parameters): Completable
11 |
12 | fun execute(onCompleted: () -> Unit, onError: (Throwable?) -> Unit = {}, params: Parameters) {
13 | val completable = buildUseCase(params).subscribeOn(schedulerProvider.io()).observeOn(schedulerProvider.ui())
14 | val disposable = completable.subscribeWith(getDisposableCompletableObserver(onCompleted, onError))
15 | disposables.add(disposable)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/mapper/WeatherEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.mapper
2 |
3 | import com.petrulak.cleankotlin.data.mapper.base.Mapper
4 | import com.petrulak.cleankotlin.data.source.local.model.WeatherEntity
5 | import com.petrulak.cleankotlin.domain.model.Weather
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class WeatherEntityMapper
11 | @Inject
12 | constructor() : Mapper() {
13 |
14 | override fun reverse(input: Weather): WeatherEntity {
15 |
16 | val id = input.id
17 | val name = input.name
18 | val visibility = input.visibility
19 |
20 | return WeatherEntity(id, name, visibility)
21 | }
22 |
23 | override fun map(input: WeatherEntity): Weather {
24 | return Weather(input.uid, input.name, input.visibility)
25 | }
26 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/mapper/WeatherMapper.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.mapper
2 |
3 | import com.petrulak.cleankotlin.data.mapper.base.Mapper
4 | import com.petrulak.cleankotlin.data.source.remote.model.WeatherDto
5 | import com.petrulak.cleankotlin.domain.model.Weather
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class WeatherMapper
11 | @Inject
12 | constructor() : Mapper() {
13 |
14 | override fun map(input: WeatherDto): Weather {
15 |
16 | val id = input.id ?: invalidLong
17 | val name = input.name ?: emptyString
18 | val visibility = input.visibility ?: invalidInt
19 |
20 | return Weather(id, name, visibility)
21 | }
22 |
23 | override fun reverse(input: Weather): WeatherDto {
24 | return WeatherDto(input.id, input.name, input.visibility)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/mapper/base/Mapper.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.mapper.base
2 |
3 | import java.util.*
4 |
5 | abstract class Mapper {
6 |
7 | val invalidInt = -1
8 | val invalidLong = (-1).toLong()
9 | val invalidDouble = -1.0
10 | val emptyString = ""
11 |
12 | abstract fun map(from: From): To
13 |
14 | abstract fun reverse(to: To): From
15 |
16 | fun map(list: List?): List {
17 | if (list != null) {
18 | val result = ArrayList(list.size)
19 | list.mapTo(result) { map(it) }
20 | return result
21 | }
22 | return emptyList()
23 | }
24 |
25 | fun reverse(list: List?): List {
26 | if (list != null) {
27 | val result = ArrayList(list.size)
28 | list.mapTo(result) { reverse(it) }
29 | return result
30 | }
31 | return emptyList()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/module/MapperModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import com.petrulak.cleankotlin.data.mapper.WeatherEntityMapper
4 | import com.petrulak.cleankotlin.data.mapper.WeatherMapper
5 | import com.petrulak.cleankotlin.data.mapper.base.Mapper
6 | import com.petrulak.cleankotlin.data.source.local.model.WeatherEntity
7 | import com.petrulak.cleankotlin.data.source.remote.model.WeatherDto
8 | import com.petrulak.cleankotlin.domain.model.Weather
9 | import dagger.Module
10 | import dagger.Provides
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | class MapperModule {
15 |
16 | @Singleton
17 | @Provides
18 | internal fun weatherMapper(mapper: WeatherMapper): Mapper {
19 | return mapper
20 | }
21 |
22 | @Singleton
23 | @Provides
24 | internal fun weatherEntityMapper(mapper: WeatherEntityMapper): Mapper {
25 | return mapper
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.base
2 |
3 | import android.os.Bundle
4 | import android.support.annotation.LayoutRes
5 | import android.support.v4.app.Fragment
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import com.petrulak.cleankotlin.platform.bus.event.EventBus
10 | import javax.inject.Inject
11 |
12 | abstract class BaseFragment : Fragment() {
13 |
14 | @Inject lateinit var eventBus: EventBus
15 |
16 | open fun initializeDependencies() {}
17 | @LayoutRes protected abstract fun layoutId(): Int
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | initializeDependencies()
21 | super.onCreate(savedInstanceState)
22 | }
23 |
24 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
25 | return inflater.inflate(layoutId(), container, false)
26 | }
27 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /home/vpaliy/Android/Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/component/ViewComponent.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.component
2 |
3 | import com.petrulak.cleankotlin.di.module.PresenterModule
4 | import com.petrulak.cleankotlin.di.scope.ViewScope
5 | import com.petrulak.cleankotlin.ui.example1.Example1Activity
6 | import com.petrulak.cleankotlin.ui.example1.fragment.Example1Fragment
7 | import com.petrulak.cleankotlin.ui.example2.Example2Activity
8 | import com.petrulak.cleankotlin.ui.example3.Example3Activity
9 | import com.petrulak.cleankotlin.ui.example3.fragment.Example3Fragment
10 | import com.petrulak.cleankotlin.ui.main.MainActivity
11 | import dagger.Component
12 |
13 | @ViewScope
14 | @Component(dependencies = arrayOf(
15 | ApplicationComponent::class),
16 | modules = arrayOf(PresenterModule::class))
17 | interface ViewComponent {
18 |
19 | fun inject(item: MainActivity)
20 |
21 | fun inject(item: Example1Activity)
22 | fun inject(item: Example1Fragment)
23 |
24 | fun inject(item: Example2Activity)
25 |
26 | fun inject(item: Example3Fragment)
27 | fun inject(item: Example3Activity)
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/di/module/InteractorMockModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherLocallyUseCase
4 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
5 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherUseCase
6 | import dagger.Module
7 | import dagger.Provides
8 | import org.mockito.Mockito
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | class InteractorMockModule {
13 |
14 | @Singleton
15 | @Provides
16 | internal fun getWeatherRemotelyUseCase(): GetWeatherRemotelyUseCase {
17 | return Mockito.mock(GetWeatherRemotelyUseCase::class.java)
18 | }
19 |
20 | @Singleton
21 | @Provides
22 | internal fun getWeatherLocallyUseCase(): GetWeatherLocallyUseCase {
23 | return Mockito.mock(GetWeatherLocallyUseCase::class.java)
24 | }
25 |
26 | @Singleton
27 | @Provides
28 | internal fun getWeatherUseCase(): GetWeatherUseCase {
29 | return Mockito.mock(GetWeatherUseCase::class.java)
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/analytics/AnalyticsManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.analytics
2 |
3 | import com.petrulak.cleankotlin.BuildConfig
4 |
5 |
6 | class AnalyticsManagerImpl(private val trackers: List) : AnalyticsManager {
7 |
8 | override fun trackEvent(event: String) {
9 | execute { trackers.forEach { it.trackEvent(event) } }
10 | }
11 |
12 | override fun trackEvent(event: String, map: HashMap) {
13 | execute { trackers.forEach { it.trackEvent(event, map) } }
14 | }
15 |
16 | override fun setUser(email: String) {
17 | execute { trackers.forEach { it.setUser(email) } }
18 | }
19 |
20 | override fun setCustomAttributes(attributes: HashMap) {
21 | execute { trackers.forEach { it.setCustomAttributes(attributes) } }
22 | }
23 |
24 | override fun clear() {
25 | execute { trackers.forEach { it.clear() } }
26 | }
27 |
28 | private fun execute(action: () -> Unit) {
29 | if (!BuildConfig.DEBUG) {
30 | action.invoke()
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Martin Petrulak
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.
22 |
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/source/local/WeatherLocalSource.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.local
2 |
3 |
4 | import com.petrulak.cleankotlin.data.mapper.WeatherEntityMapper
5 | import com.petrulak.cleankotlin.data.source.LocalSource
6 | import com.petrulak.cleankotlin.data.source.local.model.WeatherEntity
7 | import com.petrulak.cleankotlin.domain.model.Weather
8 | import io.reactivex.Completable
9 | import io.reactivex.Flowable
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | class WeatherLocalSource
15 | @Inject
16 | constructor(
17 | private val db: WeatherDatabase,
18 | private val mapper: WeatherEntityMapper
19 | ) : LocalSource {
20 |
21 | override fun save(weather: Weather): Completable {
22 | val item = mapper.reverse(weather)
23 |
24 | return Completable.fromCallable {
25 | db.weatherDao().deleteWeatherById(item.uid)
26 | db.weatherDao().insert(item)
27 | }
28 | }
29 |
30 | override fun getWeatherForCity(name: String): Flowable {
31 | return db.weatherDao().getByName(name)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
25 |
28 |
29 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/GetWeatherUseCaseImpl.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor
2 |
3 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
4 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherUseCase
5 | import com.petrulak.cleankotlin.domain.model.Weather
6 | import com.petrulak.cleankotlin.domain.repository.WeatherRepository
7 | import io.reactivex.Flowable
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class GetWeatherUseCaseImpl
13 | @Inject
14 | constructor(
15 | private val schedulerProvider: SchedulerProvider,
16 | private val repository: WeatherRepository
17 | ) : GetWeatherUseCase {
18 |
19 | private fun buildUseCase(city: String): Flowable {
20 | return when (city.isEmpty()) {
21 | true -> Flowable.error(IllegalArgumentException("Wrong parameters"))
22 | false -> repository.getWeatherForCity(city)
23 | }
24 | }
25 |
26 | override fun execute(city: String): Flowable {
27 | return buildUseCase(city).subscribeOn(schedulerProvider.io()).observeOn(schedulerProvider.ui())
28 | }
29 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/GetWeatherLocallyUseCaseImpl.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor
2 |
3 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
4 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherLocallyUseCase
5 | import com.petrulak.cleankotlin.domain.model.Weather
6 | import com.petrulak.cleankotlin.domain.repository.WeatherRepository
7 | import io.reactivex.Flowable
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class GetWeatherLocallyUseCaseImpl
13 | @Inject
14 | constructor(
15 | private val schedulerProvider: SchedulerProvider,
16 | private val repository: WeatherRepository
17 | ) : GetWeatherLocallyUseCase {
18 |
19 | private fun buildUseCase(city: String): Flowable {
20 | return when (city.isEmpty()) {
21 | true -> Flowable.error(IllegalArgumentException("Wrong parameters"))
22 | false -> repository.getWeatherForCityLocally(city)
23 | }
24 | }
25 |
26 | override fun execute(city: String): Flowable {
27 | return buildUseCase(city).subscribeOn(schedulerProvider.io()).observeOn(schedulerProvider.ui())
28 | }
29 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/GetWeatherRemotelyUseCaseImpl.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor
2 |
3 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
4 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
5 | import com.petrulak.cleankotlin.domain.model.Weather
6 | import com.petrulak.cleankotlin.domain.repository.WeatherRepository
7 | import io.reactivex.Single
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class GetWeatherRemotelyUseCaseImpl
13 | @Inject
14 | constructor(
15 | private val schedulerProvider: SchedulerProvider,
16 | private val repository: WeatherRepository
17 | ) : GetWeatherRemotelyUseCase {
18 |
19 | private fun buildUseCase(city: String): Single {
20 | return when (city.isEmpty()) {
21 | true -> Single.error(IllegalArgumentException("Wrong parameters"))
22 | false -> repository.getWeatherForCityRemotely(city)
23 | }
24 | }
25 |
26 | override fun execute(city: String): Single {
27 | return buildUseCase(city).subscribeOn(schedulerProvider.io()).observeOn(schedulerProvider.ui())
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_split_view.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
11 |
12 |
17 |
18 |
24 |
25 |
26 |
35 |
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 |
5 | def conf = rootProject.ext.config
6 |
7 | android {
8 |
9 | compileSdkVersion conf.compileSdkVersion
10 | buildToolsVersion conf.buildToolsVersion
11 |
12 | defaultConfig {
13 | minSdkVersion conf.minSdkVersion
14 | targetSdkVersion conf.targetSdkVersion
15 | versionCode conf.versionCode
16 | testInstrumentationRunner conf.testInstrumentationRunner
17 | multiDexEnabled true
18 | }
19 | }
20 |
21 | dependencies {
22 |
23 | implementation project(":domain")
24 |
25 | /* Networking */
26 | compile Deps.gson
27 | compile Deps.okhttp
28 | compile Deps.retrofit
29 | compile Deps.converter_gson
30 | compile Deps.adapter_rxjava2
31 | compile Deps.logging_interceptor
32 |
33 | /* Storage */
34 | compile Deps.room_runtime
35 | compile Deps.room_rxjava2
36 | kapt Deps.room_compiler
37 | compile Deps.kotlin_stdLib
38 |
39 | /* Testing */
40 | testCompile Deps.junit
41 | testCompile Deps.mockito_core
42 | androidTestImplementation Deps.test_runner
43 | androidTestImplementation Deps.room_testing
44 | }
45 | repositories {
46 | mavenCentral()
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kotlin MVP Clean Architecture
2 | Tired of searching for simple, readable and easy to start with template for your Android application?
3 |
4 | If your answer is yes, i have a solution for you in this repository!
5 |
6 | Sample app contains implementation of Clean architecture for Android using Kotlin, RxJava2, Dagger2 and MVP pattern.
7 | ## Features
8 | * **Clean architecture** - App is divided into 3 modules. Data -> Domain -> Presentation
9 | * **MVP** - Presentation layer is using model view presenter (MVP) design pattern.
10 | * **EventBus** - Implementation of event bus using [RxRelay](https://github.com/JakeWharton/RxRelay).
11 | * **DataBus** - Keep data in sync across various part of application.
12 | * **Networking layer** - Network calls done via [Retrofit](http://square.github.io/retrofit/).
13 | * **Persistence layer** - Store data using [Room](https://developer.android.com/topic/libraries/architecture/room.html).
14 | * **Debugging**
15 | * **Stetho** - Debug API calls, explore view hierarchy & storage.
16 | * **LeakCanary** - Discover memory leaks.
17 | * **Timber** - Log errors and pass them into your crash reporting tool (Instabug, Crashlytics etc..).
18 | * **Analytics Manager** - Basic interface to track events you are interested in. Sample usage with Mixpanel.
19 | ## Work in progress
20 | * **Unit Tests**
21 | * **UI Tests**
22 | ## Feel free to contribute :)
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_weather.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
16 |
22 |
23 |
28 |
29 |
34 |
35 |
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example1/fragment/Example1Presenter.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example1.fragment
2 |
3 | import com.petrulak.cleankotlin.di.scope.ViewScope
4 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherUseCase
5 | import com.petrulak.cleankotlin.domain.model.Weather
6 | import com.petrulak.cleankotlin.platform.extensions.getDisposableSubscriber
7 | import com.petrulak.cleankotlin.ui.base.BasePresenterImpl
8 | import com.petrulak.cleankotlin.ui.example1.fragment.Example1Contract.View
9 | import dagger.internal.Preconditions.checkNotNull
10 | import timber.log.Timber
11 | import javax.inject.Inject
12 |
13 | @ViewScope
14 | class Example1Presenter
15 | @Inject
16 | constructor(
17 | private val getWeather: GetWeatherUseCase) : BasePresenterImpl(), Example1Contract.Presenter {
18 |
19 | private var view: View? = null
20 |
21 | override fun attachView(view: View) {
22 | this.view = checkNotNull(view)
23 | }
24 |
25 | override fun start() {
26 | refresh()
27 | }
28 |
29 | override fun refresh() {
30 | val disposable = getWeather
31 | .execute("London,uk")
32 | .subscribeWith(getDisposableSubscriber({ onWeatherSuccess(it) }, { Timber.e(it) }))
33 | disposables.add(disposable)
34 | }
35 |
36 | private fun onWeatherSuccess(item: Weather) {
37 | view?.showWeather(item)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/ui/example2/Example2PresenterTest.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example2
2 |
3 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
4 | import io.reactivex.Single
5 | import org.junit.Before
6 | import org.junit.Test
7 | import org.mockito.Mockito
8 | import org.mockito.Mockito.mock
9 |
10 | class Example2PresenterTest {
11 |
12 | val TAG = "Example2PresenterTest"
13 | val mockUseCase = mock(GetWeatherRemotelyUseCase::class.java)
14 | val mockView = mock(Example2Contract.View::class.java)
15 | val presenter = Example2Presenter(mockUseCase)
16 |
17 | val CITY = "Bratislava"
18 |
19 | @Before
20 | fun setUp() {
21 | presenter.attachView(mockView)
22 | }
23 |
24 | @Test
25 | fun shouldDisplayError() {
26 | val errorMsg = "Errorrrr"
27 | val error = IllegalArgumentException(errorMsg)
28 | Mockito.`when`(mockUseCase.execute(CITY)).thenReturn(Single.error(error))
29 | presenter.refresh(CITY)
30 |
31 | /* TODO make this happen
32 | Log.e(TAG, "viewState = " + presenter.view?.viewState.toString());
33 | Log.e(TAG, "mockvie = " + mockView.viewState.toString());
34 |
35 | verify(mockView).viewState = Matchers.isA(ViewState.Loading::class.java)
36 | verify(mockView).viewState = isA()
37 | verifyNoMoreInteractions(mockView)
38 | */
39 |
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example1/Example1Activity.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example1
2 |
3 | import android.os.Bundle
4 | import com.petrulak.cleankotlin.App
5 | import com.petrulak.cleankotlin.R
6 | import com.petrulak.cleankotlin.di.component.DaggerViewComponent
7 | import com.petrulak.cleankotlin.ui.base.BaseActivity
8 | import com.petrulak.cleankotlin.ui.example1.fragment.Example1Fragment
9 | import timber.log.Timber
10 |
11 | /**
12 | * Example 1 shows how to inject your presenters within Fragment level and how to get data from
13 | * local and remote
14 | */
15 | class Example1Activity : BaseActivity() {
16 |
17 | override fun layoutId() = R.layout.activity_weather
18 |
19 | override fun afterLayout(savedInstanceState: Bundle?) {
20 | if (savedInstanceState == null) {
21 | bindFragment()
22 | }
23 | extractParcelable()
24 | }
25 |
26 | private fun extractParcelable() {
27 | intent.extras.getParcelable("parcelable")?.let {
28 | Timber.i("Parcelable message = ${it.message}")
29 | }
30 | }
31 |
32 | private fun bindFragment() {
33 | supportFragmentManager
34 | .beginTransaction()
35 | .replace(R.id.frame, Example1Fragment.newInstance())
36 | .commit()
37 | }
38 |
39 | override fun inject() {
40 | DaggerViewComponent.builder()
41 | .applicationComponent(App.instance.appComponent())
42 | .build().inject(this)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/data/src/androidTest/java/com/petrulak/cleankotlin/data/source/local/WeatherLocalSourceTest.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.source.local
2 |
3 | import android.arch.persistence.room.Room
4 | import android.support.test.InstrumentationRegistry
5 | import com.petrulak.cleankotlin.data.mapper.WeatherEntityMapper
6 | import com.petrulak.cleankotlin.domain.model.Weather
7 | import org.junit.After
8 | import org.junit.Assert
9 | import org.junit.Before
10 | import org.junit.Test
11 |
12 |
13 | class WeatherLocalSourceTest {
14 |
15 | lateinit var dataSource: WeatherLocalSource
16 | lateinit var database: WeatherDatabase
17 | lateinit var mapper: WeatherEntityMapper
18 |
19 | @Before
20 | fun setup() {
21 | database = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), WeatherDatabase::class.java).build()
22 | mapper = WeatherEntityMapper()
23 | dataSource = WeatherLocalSource(database, mapper)
24 | }
25 |
26 | @After
27 | @Throws(Exception::class)
28 | fun closeDb() {
29 | database.close()
30 | }
31 |
32 | @Test
33 | fun shouldInsertAndGetWeather() {
34 |
35 | val weather = Weather(30L, "Berlin", 123)
36 | dataSource.save(weather)
37 |
38 | val weatherEntity = dataSource.getWeatherForCity(weather.name).blockingFirst()
39 | Assert.assertEquals(weatherEntity.uid, weather.id)
40 | Assert.assertEquals(weatherEntity.name, weather.name)
41 | Assert.assertEquals(weatherEntity.visibility, weather.visibility)
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example2/Example2Presenter.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example2
2 |
3 | import com.petrulak.cleankotlin.di.scope.ViewScope
4 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
5 | import com.petrulak.cleankotlin.platform.extensions.getDisposableSingleObserver
6 | import com.petrulak.cleankotlin.ui.base.BasePresenterImpl
7 | import com.petrulak.cleankotlin.ui.example2.Example2Contract.View
8 | import javax.inject.Inject
9 |
10 |
11 | @ViewScope
12 | class Example2Presenter
13 | @Inject
14 | constructor(private val getWeatherRemotelyUseCase: GetWeatherRemotelyUseCase) : BasePresenterImpl(), Example2Contract.Presenter {
15 |
16 | var view: View? = null
17 |
18 | override fun attachView(view: View) {
19 | this.view = checkNotNull(view)
20 | }
21 |
22 | override fun start() {
23 | super.start()
24 | refresh(CITY)
25 | }
26 |
27 | override fun refresh(city: String) {
28 | val disposable = getWeatherRemotelyUseCase
29 | .execute(city)
30 | .doOnSubscribe { setViewState(ViewState.Loading()) }
31 | .doFinally { setViewState(ViewState.LoadingFinished()) }
32 | .subscribeWith(
33 | getDisposableSingleObserver(
34 | { setViewState(ViewState.Success(it)) },
35 | { setViewState(ViewState.Error(it)) })
36 | )
37 | disposables.add(disposable)
38 | }
39 |
40 | private fun setViewState(state: ViewState) {
41 | view?.viewState = state
42 | }
43 |
44 | companion object {
45 | val CITY = "London,uk"
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/module/DataModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import android.arch.persistence.room.Room
4 | import android.content.Context
5 | import com.petrulak.cleankotlin.data.mapper.WeatherEntityMapper
6 | import com.petrulak.cleankotlin.data.repository.WeatherRepositoryImpl
7 | import com.petrulak.cleankotlin.data.source.LocalSource
8 | import com.petrulak.cleankotlin.data.source.RemoteSource
9 | import com.petrulak.cleankotlin.data.source.local.WeatherDatabase
10 | import com.petrulak.cleankotlin.data.source.local.WeatherLocalSource
11 | import com.petrulak.cleankotlin.data.source.remote.WeatherRemoteSource
12 | import com.petrulak.cleankotlin.domain.repository.WeatherRepository
13 | import dagger.Module
14 | import dagger.Provides
15 | import retrofit2.Retrofit
16 | import javax.inject.Singleton
17 |
18 | @Module
19 | class DataModule {
20 |
21 | @Singleton
22 | @Provides
23 | internal fun weatherRepository(repositoryImpl: WeatherRepositoryImpl): WeatherRepository {
24 | return repositoryImpl
25 | }
26 |
27 | @Provides
28 | @Singleton
29 | internal fun provideRemoteSource(retrofit: Retrofit): RemoteSource {
30 | return WeatherRemoteSource(retrofit)
31 | }
32 |
33 | @Provides
34 | @Singleton
35 | internal fun provideLocalSource(db: WeatherDatabase, mapper: WeatherEntityMapper): LocalSource {
36 | return WeatherLocalSource(db, mapper)
37 | }
38 |
39 | @Provides
40 | @Singleton
41 | internal fun provideRoomDb(context: Context): WeatherDatabase {
42 | return Room.databaseBuilder(context, WeatherDatabase::class.java, "weather-db").build()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/module/InteractorModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
4 | import com.petrulak.cleankotlin.domain.interactor.GetWeatherLocallyUseCaseImpl
5 | import com.petrulak.cleankotlin.domain.interactor.GetWeatherRemotelyUseCaseImpl
6 | import com.petrulak.cleankotlin.domain.interactor.GetWeatherUseCaseImpl
7 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherLocallyUseCase
8 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
9 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherUseCase
10 | import com.petrulak.cleankotlin.domain.repository.WeatherRepository
11 | import dagger.Module
12 | import dagger.Provides
13 | import javax.inject.Singleton
14 |
15 |
16 | @Module
17 | class InteractorModule {
18 |
19 | @Singleton
20 | @Provides
21 | internal fun getWeatherRemotelyUseCase(schedulerProvider: SchedulerProvider, repository: WeatherRepository): GetWeatherRemotelyUseCase {
22 | return GetWeatherRemotelyUseCaseImpl(schedulerProvider, repository)
23 | }
24 |
25 | @Singleton
26 | @Provides
27 | internal fun getWeatherLocallyUseCase(schedulerProvider: SchedulerProvider, repository: WeatherRepository): GetWeatherLocallyUseCase {
28 | return GetWeatherLocallyUseCaseImpl(schedulerProvider, repository)
29 | }
30 |
31 | @Singleton
32 | @Provides
33 | internal fun getWeatherUseCase(schedulerProvider: SchedulerProvider, repository: WeatherRepository): GetWeatherUseCase {
34 | return GetWeatherUseCaseImpl(schedulerProvider, repository)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/module/PresenterModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import com.petrulak.cleankotlin.di.scope.ViewScope
4 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
5 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherUseCase
6 | import com.petrulak.cleankotlin.platform.bus.data.DataBus
7 | import com.petrulak.cleankotlin.platform.bus.event.EventBus
8 | import com.petrulak.cleankotlin.ui.example1.fragment.Example1Contract
9 | import com.petrulak.cleankotlin.ui.example1.fragment.Example1Presenter
10 | import com.petrulak.cleankotlin.ui.example2.Example2Contract
11 | import com.petrulak.cleankotlin.ui.example2.Example2Presenter
12 | import com.petrulak.cleankotlin.ui.example3.fragment.Example3Contract
13 | import com.petrulak.cleankotlin.ui.example3.fragment.Example3Presenter
14 | import dagger.Module
15 | import dagger.Provides
16 |
17 | @Module
18 | class PresenterModule {
19 |
20 | @ViewScope
21 | @Provides
22 | internal fun example1Presenter(getWeatherUseCase: GetWeatherUseCase): Example1Contract.Presenter {
23 | return Example1Presenter(getWeatherUseCase)
24 | }
25 |
26 | @ViewScope
27 | @Provides
28 | internal fun example2Presenter(remotelyUseCaseGet: GetWeatherRemotelyUseCase): Example2Contract.Presenter {
29 | return Example2Presenter(remotelyUseCaseGet)
30 | }
31 |
32 | @ViewScope
33 | @Provides
34 | internal fun example3Presenter(remotelyUseCaseGet: GetWeatherRemotelyUseCase, dataBus: DataBus, eventBus: EventBus): Example3Contract.Presenter {
35 | return Example3Presenter(remotelyUseCaseGet, dataBus, eventBus)
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/data/src/test/java/com/petrulak/cleankotlin/data/mapper/WeatherEntityMapperTest.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.mapper
2 |
3 | import com.petrulak.cleankotlin.data.source.local.model.WeatherEntity
4 | import com.petrulak.cleankotlin.domain.model.Weather
5 | import org.junit.Assert
6 | import org.junit.Test
7 |
8 |
9 | class WeatherEntityMapperTest {
10 |
11 | val mapper = WeatherEntityMapper()
12 |
13 | @Test
14 | fun mapTest_shouldMapToWeather() {
15 | val weatherEntity = WeatherEntity(30L, "Berlin", 123)
16 | val weather = mapper.map(weatherEntity)
17 | Assert.assertEquals(weather.id, 30L)
18 | Assert.assertEquals(weather.name, "Berlin")
19 | Assert.assertEquals(weather.visibility, 123)
20 | }
21 |
22 | @Test
23 | fun reverseTest_shouldMapToWeatherEntity() {
24 | val weather = Weather(30L, "Berlin", 123)
25 | val weatherDto = mapper.reverse(weather)
26 | Assert.assertEquals(weatherDto.uid, 30L)
27 | Assert.assertEquals(weatherDto.name, "Berlin")
28 | Assert.assertEquals(weatherDto.visibility, 123)
29 | }
30 |
31 | @Test
32 | fun mapTest_emptyList() {
33 | val mappedList = mapper.map(emptyList())
34 | Assert.assertEquals(mappedList.size, 0)
35 | }
36 |
37 | @Test
38 | fun mapTest_notEmptyList() {
39 | val weather = WeatherEntity(30L, "Berlin", 123)
40 | val mappedList = mapper.map(listOf(weather, weather, weather, weather))
41 | Assert.assertEquals(mappedList.size, 4)
42 | Assert.assertEquals(mappedList[0].id, 30L)
43 | Assert.assertEquals(mappedList[0].name, "Berlin")
44 | Assert.assertEquals(mappedList[0].visibility, 123)
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/component/ApplicationComponent.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.component
2 |
3 | import android.content.Context
4 | import com.petrulak.cleankotlin.di.module.*
5 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
6 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherLocallyUseCase
7 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
8 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherUseCase
9 | import com.petrulak.cleankotlin.platform.analytics.AnalyticsManager
10 | import com.petrulak.cleankotlin.platform.bus.data.DataBus
11 | import com.petrulak.cleankotlin.platform.bus.event.EventBus
12 | import com.petrulak.cleankotlin.platform.navigation.Navigator
13 | import com.petrulak.cleankotlin.ui.base.BaseActivity
14 | import com.petrulak.cleankotlin.ui.base.BaseFragment
15 | import dagger.Component
16 | import javax.inject.Singleton
17 |
18 | @Singleton
19 | @Component(modules = arrayOf(
20 | DataModule::class,
21 | MapperModule::class,
22 | NetworkModule::class,
23 | InteractorModule::class,
24 | ApplicationModule::class))
25 | interface ApplicationComponent {
26 |
27 | fun inject(item: BaseActivity)
28 | fun inject(item: BaseFragment)
29 |
30 | /* exposing to other components [com.petrulak.cleankotlin.di.component.ViewComponent] */
31 | fun context(): Context
32 |
33 | fun scheduler(): SchedulerProvider
34 | fun navigator(): Navigator
35 | fun eventBus(): EventBus
36 | fun dataBus(): DataBus
37 | fun analyticsManager(): AnalyticsManager
38 | fun getWeatherUseCase(): GetWeatherUseCase
39 | fun getWeatherRemotelyUseCase(): GetWeatherRemotelyUseCase
40 | fun getWeatherLocallyUseCase(): GetWeatherLocallyUseCase
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/module/ApplicationModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import android.content.Context
4 | import com.mixpanel.android.mpmetrics.MixpanelAPI
5 | import com.petrulak.cleankotlin.R
6 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
7 | import com.petrulak.cleankotlin.domain.executor.SchedulerProviderImpl
8 | import com.petrulak.cleankotlin.platform.analytics.AnalyticsManager
9 | import com.petrulak.cleankotlin.platform.analytics.AnalyticsManagerImpl
10 | import com.petrulak.cleankotlin.platform.analytics.MixpanelAnalyticsManager
11 | import com.petrulak.cleankotlin.platform.bus.data.DataBus
12 | import com.petrulak.cleankotlin.platform.bus.event.EventBus
13 | import com.petrulak.cleankotlin.platform.navigation.Navigator
14 | import dagger.Module
15 | import dagger.Provides
16 | import javax.inject.Singleton
17 |
18 | @Module
19 | class ApplicationModule(private val context: Context) {
20 |
21 | @Singleton
22 | @Provides
23 | internal fun context(): Context {
24 | return context
25 | }
26 |
27 | @Singleton
28 | @Provides
29 | internal fun eventBus(): EventBus {
30 | return EventBus()
31 | }
32 |
33 | @Singleton
34 | @Provides
35 | internal fun dataBus(): DataBus {
36 | return DataBus()
37 | }
38 |
39 | @Singleton
40 | @Provides
41 | internal fun schedulerProvider(): SchedulerProvider {
42 | return SchedulerProviderImpl()
43 | }
44 |
45 | @Singleton
46 | @Provides
47 | internal fun navigator(): Navigator {
48 | return Navigator()
49 | }
50 |
51 | @Singleton
52 | @Provides
53 | internal fun provideAnalyticsManager(context: Context): AnalyticsManager {
54 | val mixPanel = MixpanelAnalyticsManager(MixpanelAPI.getInstance(context, context.getString(R.string.mixpanel_key)))
55 | return AnalyticsManagerImpl(listOf(mixPanel))
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/di/module/ApplicationMockModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import android.content.Context
4 | import com.mixpanel.android.mpmetrics.MixpanelAPI
5 | import com.petrulak.cleankotlin.R
6 | import com.petrulak.cleankotlin.domain.executor.SchedulerProvider
7 | import com.petrulak.cleankotlin.domain.executor.SchedulerProviderImpl
8 | import com.petrulak.cleankotlin.platform.analytics.AnalyticsManager
9 | import com.petrulak.cleankotlin.platform.analytics.AnalyticsManagerImpl
10 | import com.petrulak.cleankotlin.platform.analytics.MixpanelAnalyticsManager
11 | import com.petrulak.cleankotlin.platform.bus.data.DataBus
12 | import com.petrulak.cleankotlin.platform.bus.event.EventBus
13 | import com.petrulak.cleankotlin.platform.navigation.Navigator
14 | import dagger.Module
15 | import dagger.Provides
16 | import javax.inject.Singleton
17 |
18 | @Module
19 | class ApplicationMockModule(private val context: Context) {
20 |
21 | @Singleton
22 | @Provides
23 | internal fun context(): Context {
24 | return context
25 | }
26 |
27 | @Singleton
28 | @Provides
29 | internal fun eventBus(): EventBus {
30 | return EventBus()
31 | }
32 |
33 | @Singleton
34 | @Provides
35 | internal fun dataBus(): DataBus {
36 | return DataBus()
37 | }
38 |
39 | @Singleton
40 | @Provides
41 | internal fun schedulerProvider(): SchedulerProvider {
42 | return SchedulerProviderImpl()
43 | }
44 |
45 | @Singleton
46 | @Provides
47 | internal fun navigator(): Navigator {
48 | return Navigator()
49 | }
50 |
51 | @Singleton
52 | @Provides
53 | internal fun provideAnalyticsManager(context: Context): AnalyticsManager {
54 | val mixPanel = MixpanelAnalyticsManager(MixpanelAPI.getInstance(context, context.getString(R.string.mixpanel_key)))
55 | return AnalyticsManagerImpl(listOf(mixPanel))
56 | }
57 |
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example1/fragment/Example1Fragment.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example1.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import com.petrulak.cleankotlin.App
6 | import com.petrulak.cleankotlin.R
7 | import com.petrulak.cleankotlin.di.component.DaggerViewComponent
8 | import com.petrulak.cleankotlin.di.module.PresenterModule
9 | import com.petrulak.cleankotlin.domain.model.Weather
10 | import com.petrulak.cleankotlin.ui.base.BaseFragment
11 | import com.petrulak.cleankotlin.ui.example1.fragment.Example1Contract.Presenter
12 | import kotlinx.android.synthetic.main.fragment_weather.*
13 | import javax.inject.Inject
14 |
15 |
16 | class Example1Fragment : BaseFragment(), Example1Contract.View {
17 |
18 | private var presenter: Presenter? = null
19 |
20 | override fun layoutId() = R.layout.fragment_weather
21 |
22 | override fun initializeDependencies() {
23 | DaggerViewComponent.builder()
24 | .applicationComponent(App.instance.appComponent())
25 | .presenterModule(PresenterModule())
26 | .build().inject(this)
27 | }
28 |
29 | @Inject
30 | override fun attachPresenter(presenter: Presenter) {
31 | this.presenter = presenter
32 | this.presenter?.attachView(this)
33 | }
34 |
35 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
36 | super.onViewCreated(view, savedInstanceState)
37 | presenter?.start()
38 | btn_refresh.setOnClickListener { presenter?.refresh() }
39 | }
40 |
41 | override fun onDestroy() {
42 | presenter?.stop()
43 | super.onDestroy()
44 | }
45 |
46 | override fun showWeather(data: Weather) {
47 | tv_city.text = data.name
48 | tv_visibility.text = data.visibility.toString()
49 | }
50 |
51 | companion object {
52 | fun newInstance(): Example1Fragment {
53 | return Example1Fragment()
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example3/fragment/Example3Fragment.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example3.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import com.petrulak.cleankotlin.App
6 | import com.petrulak.cleankotlin.R
7 | import com.petrulak.cleankotlin.di.component.DaggerViewComponent
8 | import com.petrulak.cleankotlin.di.module.PresenterModule
9 | import com.petrulak.cleankotlin.domain.model.Weather
10 | import com.petrulak.cleankotlin.ui.base.BaseFragment
11 | import com.petrulak.cleankotlin.ui.example3.fragment.Example3Contract.Presenter
12 | import kotlinx.android.synthetic.main.fragment_weather.*
13 | import javax.inject.Inject
14 |
15 |
16 | class Example3Fragment : BaseFragment(), Example3Contract.View {
17 |
18 | private var presenter: Presenter? = null
19 |
20 | override fun layoutId() = R.layout.fragment_weather
21 |
22 | override fun initializeDependencies() {
23 | DaggerViewComponent.builder()
24 | .applicationComponent(App.instance.appComponent())
25 | .presenterModule(PresenterModule())
26 | .build().inject(this)
27 | }
28 |
29 | @Inject
30 | override fun attachPresenter(presenter: Presenter) {
31 | this.presenter = presenter
32 | this.presenter?.attachView(this)
33 | }
34 |
35 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
36 | super.onViewCreated(view, savedInstanceState)
37 | presenter?.start()
38 | btn_refresh.setOnClickListener { presenter?.refresh() }
39 | }
40 |
41 | override fun onDestroy() {
42 | presenter?.stop()
43 | super.onDestroy()
44 | }
45 |
46 | override fun showWeather(data: Weather) {
47 | tv_city.text = data.name
48 | tv_visibility.text = data.visibility.toString()
49 | }
50 |
51 | companion object {
52 | fun newInstance(): Example3Fragment {
53 | return Example3Fragment()
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/data/src/main/java/com/petrulak/cleankotlin/data/repository/WeatherRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.repository
2 |
3 | import com.petrulak.cleankotlin.data.mapper.base.Mapper
4 | import com.petrulak.cleankotlin.data.source.LocalSource
5 | import com.petrulak.cleankotlin.data.source.RemoteSource
6 | import com.petrulak.cleankotlin.data.source.local.model.WeatherEntity
7 | import com.petrulak.cleankotlin.data.source.remote.model.WeatherDto
8 | import com.petrulak.cleankotlin.domain.model.Weather
9 | import com.petrulak.cleankotlin.domain.repository.WeatherRepository
10 | import io.reactivex.Completable
11 | import io.reactivex.Flowable
12 | import io.reactivex.Single
13 | import java.util.*
14 | import javax.inject.Inject
15 | import javax.inject.Singleton
16 |
17 | @Singleton
18 | class WeatherRepositoryImpl
19 | @Inject
20 | constructor(
21 | private val remoteSource: RemoteSource,
22 | private val localSource: LocalSource,
23 | private val weatherDtoMapper: Mapper,
24 | private val weatherEntityMapper: Mapper
25 | ) : WeatherRepository {
26 |
27 | override fun getWeatherForCityRemotely(city: String): Single {
28 | return remoteSource
29 | .getWeatherForCity(city)
30 | .map { weatherDtoMapper.map(it).copy(name = "London,uk", visibility = Random().nextInt(80 - 65) + 65) }
31 | .doOnSuccess { localSource.save(it) }
32 | }
33 |
34 | override fun getWeatherForCityLocally(name: String): Flowable {
35 | return localSource
36 | .getWeatherForCity(name)
37 | .map { weatherEntityMapper.map(it) }
38 | }
39 |
40 | override fun save(weather: Weather): Completable {
41 | return localSource.save(weather)
42 | }
43 |
44 | override fun getWeatherForCity(city: String): Flowable {
45 | val local = getWeatherForCityLocally(city)
46 | val remote = getWeatherForCityRemotely(city).toFlowable()
47 | return Flowable.merge(local, remote)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/data/src/test/java/com/petrulak/cleankotlin/data/mapper/WeatherMapperTest.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.data.mapper
2 |
3 | import com.petrulak.cleankotlin.data.source.remote.model.WeatherDto
4 | import com.petrulak.cleankotlin.domain.model.Weather
5 | import org.junit.Assert
6 | import org.junit.Test
7 |
8 | class WeatherMapperTest {
9 |
10 | val mapper = WeatherMapper()
11 |
12 | @Test
13 | fun mapTest_shouldMapNullValuesToWeather() {
14 | val weatherDto = WeatherDto(null, null, null)
15 | val weather = mapper.map(weatherDto)
16 | Assert.assertEquals(weather.id, mapper.invalidLong)
17 | Assert.assertEquals(weather.name, mapper.emptyString)
18 | Assert.assertEquals(weather.visibility, mapper.invalidInt)
19 | }
20 |
21 | @Test
22 | fun mapTest_shouldMapToWeather() {
23 | val weatherDto = WeatherDto(30L, "Berlin", 123)
24 | val weather = mapper.map(weatherDto)
25 | Assert.assertEquals(weather.id, 30L)
26 | Assert.assertEquals(weather.name, "Berlin")
27 | Assert.assertEquals(weather.visibility, 123)
28 | }
29 |
30 | @Test
31 | fun reverseTest_shouldMapToWeatherDto() {
32 | val weather = Weather(30L, "Berlin", 123)
33 | val weatherDto = mapper.reverse(weather)
34 | Assert.assertEquals(weatherDto.id, 30L)
35 | Assert.assertEquals(weatherDto.name, "Berlin")
36 | Assert.assertEquals(weatherDto.visibility, 123)
37 | }
38 |
39 | @Test
40 | fun mapTest_emptyList() {
41 | val mappedList = mapper.map(emptyList())
42 | Assert.assertEquals(mappedList.size, 0)
43 | }
44 |
45 | @Test
46 | fun mapTest_notEmptyList() {
47 | val weather = WeatherDto(30L, "Berlin", 123)
48 | val mappedList = mapper.map(listOf(weather, weather, weather, weather))
49 | Assert.assertEquals(mappedList.size, 4)
50 | Assert.assertEquals(mappedList[0].id, 30L)
51 | Assert.assertEquals(mappedList[0].name, "Berlin")
52 | Assert.assertEquals(mappedList[0].visibility, 123)
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.main
2 |
3 | import android.os.Bundle
4 | import com.petrulak.cleankotlin.App
5 | import com.petrulak.cleankotlin.R
6 | import com.petrulak.cleankotlin.di.component.DaggerViewComponent
7 | import com.petrulak.cleankotlin.ui.base.BaseActivity
8 | import com.petrulak.cleankotlin.ui.example1.Example1Activity
9 | import com.petrulak.cleankotlin.ui.example1.ExampleParcelable
10 | import com.petrulak.cleankotlin.ui.example2.Example2Activity
11 | import com.petrulak.cleankotlin.ui.example3.Example3Activity
12 | import kotlinx.android.synthetic.main.activity_main.*
13 |
14 | class MainActivity : BaseActivity() {
15 |
16 | override fun layoutId() = R.layout.activity_main
17 |
18 | override fun onViewsBound() {
19 | logAnalytics()
20 | btn_one.setOnClickListener { button1Clicked() }
21 | btn_two.setOnClickListener { button2Clicked() }
22 | btn_three.setOnClickListener { button3Clicked() }
23 | }
24 |
25 | override fun inject() {
26 | DaggerViewComponent.builder()
27 | .applicationComponent(App.instance.appComponent())
28 | .build().inject(this)
29 | }
30 |
31 | private fun button1Clicked() {
32 | val dummy = ExampleParcelable("Hello from here")
33 | val bundle = Bundle()
34 | bundle.putParcelable("parcelable", dummy)
35 | navigator.navigate(this, Example1Activity::class.java, bundle)
36 | }
37 |
38 | private fun button2Clicked() {
39 | navigator.navigate(this, Example2Activity::class.java)
40 | }
41 |
42 | private fun button3Clicked() {
43 | navigator.navigate(this, Example3Activity::class.java)
44 | }
45 |
46 | /**
47 | * This in an example how to use [com.petrulak.cleankotlin.platform.analytics.AnalyticsManager] to
48 | * track events.
49 | */
50 | private fun logAnalytics() {
51 |
52 | analyticsManager.setUser("john@doe.com")
53 |
54 | analyticsManager.trackEvent("Activity 1")
55 |
56 | val map = hashMapOf("userRole" to "admin", "userAge" to 30, "isPremiumUser" to true)
57 | analyticsManager.trackEvent("Activity 2", map)
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/App.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin
2 |
3 | import android.app.Application
4 | import android.os.StrictMode
5 | import com.facebook.stetho.Stetho
6 | import com.petrulak.cleankotlin.di.component.ApplicationComponent
7 | import com.petrulak.cleankotlin.di.component.DaggerApplicationComponent
8 | import com.petrulak.cleankotlin.di.module.*
9 | import com.petrulak.cleankotlin.platform.logging.ErrorReportingTree
10 | import com.squareup.leakcanary.LeakCanary
11 | import timber.log.Timber
12 |
13 | open class App : Application() {
14 |
15 | lateinit var applicationComponent: ApplicationComponent
16 |
17 | override fun onCreate() {
18 | super.onCreate()
19 | instance = this
20 | applicationComponent = initializeAppComponent()
21 | initialize()
22 | }
23 |
24 | open fun initializeAppComponent(): ApplicationComponent {
25 | return DaggerApplicationComponent.builder()
26 | .applicationModule(ApplicationModule(this))
27 | .dataModule(DataModule())
28 | .networkModule(NetworkModule())
29 | .mapperModule(MapperModule())
30 | .interactorModule(InteractorModule())
31 | .build()
32 | }
33 |
34 | private fun initialize() {
35 | if (BuildConfig.DEBUG) {
36 |
37 | Timber.plant(Timber.DebugTree())
38 |
39 | // https://developer.android.com/reference/android/os/StrictMode.html
40 | StrictMode.enableDefaults()
41 |
42 | // http://facebook.github.io/stetho/
43 | Stetho.initializeWithDefaults(this)
44 |
45 | // https://github.com/square/leakcanary
46 | if (LeakCanary.isInAnalyzerProcess(this)) {
47 | return
48 | }
49 | LeakCanary.install(this)
50 |
51 | } else {
52 | Timber.plant(ErrorReportingTree())
53 | // https://medium.com/fuzz/getting-the-most-out-of-crashlytics-380afb703876
54 | // Crashlytics.setString("git_sha", BuildConfig.GIT_SHA)
55 | }
56 | }
57 |
58 | fun appComponent(): ApplicationComponent {
59 | return applicationComponent
60 | }
61 |
62 | companion object {
63 | lateinit var instance: App
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example3/Example3Activity.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example3
2 |
3 | import android.os.Bundle
4 | import com.petrulak.cleankotlin.App
5 | import com.petrulak.cleankotlin.R
6 | import com.petrulak.cleankotlin.di.component.DaggerViewComponent
7 | import com.petrulak.cleankotlin.platform.bus.event.events.BaseEvent
8 | import com.petrulak.cleankotlin.platform.bus.event.events.FragmentSyncEvent
9 | import com.petrulak.cleankotlin.ui.base.BaseActivity
10 | import com.petrulak.cleankotlin.ui.example3.fragment.Example3Fragment
11 | import kotlinx.android.synthetic.main.activity_split_view.*
12 |
13 | /**
14 | * Example 3 shows how to inject your presenters within Fragment level and how to
15 | * synchronize data between 2 fragments.
16 | */
17 | class Example3Activity : BaseActivity() {
18 |
19 | override fun layoutId() = R.layout.activity_split_view
20 |
21 | override fun afterLayout(savedInstanceState: Bundle?) {
22 | if (savedInstanceState == null) {
23 | bindFragment()
24 | }
25 | }
26 |
27 | private fun bindFragment() {
28 | supportFragmentManager
29 | .beginTransaction()
30 | .replace(R.id.frame1, Example3Fragment.newInstance())
31 | .commit()
32 |
33 | supportFragmentManager
34 | .beginTransaction()
35 | .replace(R.id.frame2, Example3Fragment.newInstance())
36 | .commit()
37 | }
38 |
39 | override fun onViewsBound() {
40 | switchBtn.setOnCheckedChangeListener({ _, isChecked ->
41 | when (isChecked) {
42 | true -> eventBus.fragmentSyncEvent.emmit(BaseEvent(FragmentSyncEvent.ACTION_SYNC_ON))
43 | false -> eventBus.fragmentSyncEvent.emmit(BaseEvent(FragmentSyncEvent.ACTION_SYNC_OFF))
44 | }
45 | })
46 |
47 | /**
48 | * This is how you can add some data/payload to event.
49 | * You can see [com.petrulak.cleankotlin.ui.example3.fragment.Example3Presenter.processDummyEvent] how to process event.
50 | * val dummy = Weather(1, "", 1)
51 | * eventBus.weatherDummyEvent.emmit(BaseEvent(WeatherDummyEvent.ACTION_HELLO, dummy))
52 | */
53 | }
54 |
55 | override fun inject() {
56 | DaggerViewComponent.builder()
57 | .applicationComponent(App.instance.appComponent())
58 | .build().inject(this)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/petrulak/cleankotlin/ui/example2/Example2ActivityTest.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example2
2 |
3 | import android.content.Intent
4 | import android.support.test.InstrumentationRegistry
5 | import android.support.test.espresso.Espresso.onView
6 | import android.support.test.espresso.assertion.ViewAssertions.matches
7 | import android.support.test.espresso.matcher.ViewMatchers.withId
8 | import android.support.test.espresso.matcher.ViewMatchers.withText
9 | import android.support.test.rule.ActivityTestRule
10 | import android.support.test.runner.AndroidJUnit4
11 | import com.petrulak.cleankotlin.MockApp
12 | import com.petrulak.cleankotlin.R
13 | import com.petrulak.cleankotlin.di.component.ApplicationMockComponent
14 | import com.petrulak.cleankotlin.di.component.DaggerViewMockComponent
15 | import com.petrulak.cleankotlin.di.module.PresenterMockModule
16 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
17 | import com.petrulak.cleankotlin.domain.model.Weather
18 | import io.reactivex.Single
19 | import org.junit.Before
20 | import org.junit.Rule
21 | import org.junit.Test
22 | import org.junit.runner.RunWith
23 | import org.mockito.Mockito
24 | import javax.inject.Inject
25 |
26 |
27 | @RunWith(AndroidJUnit4::class)
28 | class Example2ActivityTest {
29 |
30 | @get:Rule
31 | var activity = ActivityTestRule(Example2Activity::class.java, true, false) // do not launch the activity immediately
32 |
33 | @Inject lateinit var useCase: GetWeatherRemotelyUseCase
34 |
35 | @Before
36 | fun setUp() {
37 | val instrumentation = InstrumentationRegistry.getInstrumentation()
38 | val app = instrumentation.targetContext.applicationContext as MockApp
39 | val component = app.applicationComponent as ApplicationMockComponent
40 |
41 | DaggerViewMockComponent.builder()
42 | .presenterMockModule(PresenterMockModule())
43 | .applicationMockComponent(component)
44 | .build().inject(this)
45 | }
46 |
47 | @Test
48 | fun shouldUpdateUIAfterObtainingWeatherData() {
49 | val mockedWeather = Weather(1L, "Bratislava", 456)
50 | val mockedSingle = Single.just(mockedWeather)
51 | Mockito.`when`(useCase.execute(mockedWeather.name)).thenReturn(mockedSingle)
52 |
53 | activity.launchActivity(Intent())
54 | onView(withId(R.id.tv_city)).check(matches(withText(mockedWeather.name)))
55 | }
56 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/petrulak/cleankotlin/domain/interactor/base/BaseInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.domain.interactor.base
2 |
3 | import io.reactivex.disposables.CompositeDisposable
4 | import io.reactivex.observers.DisposableCompletableObserver
5 | import io.reactivex.observers.DisposableSingleObserver
6 | import io.reactivex.subscribers.DisposableSubscriber
7 | import timber.log.Timber
8 |
9 |
10 | /**
11 | * If you have set up Crashlytics and/or Instabug in [com.petrulak.cleankotlin.platform.logging.ErrorReportingTree]
12 | * all the errors will be pushed to that platform.
13 | */
14 | abstract class BaseInteractor {
15 |
16 | protected var disposables: CompositeDisposable = CompositeDisposable()
17 |
18 | protected fun getDisposableSingleObserver(
19 | onNext: (T) -> Unit,
20 | onError: (Throwable) -> Unit = {}
21 | ): DisposableSingleObserver {
22 |
23 | return object : DisposableSingleObserver() {
24 | override fun onSuccess(value: T) {
25 | onNext.invoke(value)
26 | }
27 |
28 | override fun onError(e: Throwable) {
29 | Timber.e(e, "DisposableSingleObserver error")
30 | onError.invoke(e)
31 | }
32 | }
33 | }
34 |
35 | protected fun getDisposableCompletableObserver(
36 | onComplete: () -> Unit,
37 | onError: (Throwable) -> Unit = {}
38 | ): DisposableCompletableObserver {
39 |
40 | return object : DisposableCompletableObserver() {
41 |
42 | override fun onComplete() {
43 | onComplete.invoke()
44 | }
45 |
46 | override fun onError(e: Throwable) {
47 | Timber.e(e, "DisposableCompletableObserver error")
48 | onError.invoke(e)
49 | }
50 | }
51 | }
52 |
53 | protected fun getDisposableSubscriber(onNext: (T) -> Unit, onError: (Throwable) -> Unit = {}): DisposableSubscriber {
54 |
55 | return object : DisposableSubscriber() {
56 |
57 | override fun onNext(value: T) {
58 | onNext.invoke(value)
59 | }
60 |
61 | override fun onComplete() {
62 | }
63 |
64 | override fun onError(e: Throwable) {
65 | Timber.e(e, "DisposableSubscriber error")
66 | onError.invoke(e)
67 | }
68 | }
69 | }
70 |
71 | fun dispose() {
72 | disposables.clear()
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.base;
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.os.Bundle
6 | import android.support.annotation.CallSuper
7 | import android.support.annotation.LayoutRes
8 | import android.support.v7.app.AppCompatActivity
9 | import com.petrulak.cleankotlin.platform.analytics.AnalyticsManager
10 | import com.petrulak.cleankotlin.platform.bus.event.EventBus
11 | import com.petrulak.cleankotlin.platform.connectivity.ConnectivityChecker
12 | import com.petrulak.cleankotlin.platform.extensions.lazyFast
13 | import com.petrulak.cleankotlin.platform.navigation.Navigator
14 | import io.reactivex.disposables.CompositeDisposable
15 | import timber.log.Timber
16 | import javax.inject.Inject
17 |
18 |
19 | abstract class BaseActivity : AppCompatActivity(), ConnectivityChecker.Callbacks {
20 |
21 | private val connectivityChecker: ConnectivityChecker by lazyFast { initializeConnectivityChecker() }
22 | private val disposables = CompositeDisposable()
23 |
24 | @Inject lateinit var eventBus: EventBus
25 | @Inject lateinit var navigator: Navigator
26 | @Inject lateinit var analyticsManager: AnalyticsManager
27 |
28 | protected abstract fun inject()
29 | protected open fun afterLayout(savedInstanceState: Bundle?) {}
30 | protected open fun onViewsBound() {}
31 | @LayoutRes protected abstract fun layoutId(): Int
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | inject()
36 | setContentView(layoutId())
37 | afterLayout(savedInstanceState)
38 | onViewsBound()
39 | }
40 |
41 | @CallSuper
42 | override fun onDestroy() {
43 | disposables.clear()
44 | super.onDestroy()
45 | }
46 |
47 | override fun onResume() {
48 | super.onResume()
49 | connectivityChecker.start()
50 | }
51 |
52 | override fun onPause() {
53 | super.onPause()
54 | connectivityChecker.stop()
55 | }
56 |
57 | private fun initializeConnectivityChecker(): ConnectivityChecker {
58 | return ConnectivityChecker(getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager, this)
59 | }
60 |
61 | override fun onConnected() {
62 | Timber.i("Internet connection is ON!!")
63 | }
64 |
65 | override fun onDisconnected() {
66 | Timber.i("Internet connection is OFF!!")
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/goodies.md:
--------------------------------------------------------------------------------
1 | # CODE
2 | ## UI
3 | * [SVG to XML convertor](http://a-student.github.io/SvgToVectorDrawableConverter.Web/)
4 | * [Big SVG repo](https://github.com/encharm/Font-Awesome-SVG-PNG/tree/master/black/svg)
5 | * [Material Design : Color palettes](https://material.io/guidelines/resources/color-palettes.html)
6 | * [Material Design : Typography](https://material.io/guidelines/style/typography.html)
7 | * [Material Design : Widgets/Components](https://material.io/guidelines/components/widgets.html)
8 | * [Activity/Fragment transitions](https://github.com/lgvalle/Material-Animations)
9 | * [Animation library - ExpectAnim](https://github.com/florent37/ExpectAnim)
10 | * [Material Doc](https://materialdoc.com/)
11 | * [Keyframe Animations with ConstraintLayout and ConstraintSet](https://www.youtube.com/watch?v=OHcfs6rStRo)
12 | * [Transitioner - Animations between nested views](https://github.com/dev-labs-bg/transitioner)
13 | # Tooling
14 | ## AndroidStudio
15 | * [Room SQL higlighting](https://www.reddit.com/r/androiddev/comments/7g9246/dont_forget_to_inject_android_room_sql_language/)
16 | * [DagGraph - Dagger dependency graph generator for Android Developers](https://github.com/dvdciri/daggraph)
17 | ## Gradle
18 | * [Gradle tips and recipes](https://developer.android.com/studio/build/gradle-tips.html)
19 | # Tutorials
20 | * [Google code labs](https://codelabs.developers.google.com/?cat=Android)
21 | * [caster.io](https://caster.io/)
22 | # News
23 | * [Android Weekly](http://androidweekly.net/)
24 | * [AndroidDev Digest](https://www.androiddevdigest.com/)
25 | * [Reddit - androiddev](https://www.reddit.com/r/androiddev/top/?sort=top&t=day)
26 | * [GDG Android Berlin - Slack](https://adg-berlin.herokuapp.com/)
27 | * [Fragmented podcast](http://fragmentedpodcast.com/)
28 | * [Android backstage](http://androidbackstage.blogspot.com/)
29 | * [Android developers(youtube)](https://www.youtube.com/user/androiddevelopers/videos)
30 | * [Android Dialog (youtube)](https://www.youtube.com/channel/UCMEmNnHT69aZuaOrE-dF6ug/featured)
31 | * [Kotlin - Weekly newsletter](http://www.kotlinweekly.net/)
32 | * [Kotlin - Slack channel](http://kotlinslackin.herokuapp.com/)
33 | * [Kotlin - Talking Kotlin podcast](http://talkingkotlin.com/)
34 | # General
35 | ## Security
36 | * [A follow-up on how to store tokens securely in Android](https://medium.com/google-developer-experts/a-follow-up-on-how-to-store-tokens-securely-in-android-e84ac5f15f17)
37 | ## Style
38 | * [Kotlin Coding Conventions](http://kotlinlang.org/docs/reference/coding-conventions.html)
39 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: "kotlin-android"
3 | apply plugin: "kotlin-kapt"
4 | apply plugin: "kotlin-android-extensions"
5 |
6 | def conf = rootProject.ext.config
7 |
8 | android {
9 |
10 | compileSdkVersion conf.compileSdkVersion
11 | buildToolsVersion conf.buildToolsVersion
12 |
13 | defaultConfig {
14 |
15 | applicationId conf.applicationId
16 | versionCode conf.versionCode
17 | versionName conf.versionName
18 |
19 | minSdkVersion conf.minSdkVersion
20 | targetSdkVersion conf.targetSdkVersion
21 |
22 | testInstrumentationRunner conf.testInstrumentationRunner
23 | vectorDrawables.useSupportLibrary true
24 | multiDexEnabled true
25 |
26 | def gitSha = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim()
27 | buildConfigField "String", "GIT_SHA", "\"${gitSha}\""
28 |
29 | resValue "string", "mixpanel_key", "your_key_goes_here"
30 |
31 | resValue "string", "base_url", "\"http://samples.openweathermap.org/\""
32 | resValue "string", "weather_api_key", "your_key_goes_here"
33 | }
34 |
35 | buildTypes {
36 |
37 | debug {
38 | debuggable true
39 | applicationIdSuffix ".debug"
40 | buildConfigField "String", "GIT_SHA", "\"DUMMY_GIT_SHA\""
41 | }
42 |
43 | release {
44 | }
45 | }
46 |
47 | packagingOptions {
48 | exclude 'META-INF/rxjava.properties'
49 | }
50 | }
51 |
52 | androidExtensions {
53 | experimental = true
54 | }
55 |
56 | dependencies {
57 |
58 | implementation project(':domain')
59 | implementation project(':data')
60 |
61 | /* View */
62 | compile Deps.appCompat_v7
63 |
64 | /* Core */
65 | compile Deps.rxrelay
66 | compile Deps.dagger
67 | kapt Deps.dagger_compiler
68 | implementation Deps.core_ktx
69 |
70 | /* Debug */
71 | compile Deps.leakcanary_android
72 | compile Deps.stetho
73 | compile Deps.stetho_okhttp3
74 |
75 | /* Analytics */
76 | compile Deps.mixpanel_android
77 |
78 | /* Various */
79 | compile Deps.reactivenetwork_rx2
80 | compile Deps.kotlin_stdLib
81 |
82 | /* Testing */
83 | androidTestCompile Deps.junit
84 | androidTestCompile Deps.mockito_core
85 | androidTestCompile Deps.runner
86 |
87 | androidTestCompile Deps.dagger
88 | kaptTest Deps.dagger_compiler
89 | kaptAndroidTest Deps.dagger_compiler
90 |
91 | androidTestCompile Deps.test_rules
92 | androidTestCompile Deps.espresso_core
93 |
94 | androidTestCompile Deps.dexmaker
95 | androidTestCompile Deps.dexmaker_mockito
96 |
97 | }
98 | repositories {
99 | mavenCentral()
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example2/Example2Activity.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example2
2 |
3 | import android.view.View
4 | import com.petrulak.cleankotlin.App
5 | import com.petrulak.cleankotlin.R
6 | import com.petrulak.cleankotlin.di.component.DaggerViewComponent
7 | import com.petrulak.cleankotlin.di.module.PresenterModule
8 | import com.petrulak.cleankotlin.domain.model.Weather
9 | import com.petrulak.cleankotlin.ui.base.BaseActivity
10 | import kotlinx.android.synthetic.main.fragment_weather.*
11 | import javax.inject.Inject
12 | import kotlin.properties.Delegates
13 |
14 | /**
15 | * Example 2 shows how to inject your presenters within Activity level and how to
16 | * get weather data from remote source (REST API).
17 | */
18 | class Example2Activity : BaseActivity(), Example2Contract.View {
19 |
20 | private var presenter: Example2Contract.Presenter? = null
21 |
22 | override fun layoutId() = R.layout.fragment_weather
23 |
24 | override fun onViewsBound() {
25 | presenter?.start()
26 | btn_refresh.setOnClickListener { presenter?.refresh(Example2Presenter.CITY) }
27 | }
28 |
29 | override fun inject() {
30 | DaggerViewComponent.builder()
31 | .presenterModule(PresenterModule())
32 | .applicationComponent(App.instance.appComponent())
33 | .build().inject(this)
34 | }
35 |
36 | @Inject
37 | override fun attachPresenter(presenter: Example2Contract.Presenter) {
38 | this.presenter = presenter
39 | this.presenter?.attachView(this)
40 | }
41 |
42 | override var viewState: ViewState by Delegates.observable(ViewState.Init(), { _, old, new ->
43 | processStateChange(new)
44 | })
45 |
46 | private fun processStateChange(new: ViewState) {
47 | //using `when` as a statement, forces us to implement all possible values of `ViewState`
48 | return when (new) {
49 | is ViewState.Init -> initialize()
50 | is ViewState.Loading -> loading()
51 | is ViewState.Success -> onNewItem(new.item)
52 | is ViewState.Error -> onError(new.error)
53 | is ViewState.LoadingFinished -> finishLoading()
54 | }
55 | }
56 |
57 | private fun initialize() {
58 | pb_refresh.visibility = View.GONE
59 | }
60 |
61 | private fun loading() {
62 | pb_refresh.visibility = View.VISIBLE
63 | tv_error.visibility = View.GONE
64 | }
65 |
66 | private fun onNewItem(data: Weather) {
67 | tv_city.text = data.name
68 | tv_visibility.text = data.visibility.toString()
69 | }
70 |
71 | private fun finishLoading() {
72 | pb_refresh.visibility = View.GONE
73 | }
74 |
75 | private fun onError(e: Throwable) {
76 | tv_error.visibility = View.VISIBLE
77 | tv_error.text = e.message
78 | }
79 |
80 | override fun onDestroy() {
81 | presenter?.stop()
82 | super.onDestroy()
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/ui/example3/fragment/Example3Presenter.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.ui.example3.fragment
2 |
3 | import android.util.Log
4 | import com.petrulak.cleankotlin.di.scope.ViewScope
5 | import com.petrulak.cleankotlin.domain.interactor.definition.GetWeatherRemotelyUseCase
6 | import com.petrulak.cleankotlin.domain.model.Weather
7 | import com.petrulak.cleankotlin.platform.bus.data.DataBus
8 | import com.petrulak.cleankotlin.platform.bus.event.EventBus
9 | import com.petrulak.cleankotlin.platform.bus.event.events.BaseEvent
10 | import com.petrulak.cleankotlin.platform.bus.event.events.FragmentSyncEvent
11 | import com.petrulak.cleankotlin.platform.extensions.getDisposableSingleObserver
12 | import com.petrulak.cleankotlin.ui.base.BasePresenterImpl
13 | import com.petrulak.cleankotlin.ui.example3.fragment.Example3Contract.View
14 | import dagger.internal.Preconditions.checkNotNull
15 | import io.reactivex.disposables.CompositeDisposable
16 | import java.util.*
17 | import javax.inject.Inject
18 |
19 | @ViewScope
20 | class Example3Presenter
21 | @Inject
22 | constructor(private val getWeatherRemotelyUseCase: GetWeatherRemotelyUseCase,
23 | private val dataBus: DataBus,
24 | private val eventBus: EventBus) : BasePresenterImpl(), Example3Contract.Presenter {
25 |
26 | private var view: View? = null
27 |
28 | private val dataDisposable = CompositeDisposable()
29 |
30 | override fun attachView(view: View) {
31 | this.view = checkNotNull(view)
32 | }
33 |
34 | override fun start() {
35 | super.start()
36 | refresh()
37 | subscribeToData()
38 | subscribeToFragmentSyncEvents()
39 | // subscribeToDummyEvents()
40 | }
41 |
42 | private fun subscribeToData() {
43 | val disposable = dataBus.weatherDataBus.flowable.subscribe({ view?.showWeather(it) })
44 | dataDisposable.add(disposable)
45 | }
46 |
47 | private fun subscribeToFragmentSyncEvents() {
48 | val disposable = eventBus.fragmentSyncEvent.flowable.subscribe({ processEvent(it) })
49 | disposables.add(disposable)
50 | }
51 |
52 | /**
53 | * You can consume data/payload which is included in the event.
54 | */
55 | private fun subscribeToDummyEvents() {
56 | val disposable = eventBus.weatherDummyEvent.flowable.subscribe({ processDummyEvent(it) })
57 | disposables.add(disposable)
58 | }
59 |
60 | private fun processDummyEvent(event: BaseEvent) {
61 | //payload can be null, we have to perform null check
62 | event.payload?.let {
63 | val payload = it
64 | Log.e("Hello", "This is payload: $payload")
65 | }
66 | }
67 |
68 | private fun processEvent(event: BaseEvent) {
69 | when (event.eventType) {
70 | FragmentSyncEvent.ACTION_SYNC_OFF -> dataDisposable.clear()
71 | FragmentSyncEvent.ACTION_SYNC_ON -> subscribeToData()
72 | }
73 | }
74 |
75 | override fun stop() {
76 | super.stop()
77 | dataDisposable.clear()
78 | }
79 |
80 | override fun refresh() {
81 | val disposable = getWeatherRemotelyUseCase
82 | .execute("London,uk")
83 | .subscribeWith(getDisposableSingleObserver({ onSuccess(it) }, { onError(it) }))
84 | disposables.add(disposable)
85 | }
86 |
87 | private fun onSuccess(weather: Weather) {
88 | //Changing visibility value manually to see changes in the UI
89 | val modified = Weather(weather.id, weather.name, Random().nextInt(80 - 65) + 65)
90 | dataBus.weatherDataBus.emmit(modified)
91 | view?.showWeather(modified)
92 | }
93 |
94 | private fun onError(t: Throwable) {
95 |
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/di/module/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.di.module
2 |
3 | import android.content.Context
4 | import com.facebook.stetho.okhttp3.StethoInterceptor
5 | import com.google.gson.Gson
6 | import com.petrulak.cleankotlin.BuildConfig
7 | import com.petrulak.cleankotlin.R
8 | import com.petrulak.cleankotlin.data.source.remote.RequestInterceptor
9 | import dagger.Module
10 | import dagger.Provides
11 | import okhttp3.OkHttpClient
12 | import okhttp3.logging.HttpLoggingInterceptor
13 | import retrofit2.Retrofit
14 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
15 | import retrofit2.converter.gson.GsonConverterFactory
16 | import java.security.cert.CertificateException
17 | import java.security.cert.X509Certificate
18 | import javax.inject.Named
19 | import javax.inject.Singleton
20 | import javax.net.ssl.SSLContext
21 | import javax.net.ssl.TrustManager
22 | import javax.net.ssl.X509TrustManager
23 |
24 |
25 | @Module
26 | class NetworkModule {
27 |
28 | @Provides @Named("prodOkhttpClient")
29 | @Singleton
30 | internal fun provideOkHttpClient(context: Context): OkHttpClient {
31 |
32 | val client = OkHttpClient.Builder()
33 | client.addInterceptor(RequestInterceptor(context.getString(R.string.weather_api_key)))
34 |
35 | if (BuildConfig.DEBUG) {
36 |
37 | client.addNetworkInterceptor(StethoInterceptor())
38 |
39 | val logInterceptor = HttpLoggingInterceptor()
40 | logInterceptor.level = HttpLoggingInterceptor.Level.BODY
41 | client.addInterceptor(logInterceptor)
42 | }
43 |
44 | return client.build()
45 | }
46 |
47 | @Provides
48 | @Singleton
49 | internal fun provideRestAdapter(context: Context, @Named("prodOkhttpClient") okHttpClient: OkHttpClient): Retrofit {
50 |
51 | return Retrofit.Builder()
52 | .baseUrl(context.getString(R.string.base_url))
53 | .addConverterFactory(GsonConverterFactory.create(Gson()))
54 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
55 | .client(okHttpClient)
56 | .build()
57 | }
58 |
59 | @Provides @Named("unsafeOkhttpClient")
60 | @Singleton
61 | internal fun provideUnsafeOkhttpClient(context: Context): OkHttpClient {
62 |
63 | /* Trust anything*/
64 | val trustAllCerts = arrayOf(object : X509TrustManager {
65 | override fun getAcceptedIssuers(): Array {
66 | return emptyArray()
67 | }
68 |
69 | @Throws(CertificateException::class)
70 | override fun checkClientTrusted(chain: Array, authType: String) {
71 | }
72 |
73 | @Throws(CertificateException::class)
74 | override fun checkServerTrusted(chain: Array, authType: String) {
75 | }
76 | })
77 |
78 | val sslContext = SSLContext.getInstance("SSL")
79 | sslContext.init(null, trustAllCerts, java.security.SecureRandom())
80 | val sslSocketFactory = sslContext.socketFactory
81 |
82 | val client = OkHttpClient.Builder()
83 | client.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
84 | client.hostnameVerifier { _, _ -> true }
85 |
86 | /* Rest of config*/
87 | client.addInterceptor(RequestInterceptor(context.getString(R.string.weather_api_key)))
88 | client.addNetworkInterceptor(StethoInterceptor())
89 |
90 | val logInterceptor = HttpLoggingInterceptor()
91 | logInterceptor.level = HttpLoggingInterceptor.Level.NONE
92 | client.addInterceptor(logInterceptor)
93 |
94 | if (!BuildConfig.DEBUG) {
95 | throw RuntimeException("You fool. Do not use this in production!!!")
96 | }
97 |
98 | return client.build()
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/extensions/RxExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.extensions
2 |
3 | import io.reactivex.Completable
4 | import io.reactivex.Flowable
5 | import io.reactivex.Observable
6 | import io.reactivex.Single
7 | import io.reactivex.android.schedulers.AndroidSchedulers
8 | import io.reactivex.disposables.Disposable
9 | import io.reactivex.observers.DisposableCompletableObserver
10 | import io.reactivex.observers.DisposableObserver
11 | import io.reactivex.observers.DisposableSingleObserver
12 | import io.reactivex.schedulers.Schedulers
13 | import io.reactivex.subscribers.DisposableSubscriber
14 | import timber.log.Timber
15 |
16 |
17 | fun subscribeOnIo(observable: Flowable, onNext: (T1) -> Unit = {}, onError: (Throwable) -> Unit = {}): Disposable {
18 | return observable
19 | .subscribeOn(Schedulers.io())
20 | .observeOn(AndroidSchedulers.mainThread())
21 | .subscribeWith(object : DisposableSubscriber() {
22 |
23 | override fun onNext(t: T1) {
24 | onNext.invoke(t)
25 | }
26 |
27 | override fun onComplete() {
28 | }
29 |
30 | override fun onError(e: Throwable) {
31 | Timber.e(e)
32 | onError.invoke(e)
33 | }
34 | })
35 | }
36 |
37 | fun subscribeOnIo(observable: Observable, onNext: (T1) -> Unit = {}, onError: (Throwable) -> Unit = {}): Disposable {
38 | return observable
39 | .subscribeOn(Schedulers.io())
40 | .observeOn(AndroidSchedulers.mainThread())
41 | .subscribeWith(object : DisposableObserver() {
42 |
43 | override fun onNext(t: T1) {
44 | onNext.invoke(t)
45 | }
46 |
47 | override fun onComplete() {
48 | }
49 |
50 | override fun onError(e: Throwable) {
51 | onError.invoke(e)
52 | }
53 | })
54 | }
55 |
56 | fun subscribeOnIo(single: Single, onSuccess: (T1) -> Unit = {}, onError: (Throwable) -> Unit = {}): Disposable {
57 | return single
58 | .subscribeOn(Schedulers.io())
59 | .observeOn(AndroidSchedulers.mainThread())
60 | .subscribeWith(object : DisposableSingleObserver() {
61 |
62 | override fun onSuccess(t: T1) {
63 | onSuccess.invoke(t)
64 | }
65 |
66 | override fun onError(e: Throwable) {
67 | Timber.e(e)
68 | onError.invoke(e)
69 | }
70 | })
71 | }
72 |
73 | fun subscribeOnIo(completable: Completable, onCompleted: () -> Unit, onError: (Throwable) -> Unit) {
74 | return completable
75 | .subscribeOn(Schedulers.io())
76 | .observeOn(AndroidSchedulers.mainThread())
77 | .subscribe(object : DisposableCompletableObserver() {
78 |
79 | override fun onComplete() {
80 | onCompleted.invoke()
81 | }
82 |
83 | override fun onError(e: Throwable) {
84 | onError.invoke(e)
85 | }
86 | })
87 | }
88 |
89 | fun getDisposableSingleObserver(onNext: (T) -> Unit, onError: (Throwable) -> Unit = {}): DisposableSingleObserver {
90 |
91 | return object : DisposableSingleObserver() {
92 | override fun onSuccess(value: T) {
93 | onNext.invoke(value)
94 | }
95 |
96 | override fun onError(e: Throwable) {
97 | Timber.e(e, "DisposableSingleObserver error")
98 | onError.invoke(e)
99 | }
100 | }
101 | }
102 |
103 | fun getDisposableCompletableObserver(onComplete: () -> Unit, onError: (Throwable) -> Unit = {}): DisposableCompletableObserver {
104 |
105 | return object : DisposableCompletableObserver() {
106 |
107 | override fun onComplete() {
108 | onComplete.invoke()
109 | }
110 |
111 | override fun onError(e: Throwable) {
112 | Timber.e(e, "DisposableCompletableObserver error")
113 | onError.invoke(e)
114 | }
115 | }
116 | }
117 |
118 | fun getDisposableSubscriber(onNext: (T) -> Unit, onError: (Throwable) -> Unit = {}): DisposableSubscriber {
119 |
120 | return object : DisposableSubscriber() {
121 |
122 | override fun onNext(value: T) {
123 | onNext.invoke(value)
124 | }
125 |
126 | override fun onComplete() {
127 | }
128 |
129 | override fun onError(e: Throwable) {
130 | Timber.e(e, "DisposableSubscriber error")
131 | onError.invoke(e)
132 | }
133 | }
134 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/petrulak/cleankotlin/platform/connectivity/ConnectivityChecker.kt:
--------------------------------------------------------------------------------
1 | package com.petrulak.cleankotlin.platform.connectivity
2 |
3 | import android.net.ConnectivityManager
4 | import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
5 | import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.SocketInternetObservingStrategy
6 | import com.petrulak.cleankotlin.platform.extensions.subscribeOnIo
7 | import io.reactivex.Observable
8 | import io.reactivex.android.schedulers.AndroidSchedulers
9 | import io.reactivex.disposables.CompositeDisposable
10 | import io.reactivex.schedulers.Schedulers
11 | import java.util.concurrent.TimeUnit
12 |
13 |
14 | /**
15 | * Class which checks if internet connection is present on given device.
16 | * Since sometimes there is no internet connection present(bad cellular reception, wifi at the airport - sign up needed),
17 | * class performs pinging of Google servers.
18 | */
19 | class ConnectivityChecker(
20 | private val connectivityManager: ConnectivityManager,
21 | private val callbacks: Callbacks) {
22 |
23 | private val INITIAL_INTERVAL: Long = 1
24 | private val HW_CHECK_INTERVAL: Long = 500 // how often do you want to check if WIFI/CELLULAR is on ?
25 | private val PING_CHECK_INTERVAL: Long = 3000 // how often do you want to perform ping ?
26 |
27 | private var isConnected: Boolean? = null
28 | private var hardwareStateObservable: Observable
29 | private var connectivityStateObservable: Observable
30 | private val subscriptions: CompositeDisposable = CompositeDisposable()
31 |
32 | interface Callbacks {
33 | fun onConnected()
34 | fun onDisconnected()
35 | }
36 |
37 | init {
38 | connectivityStateObservable = initializeConnectivityStateObservable()
39 |
40 | hardwareStateObservable = Observable
41 | .interval(HW_CHECK_INTERVAL, TimeUnit.MILLISECONDS)
42 | .timeInterval()
43 | .flatMap { Observable.just(hasConnection()) }
44 | }
45 |
46 | private fun initializeConnectivityStateObservable(interval: Long = -1): Observable {
47 | val observable = when (interval > 0) {
48 | true -> Observable.interval(interval, TimeUnit.MILLISECONDS)
49 | false -> Observable.just(INITIAL_INTERVAL)
50 | }
51 | return observable.flatMap {
52 | ReactiveNetwork.observeInternetConnectivity(SocketInternetObservingStrategy())
53 | .subscribeOn(Schedulers.io())
54 | .observeOn(AndroidSchedulers.mainThread())
55 | }
56 | }
57 |
58 | fun start() {
59 | startConnectivityStateChecker()
60 | }
61 |
62 | private fun startHardwareStateChecker() {
63 | subscriptions.add(subscribeOnIo(hardwareStateObservable, ({ handleConnectionChange(it) })))
64 | }
65 |
66 | private fun startConnectivityStateChecker() {
67 | subscriptions.add(subscribeOnIo(connectivityStateObservable, ({ handleConnectionChange(it) })))
68 | }
69 |
70 | private fun handleConnectionChange(value: Boolean) {
71 | if (isConnected == null || value != isConnected) {
72 | executeCallbacks(value)
73 | processConnectionChange(value)
74 | }
75 | isConnected = value
76 | }
77 |
78 | private fun processConnectionChange(isConnected: Boolean) {
79 | when (isConnected) {
80 | true -> scheduleWhenConnectivityIsAvailable()
81 | false -> scheduleWhenNoConnectivity()
82 | }
83 | }
84 |
85 | private fun scheduleWhenNoConnectivity() {
86 | subscriptions.clear()
87 | connectivityStateObservable = initializeConnectivityStateObservable(HW_CHECK_INTERVAL)
88 | startConnectivityStateChecker()
89 | }
90 |
91 | private fun scheduleWhenConnectivityIsAvailable() {
92 | subscriptions.clear()
93 | startHardwareStateChecker()
94 | connectivityStateObservable = initializeConnectivityStateObservable(PING_CHECK_INTERVAL)
95 | startConnectivityStateChecker()
96 |
97 | }
98 |
99 | private fun executeCallbacks(isConnected: Boolean) {
100 |
101 | when (isConnected) {
102 | true -> callbacks.onConnected()
103 | false -> callbacks.onDisconnected()
104 | }
105 | }
106 |
107 | private fun hasConnection(): Boolean {
108 | return (connectivityManager.activeNetworkInfo != null
109 | && connectivityManager.activeNetworkInfo.isAvailable
110 | && connectivityManager.activeNetworkInfo.isConnected)
111 | }
112 |
113 | fun stop() {
114 | subscriptions.clear()
115 | }
116 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------