├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── sample │ │ └── makhrov │ │ └── cleanarchsample │ │ ├── App.kt │ │ ├── dagger │ │ ├── AppComponent.kt │ │ └── AppModule.kt │ │ ├── data │ │ ├── UserRepository.kt │ │ ├── UserRepositoryInterface.kt │ │ └── models │ │ │ └── User.kt │ │ ├── domain │ │ ├── ChangeEmailUseCase.kt │ │ └── mapper │ │ │ └── UserMapper.kt │ │ ├── presentation │ │ ├── ChangeEmailPresenter.kt │ │ ├── ChangeEmailView.kt │ │ ├── MainActivity.kt │ │ └── models │ │ │ └── VMUser.kt │ │ └── utils │ │ └── EmailValidationUtils.kt │ └── res │ ├── drawable │ └── confirm_button_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean architecture in Android with Kotlin + RxJava + Dagger 2 2 | 3 | Here, in UPTech we spend a lot of time developing successful architecture patterns for an android application, we’ve decided to share our experience with the community. 4 | 5 | The task is to create a maintainable solution with interchangeable parts, which will allow us to easily change implementations on each application layer. 6 | Solution must smoothly work with android threading and be easy to understand for new developers 7 | 8 | The Solution is our adaptation of Clean Architecture principles. 9 | 10 | Check the Medium article [Clean architecture in Android with Kotlin + RxJava + Dagger 2](https://medium.com/uptech-team/clean-architecture-in-android-with-kotlin-rxjava-dagger-2-2fdc7441edfc) 11 | 12 | Feel free to use the code for your own purposes. 13 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-kapt' 3 | apply plugin: 'com.neenbedankt.android-apt' 4 | apply plugin: 'kotlin-android' 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | 8 | android { 9 | compileSdkVersion 25 10 | buildToolsVersion "25.0.2" 11 | defaultConfig { 12 | applicationId "com.sample.makhrov.cleanarchsample" 13 | minSdkVersion 16 14 | targetSdkVersion 25 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 30 | exclude group: 'com.android.support', module: 'support-annotations' 31 | }) 32 | compile 'com.android.support:appcompat-v7:25.3.1' 33 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 34 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 35 | apt "com.google.dagger:dagger-compiler:2.7" 36 | kapt "com.google.dagger:dagger-compiler:2.7" 37 | provided 'javax.annotation:jsr250-api:1.0' 38 | compile "com.google.dagger:dagger:2.7" 39 | compile "io.reactivex.rxjava2:rxjava:2.1.0" 40 | compile 'io.reactivex.rxjava2:rxandroid:2.0.1' 41 | } 42 | repositories { 43 | mavenCentral() 44 | } 45 | -------------------------------------------------------------------------------- /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 /Users/anton/Library/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/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/App.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample 2 | 3 | import android.app.Application 4 | import com.sample.makhrov.cleanarchsample.dagger.AppComponent 5 | import com.sample.makhrov.cleanarchsample.dagger.AppModule 6 | import com.sample.makhrov.cleanarchsample.dagger.DaggerAppComponent 7 | 8 | /** 9 | * Created on 5/4/17. 10 | */ 11 | class App : Application() { 12 | companion object{ 13 | lateinit var appComponent: AppComponent 14 | } 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | appComponent = DaggerAppComponent.builder() 19 | .appModule(AppModule(this)) 20 | .build() 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/dagger/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.dagger 2 | 3 | import com.sample.makhrov.cleanarchsample.presentation.MainActivity 4 | import dagger.Component 5 | import javax.inject.Singleton 6 | 7 | /** 8 | * Created on 5/4/17. 9 | */ 10 | @Singleton 11 | @Component(modules = arrayOf(AppModule::class)) 12 | public interface AppComponent { 13 | fun inject(mainActivity: MainActivity) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/dagger/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.dagger 2 | 3 | import android.content.Context 4 | import com.sample.makhrov.cleanarchsample.data.UserRepository 5 | import com.sample.makhrov.cleanarchsample.data.UserRepositoryInterface 6 | import dagger.Module 7 | import dagger.Provides 8 | import javax.inject.Singleton 9 | 10 | /** 11 | * Created on 5/4/17. 12 | */ 13 | @Module 14 | class AppModule(val context: Context) { 15 | 16 | @Provides 17 | @Singleton 18 | fun provideUserRepository(): UserRepositoryInterface { 19 | return UserRepository() 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/data/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.data 2 | 3 | import com.sample.makhrov.cleanarchsample.data.models.User 4 | import io.reactivex.Observable 5 | import io.reactivex.schedulers.Schedulers 6 | import java.util.* 7 | 8 | /** 9 | * Created on 5/4/17. 10 | */ 11 | class UserRepository : UserRepositoryInterface { 12 | override fun changeUserEmail(email: String): Observable { 13 | return Observable.create { sb -> 14 | //Transaction or network imitation 15 | Thread.sleep(2000) 16 | 17 | sb.onNext(User(email, UUID.randomUUID().toString())) 18 | }.subscribeOn(Schedulers.io()) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/data/UserRepositoryInterface.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.data 2 | 3 | import com.sample.makhrov.cleanarchsample.data.models.User 4 | import io.reactivex.Observable 5 | 6 | /** 7 | * Created on 5/4/17. 8 | */ 9 | interface UserRepositoryInterface { 10 | fun changeUserEmail(email: String): Observable 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/data/models/User.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.data.models 2 | 3 | /** 4 | * Created on 5/4/17. 5 | */ 6 | data class User(val email: String, val _id: String) -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/domain/ChangeEmailUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.domain 2 | 3 | import com.sample.makhrov.cleanarchsample.data.UserRepositoryInterface 4 | import com.sample.makhrov.cleanarchsample.domain.mapper.UserMapper 5 | import com.sample.makhrov.cleanarchsample.presentation.models.VMUser 6 | import io.reactivex.Observable 7 | import io.reactivex.android.schedulers.AndroidSchedulers 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | /** 12 | * Created on 5/4/17. 13 | */ 14 | @Singleton 15 | class ChangeEmailUseCase @Inject constructor(private val userRepository: UserRepositoryInterface) { 16 | fun changeUserEmail(email: String): Observable { 17 | return userRepository.changeUserEmail(email) 18 | .map { 19 | UserMapper.toVM(it) 20 | } 21 | .observeOn(AndroidSchedulers.mainThread()) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/domain/mapper/UserMapper.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.domain.mapper 2 | 3 | import com.sample.makhrov.cleanarchsample.data.models.User 4 | import com.sample.makhrov.cleanarchsample.presentation.models.VMUser 5 | 6 | /** 7 | * Created on 5/4/17. 8 | */ 9 | object UserMapper { 10 | fun toVM(user: User): VMUser { 11 | return VMUser(user.email) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/presentation/ChangeEmailPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.presentation 2 | 3 | import com.sample.makhrov.cleanarchsample.domain.ChangeEmailUseCase 4 | import com.sample.makhrov.cleanarchsample.utils.EmailValidationUtils 5 | import javax.inject.Inject 6 | 7 | /** 8 | * Created on 5/4/17. 9 | */ 10 | class ChangeEmailPresenter @Inject constructor(private val changeEmailUseCase: ChangeEmailUseCase) { 11 | 12 | var view: ChangeEmailView? = null 13 | 14 | fun onConfirmButtonPressed() { 15 | view?.let { view -> 16 | val emailInput = view.getEmailInput() 17 | emailInput?.let { 18 | if (EmailValidationUtils.isValidEmail(emailInput)) { 19 | view.hideKeyboard() 20 | changeEmailUseCase.changeUserEmail(emailInput) 21 | .subscribe({ user -> 22 | view.showChangedEmail(user.email) 23 | }, { 24 | //Error while changing email 25 | }) 26 | } else { 27 | view.showEmailValidationError() 28 | } 29 | } ?: view.showEmailValidationError() 30 | } 31 | } 32 | 33 | fun onViewCreated(view: ChangeEmailView) { 34 | this.view = view 35 | } 36 | 37 | fun onViewDestroyed() { 38 | view = null 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/presentation/ChangeEmailView.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.presentation 2 | 3 | /** 4 | * Created on 5/4/17. 5 | */ 6 | interface ChangeEmailView { 7 | fun getEmailInput(): String? 8 | fun showEmailValidationError() 9 | fun showChangedEmail(email: String) 10 | fun hideKeyboard() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/presentation/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.presentation 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import android.view.inputmethod.InputMethodManager 7 | import com.sample.makhrov.cleanarchsample.App 8 | import com.sample.makhrov.cleanarchsample.R 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | import javax.inject.Inject 11 | 12 | class MainActivity : AppCompatActivity(), ChangeEmailView { 13 | 14 | @Inject 15 | lateinit var presenter: ChangeEmailPresenter 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_main) 20 | App.appComponent.inject(this) 21 | 22 | confirmButton.setOnClickListener { 23 | presenter.onConfirmButtonPressed() 24 | } 25 | 26 | presenter.onViewCreated(this) 27 | } 28 | 29 | override fun onDestroy() { 30 | presenter.onViewDestroyed() 31 | super.onDestroy() 32 | } 33 | 34 | override fun getEmailInput() = emailEditText.text.toString() 35 | 36 | override fun showEmailValidationError() { 37 | emailEditText.error = getString(R.string.email_validation_error) 38 | } 39 | 40 | override fun showChangedEmail(email: String) { 41 | changedEmailTextView.text = email 42 | } 43 | 44 | override fun hideKeyboard() { 45 | val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 46 | currentFocus?.windowToken.let { 47 | imm.hideSoftInputFromWindow(it, 0) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/presentation/models/VMUser.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.presentation.models 2 | 3 | /** 4 | * Created on 5/4/17. 5 | */ 6 | data class VMUser(val email: String) -------------------------------------------------------------------------------- /app/src/main/java/com/sample/makhrov/cleanarchsample/utils/EmailValidationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sample.makhrov.cleanarchsample.utils 2 | 3 | import android.text.TextUtils 4 | 5 | /** 6 | * Created on 5/4/17. 7 | */ 8 | object EmailValidationUtils { 9 | 10 | fun isValidEmail(email: String?): Boolean { 11 | return email?.let { 12 | !TextUtils.isEmpty(email) && android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() 13 | } ?: false 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/confirm_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 18 | 19 | 28 | 29 |