├── .gitignore ├── README.md ├── RobustUnitTestingInAndroid ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── code │ │ │ └── sliski │ │ │ ├── App.kt │ │ │ ├── AppComponent.kt │ │ │ ├── AppModule.kt │ │ │ ├── UserRole.kt │ │ │ ├── api │ │ │ ├── Client.kt │ │ │ ├── StackoverflowApi.kt │ │ │ ├── StackoverflowClient.kt │ │ │ └── model │ │ │ │ ├── Post.kt │ │ │ │ └── PostWrapper.kt │ │ │ ├── extension │ │ │ ├── Any.kt │ │ │ ├── AppCompatActivity.kt │ │ │ ├── EditText.kt │ │ │ ├── Fragment.kt │ │ │ └── String.kt │ │ │ ├── loginscreen │ │ │ ├── di │ │ │ │ ├── LoginComponent.kt │ │ │ │ ├── LoginModule.kt │ │ │ │ └── LoginScope.kt │ │ │ └── ui │ │ │ │ ├── LoginFragment.kt │ │ │ │ ├── LoginFragmentPresenter.kt │ │ │ │ └── View.kt │ │ │ ├── mainscreen │ │ │ ├── di │ │ │ │ ├── MainComponent.kt │ │ │ │ ├── MainModule.kt │ │ │ │ └── MainScope.kt │ │ │ └── ui │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainActivityPresenter.kt │ │ │ │ └── View.kt │ │ │ ├── mvp │ │ │ └── Presenter.kt │ │ │ ├── postdetailsscreen │ │ │ ├── OnPostClickListener.kt │ │ │ └── PostDetailsFragment.kt │ │ │ ├── postlistscreen │ │ │ ├── di │ │ │ │ ├── PostListComponent.kt │ │ │ │ ├── PostListModule.kt │ │ │ │ └── PostListScope.kt │ │ │ ├── domain │ │ │ │ └── PostListDomain.kt │ │ │ └── ui │ │ │ │ ├── mapper │ │ │ │ ├── PostPresentationMapper.kt │ │ │ │ └── PostPresentationMapperImpl.kt │ │ │ │ ├── model │ │ │ │ └── PresentationPost.kt │ │ │ │ ├── presenter │ │ │ │ ├── PostListFragmentPresenter.kt │ │ │ │ └── PostListProvider.kt │ │ │ │ └── view │ │ │ │ ├── PostListFragment.kt │ │ │ │ └── View.kt │ │ │ ├── preference │ │ │ ├── PreferencesManager.kt │ │ │ └── PreferencesManagerImpl.kt │ │ │ └── userinfoscreen │ │ │ ├── di │ │ │ ├── UserInfoComponent.kt │ │ │ ├── UserInfoModule.kt │ │ │ └── UserInfoScope.kt │ │ │ └── ui │ │ │ ├── UserInfoFragment.kt │ │ │ ├── UserInfoFragmentPresenter.kt │ │ │ └── View.kt │ └── res │ │ ├── drawable │ │ └── logo.png │ │ ├── layout-large │ │ └── user_info_fragment.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── details_fragment.xml │ │ ├── user_id_fragment.xml │ │ └── user_info_fragment.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-large │ │ └── booleans.xml │ │ ├── values-sw600dp │ │ └── booleans.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values-xlarge │ │ └── booleans.xml │ │ └── values │ │ ├── booleans.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── theme.xml │ └── test │ └── java │ └── com │ └── code │ └── sliski │ ├── loginscreen │ └── ui │ │ └── LoginFragmentPresenterTest.groovy │ ├── mainscreen │ └── ui │ │ └── MainActivityPresenterTest.groovy │ └── userinfoscreen │ └── ui │ └── UserInfoFragmentPresenterTest.groovy ├── build.gradle ├── circle.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── slides ├── RobustUnitTesting.pdf ├── RobustUnitTestingInAndroid.apk └── img.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Intellij 29 | .idea/ 30 | *.iml 31 | 32 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Circle 2 | CI](https://circleci.com/gh/sliskiCode/Robust-unit-testing-in-Android.svg?style=svg)](https://circleci.com/gh/sliskiCode/Robust-unit-testing-in-Android) 3 | 4 | Robust unit testing in Android 5 | ============================== 6 | 7 | ![alt tag](https://github.com/sliskiCode/Robust-unit-testing-in-Android/blob/master/slides/img.png) 8 | 9 | This is a sample project used in my presentation at [ADB #5 Cracow](http://www.meetup.com/ADB-Android-Developers-Backstage/events/221578987/). 10 | 11 | Project shows how to unit test Android aplication written in [Kotlin](http://kotlinlang.org/docs/tutorials/kotlin-android.html) using [Dagger 2.0](http://google.github.io/dagger/) and [MVP pattern](https://github.com/antoniolg/androidmvp). 12 | -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.0.6' 3 | ext.support_version = '25.1.0' 4 | ext.dagger_version = '2.5' 5 | ext.retrofit_version = '2.2.0-SNAPSHOT' 6 | repositories { 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:2.3.0-beta1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" 13 | classpath 'org.codehaus.groovy:groovy-android-gradle-plugin:1.1.0' 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | jcenter() 20 | maven { url "https://oss.sonatype.org/content/repositories/snapshots" } 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'groovyx.android' 26 | apply plugin: 'kotlin-android' 27 | apply plugin: 'kotlin-android-extensions' 28 | 29 | android { 30 | compileSdkVersion 25 31 | buildToolsVersion '25.0.2' 32 | 33 | defaultConfig { 34 | applicationId "com.code.sliski.ui" 35 | minSdkVersion 15 36 | targetSdkVersion 25 37 | versionCode 1 38 | versionName "1.0" 39 | } 40 | 41 | lintOptions { 42 | abortOnError false 43 | } 44 | 45 | sourceSets { 46 | main.java.srcDirs += 'src/main/kotlin' 47 | } 48 | } 49 | 50 | dependencies { 51 | // Android 52 | compile "com.android.support:appcompat-v7:$support_version" 53 | compile "com.android.support:support-v4:$support_version" 54 | 55 | // Kotlin 56 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 57 | 58 | // Dagger 59 | compile "com.google.dagger:dagger:$dagger_version" 60 | kapt "com.google.dagger:dagger-compiler:$dagger_version" 61 | provided 'org.glassfish:javax.annotation:10.0-b28' 62 | 63 | // Retrofit 64 | compile "com.squareup.retrofit2:retrofit:$retrofit_version" 65 | compile "com.squareup.retrofit2:converter-gson:$retrofit_version" 66 | compile "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version" 67 | compile 'io.reactivex.rxjava2:rxandroid:2.0.1' 68 | compile 'io.reactivex.rxjava2:rxjava:2.0.4' 69 | 70 | // Preferences 71 | compile 'com.github.talenguyen:prettysharedpreferences:1.0.2' 72 | 73 | // Spock 74 | testCompile 'org.mockito:mockito-core:2.1.0' 75 | testCompile 'org.codehaus.groovy:groovy:2.4.7:grooid' 76 | testCompile('org.spockframework:spock-core:1.1-groovy-2.4-rc-2') 77 | testCompile 'cglib:cglib-nodep:3.2.0' 78 | } 79 | 80 | kapt { 81 | generateStubs = true 82 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/App.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski 2 | 3 | import android.app.Application 4 | import com.code.sliski.postlistscreen.di.DaggerPostListComponent 5 | import com.code.sliski.postlistscreen.di.PostListComponent 6 | import com.code.sliski.postlistscreen.di.PostListModule 7 | 8 | class App : Application() { 9 | 10 | val component: AppComponent by lazy { 11 | DaggerAppComponent.builder() 12 | .appModule(AppModule(this)) 13 | .build() 14 | } 15 | 16 | var postListComponent: PostListComponent? = null 17 | 18 | fun postListComponent(): PostListComponent { 19 | if (postListComponent == null) { 20 | postListComponent = DaggerPostListComponent.builder() 21 | .appComponent(component) 22 | .postListModule(PostListModule()) 23 | .build() 24 | } 25 | 26 | return postListComponent!! 27 | } 28 | 29 | fun releasePostListComponent() { 30 | postListComponent = null 31 | } 32 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski 2 | 3 | import android.content.Context 4 | import com.code.sliski.preference.PreferencesManager 5 | import dagger.Component 6 | import javax.inject.Singleton 7 | 8 | @Singleton 9 | @Component(modules = arrayOf(AppModule::class)) 10 | interface AppComponent { 11 | 12 | fun applicationContext(): Context 13 | fun preferencesManager(): PreferencesManager 14 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski 2 | 3 | import android.content.Context 4 | import com.code.sliski.preference.PreferencesManager 5 | import com.code.sliski.preference.PreferencesManagerImpl 6 | import dagger.Module 7 | import dagger.Provides 8 | 9 | @Module 10 | class AppModule(private val application: App) { 11 | 12 | @Provides 13 | fun preferencesManager(context: Context): PreferencesManager = 14 | PreferencesManagerImpl(context.getSharedPreferences(context.getString(R.string.preferences), 15 | Context.MODE_PRIVATE)) 16 | 17 | @Provides 18 | fun applicationContext(): Context = application 19 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/UserRole.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski 2 | 3 | enum class UserRole { 4 | HERO, 5 | GRUNT, 6 | PEON 7 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/api/Client.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.api 2 | 3 | import com.code.sliski.api.model.Post 4 | import com.code.sliski.api.model.PostWrapper 5 | import io.reactivex.Single 6 | 7 | interface Client { 8 | 9 | fun getPosts(userId: Long): Single> 10 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/api/StackoverflowApi.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.api 2 | 3 | import com.code.sliski.api.model.Post; 4 | import com.code.sliski.api.model.PostWrapper 5 | import io.reactivex.Single 6 | import retrofit2.http.GET 7 | import retrofit2.http.Path 8 | 9 | interface StackoverflowApi { 10 | 11 | @GET("users/{userId}/posts?site=stackoverflow") 12 | fun getPosts(@Path("userId") userId: Long): Single> 13 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/api/StackoverflowClient.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.api 2 | 3 | class StackoverflowClient(api: StackoverflowApi) : Client, StackoverflowApi by api -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/api/model/Post.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.api.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Post constructor(@SerializedName("post_id") val postId: Long, 6 | @SerializedName("score") val score: Int, 7 | @SerializedName("link") val link: String) -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/api/model/PostWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.api.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class PostWrapper(@SerializedName("items") val posts: List) -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/extension/Any.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.extension 2 | 3 | inline fun T?.ifNotNull(function: (T) -> Unit) { 4 | if (this != null) function(this) 5 | } 6 | 7 | inline fun T?.ifNull(function: (T?) -> Unit) { 8 | if (this == null) function(this) 9 | } 10 | 11 | fun T?.isNull() = this == null -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/extension/AppCompatActivity.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.extension 2 | 3 | import android.support.v4.app.Fragment 4 | import android.support.v7.app.AppCompatActivity 5 | import android.widget.Toast 6 | 7 | fun AppCompatActivity.addFragment(layoutId: Int, fragment: Fragment) = 8 | supportFragmentManager 9 | .beginTransaction() 10 | .add(layoutId, fragment) 11 | .commit() 12 | 13 | @Suppress("UNCHECKED_CAST") 14 | fun AppCompatActivity.application() = application as T 15 | 16 | fun AppCompatActivity.showMessage(messageResId: Int) { 17 | showMessage(getString(messageResId)) 18 | } 19 | 20 | fun AppCompatActivity.showMessage(message: String) { 21 | Toast.makeText(this, 22 | message, 23 | Toast.LENGTH_SHORT).show() 24 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/extension/EditText.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.extension 2 | 3 | import android.text.Editable 4 | import android.text.TextWatcher 5 | import android.widget.EditText 6 | 7 | fun EditText.setOnTextChangedListener(function: (String) -> Unit) = 8 | addTextChangedListener(object : TextWatcher { 9 | override fun afterTextChanged(text: Editable) { 10 | function(text.toString()) 11 | } 12 | 13 | override fun beforeTextChanged(text: CharSequence, p1: Int, p2: Int, p3: Int) { 14 | } 15 | 16 | override fun onTextChanged(text: CharSequence, p1: Int, p2: Int, p3: Int) { 17 | } 18 | }) -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/extension/Fragment.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.extension 2 | 3 | import android.support.v4.app.Fragment 4 | import android.view.View 5 | import android.widget.Toast 6 | import com.code.sliski.R 7 | 8 | fun Fragment.isTablet() = resources.getBoolean(R.bool.isTablet) 9 | 10 | fun Fragment.replaceWith(fragment: Fragment) = 11 | activity.supportFragmentManager 12 | .beginTransaction() 13 | .replace((view!!.parent as View).id, fragment) 14 | .commit() 15 | 16 | fun Fragment.addToBackStack(layoutId: Int, fragment: Fragment) = 17 | parentFragment 18 | .childFragmentManager 19 | .beginTransaction() 20 | .replace(layoutId, fragment) 21 | .addToBackStack("backstack") 22 | .commit() 23 | 24 | fun Fragment.addChildFragment(layoutId: Int, fragment: Fragment) = 25 | childFragmentManager 26 | .beginTransaction() 27 | .add(layoutId, fragment) 28 | .commit() 29 | 30 | fun Fragment.showMessage(messageResId: Int) { 31 | showMessage(activity.getString(messageResId)) 32 | } 33 | 34 | fun Fragment.showMessage(message: String) { 35 | Toast.makeText(activity, 36 | message, 37 | Toast.LENGTH_SHORT).show() 38 | } 39 | 40 | @Suppress("UNCHECKED_CAST") 41 | fun Fragment.application() = activity.application as T -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/extension/String.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.extension 2 | 3 | fun String.isPositiveNumber() = matches(Regex("^\\d+$")) 4 | -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/loginscreen/di/LoginComponent.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.loginscreen.di 2 | 3 | import com.code.sliski.AppComponent 4 | import com.code.sliski.loginscreen.ui.LoginFragment 5 | import dagger.Component 6 | 7 | @LoginScope 8 | @Component(modules = arrayOf(LoginModule::class), 9 | dependencies = arrayOf(AppComponent::class)) 10 | interface LoginComponent { 11 | 12 | fun inject(view: LoginFragment) 13 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/loginscreen/di/LoginModule.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.loginscreen.di 2 | 3 | import com.code.sliski.loginscreen.ui.LoginFragmentPresenter 4 | import com.code.sliski.preference.PreferencesManager 5 | import dagger.Module 6 | import dagger.Provides 7 | 8 | @Module 9 | class LoginModule { 10 | 11 | @Provides @LoginScope 12 | fun loginFragmentPresenter(preferencesManager: PreferencesManager): LoginFragmentPresenter = 13 | LoginFragmentPresenter(preferencesManager) 14 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/loginscreen/di/LoginScope.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.loginscreen.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope annotation class LoginScope 6 | -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/loginscreen/ui/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.loginscreen.ui 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View as AndroidView 7 | import android.view.ViewGroup 8 | import com.code.sliski.App 9 | import com.code.sliski.R.layout.user_id_fragment 10 | import com.code.sliski.R.string.bad_format_info 11 | import com.code.sliski.extension.application 12 | import com.code.sliski.extension.replaceWith 13 | import com.code.sliski.extension.showMessage 14 | import com.code.sliski.loginscreen.di.DaggerLoginComponent 15 | import com.code.sliski.loginscreen.di.LoginModule 16 | import com.code.sliski.userinfoscreen.ui.UserInfoFragment 17 | import kotlinx.android.synthetic.main.user_id_fragment.* 18 | import javax.inject.Inject 19 | 20 | class LoginFragment : Fragment(), View { 21 | 22 | @Inject 23 | lateinit var presenter: LoginFragmentPresenter 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | DaggerLoginComponent.builder() 28 | .appComponent(application().component) 29 | .loginModule(LoginModule()) 30 | .build() 31 | .inject(this) 32 | } 33 | 34 | override fun onCreateView(inflater: LayoutInflater, 35 | container: ViewGroup?, 36 | savedInstanceState: Bundle?): AndroidView? = 37 | inflater.inflate(user_id_fragment, container, false) 38 | 39 | override fun onActivityCreated(savedInstanceState: Bundle?) { 40 | super.onActivityCreated(savedInstanceState) 41 | presenter.attach(this) 42 | go_button.setOnClickListener { 43 | presenter.present(user_id.text.toString()) 44 | } 45 | } 46 | 47 | override fun onStart() { 48 | super.onStart() 49 | presenter.attach(this) 50 | } 51 | 52 | override fun onStop() { 53 | super.onStop() 54 | presenter.detach() 55 | } 56 | 57 | override fun showBadFormatInfo() { 58 | showMessage(bad_format_info) 59 | } 60 | 61 | override fun showUserInfoScreen() { 62 | replaceWith(UserInfoFragment()) 63 | } 64 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/loginscreen/ui/LoginFragmentPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.loginscreen.ui 2 | 3 | import com.code.sliski.extension.isPositiveNumber 4 | import com.code.sliski.mvp.Presenter 5 | import com.code.sliski.preference.PreferencesManager 6 | 7 | class LoginFragmentPresenter(private val preferencesManager: PreferencesManager) : Presenter() { 8 | 9 | fun present(userId: String) = if (userId.isPositiveNumber()) { 10 | preferencesManager.saveUserId(userId.toLong()) 11 | view?.showUserInfoScreen() 12 | } else { 13 | view?.showBadFormatInfo() 14 | } 15 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/loginscreen/ui/View.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.loginscreen.ui 2 | 3 | interface View { 4 | fun showBadFormatInfo() 5 | fun showUserInfoScreen() 6 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/mainscreen/di/MainComponent.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.mainscreen.di 2 | 3 | import com.code.sliski.AppComponent 4 | import com.code.sliski.mainscreen.ui.MainActivity 5 | import dagger.Component 6 | 7 | @MainScope 8 | @Component(modules = arrayOf(MainModule::class), 9 | dependencies = arrayOf(AppComponent::class)) 10 | interface MainComponent { 11 | 12 | fun inject(view: MainActivity) 13 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/mainscreen/di/MainModule.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.mainscreen.di 2 | 3 | import android.content.Context 4 | import com.code.sliski.R.string.* 5 | import com.code.sliski.UserRole.* 6 | import com.code.sliski.mainscreen.ui.MainActivityPresenter 7 | import com.code.sliski.preference.PreferencesManager 8 | import dagger.Module 9 | import dagger.Provides 10 | 11 | @Module 12 | class MainModule { 13 | 14 | @Provides @MainScope 15 | fun mainActivityPresenter(preferencesManager: PreferencesManager, context: Context): MainActivityPresenter { 16 | val userRoleDictionary = mapOf(Pair(HERO, context.getString(hero_hello)), 17 | Pair(GRUNT, context.getString(grunt_hello)), 18 | Pair(PEON, context.getString(peon_hello))) 19 | return MainActivityPresenter(preferencesManager.getUserId(), 20 | HERO, 21 | userRoleDictionary) 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/mainscreen/di/MainScope.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.mainscreen.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope annotation class MainScope -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/mainscreen/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.mainscreen.ui 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import com.code.sliski.App 6 | import com.code.sliski.R.id.container 7 | import com.code.sliski.R.layout.activity_main 8 | import com.code.sliski.extension.addFragment 9 | import com.code.sliski.extension.application 10 | import com.code.sliski.extension.showMessage 11 | import com.code.sliski.loginscreen.ui.LoginFragment 12 | import com.code.sliski.mainscreen.di.DaggerMainComponent 13 | import com.code.sliski.mainscreen.di.MainModule 14 | import com.code.sliski.userinfoscreen.ui.UserInfoFragment 15 | import javax.inject.Inject 16 | 17 | class MainActivity : AppCompatActivity(), View { 18 | 19 | @Inject 20 | lateinit var presenter: MainActivityPresenter 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(activity_main) 25 | DaggerMainComponent.builder() 26 | .appComponent(application().component) 27 | .mainModule(MainModule()) 28 | .build() 29 | .inject(this) 30 | presenter.attach(this) 31 | presenter.present(savedInstanceState) 32 | } 33 | 34 | override fun onStart() { 35 | super.onStart() 36 | presenter.attach(this) 37 | } 38 | 39 | override fun onStop() { 40 | super.onStop() 41 | presenter.detach() 42 | } 43 | 44 | override fun showLoginScreen() { 45 | addFragment(container, LoginFragment()) 46 | } 47 | 48 | override fun showUserInfoScreen() { 49 | addFragment(container, UserInfoFragment()) 50 | } 51 | 52 | override fun displayMessage(message: String) { 53 | showMessage(message) 54 | } 55 | 56 | override fun onBackPressed() { 57 | supportFragmentManager.fragments.forEach { 58 | if (it != null && it.isVisible) { 59 | val childFm = it.childFragmentManager 60 | if (childFm.backStackEntryCount > 0) { 61 | childFm.popBackStack() 62 | return 63 | } 64 | } 65 | } 66 | super.onBackPressed() 67 | } 68 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/mainscreen/ui/MainActivityPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.mainscreen.ui 2 | 3 | import android.os.Bundle 4 | import com.code.sliski.UserRole 5 | import com.code.sliski.extension.ifNull 6 | import com.code.sliski.mvp.Presenter 7 | 8 | class MainActivityPresenter(private val userId: Long, 9 | private val userRole: UserRole, 10 | private val userRoleDictionary: Map) : Presenter() { 11 | 12 | private val isLoggedIn by lazy { userId != 0L } 13 | 14 | fun present(state: Bundle?) = state.ifNull { 15 | showScreen() 16 | showMessage() 17 | } 18 | 19 | private fun showScreen() = if (isLoggedIn) { 20 | view?.showUserInfoScreen() 21 | } else { 22 | view?.showLoginScreen() 23 | } 24 | 25 | private fun showMessage() { 26 | if (isLoggedIn) { 27 | view?.displayMessage(userRoleDictionary[userRole]!!) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/mainscreen/ui/View.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.mainscreen.ui 2 | 3 | interface View { 4 | fun showLoginScreen() 5 | fun showUserInfoScreen() 6 | fun displayMessage(message: String) 7 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/mvp/Presenter.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.mvp 2 | 3 | abstract class Presenter { 4 | 5 | protected var view: T? = null 6 | 7 | fun attach(view: T) { 8 | this.view = view 9 | } 10 | 11 | fun detach() { 12 | view = null 13 | } 14 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postdetailsscreen/OnPostClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postdetailsscreen 2 | 3 | import com.code.sliski.postlistscreen.ui.model.PresentationPost 4 | 5 | interface OnPostClickListener { 6 | 7 | fun onPostClick(post: PresentationPost) 8 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postdetailsscreen/PostDetailsFragment.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postdetailsscreen 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import com.code.sliski.R 9 | import com.code.sliski.postlistscreen.ui.model.PresentationPost 10 | import kotlinx.android.synthetic.main.details_fragment.* 11 | 12 | class PostDetailsFragment : Fragment(), 13 | OnPostClickListener { 14 | 15 | companion object { 16 | val POST: String = "POST" 17 | 18 | fun getInstance(post: PresentationPost?): PostDetailsFragment { 19 | val fragment = PostDetailsFragment() 20 | val arguments = Bundle() 21 | arguments.putParcelable(POST, post) 22 | fragment.arguments = arguments 23 | return fragment 24 | } 25 | } 26 | 27 | private var post: PresentationPost? = null 28 | 29 | override fun onCreateView(inflater: LayoutInflater, 30 | container: ViewGroup?, 31 | savedInstanceState: Bundle?): View = 32 | inflater.inflate(R.layout.details_fragment, container, false) 33 | 34 | override fun onActivityCreated(savedInstanceState: Bundle?) { 35 | super.onActivityCreated(savedInstanceState) 36 | post = arguments.getParcelable(POST) 37 | 38 | updateView() 39 | } 40 | 41 | override fun onPostClick(post: PresentationPost) { 42 | this.post = post 43 | updateView() 44 | } 45 | 46 | private fun updateView() { 47 | score.text = post?.score.toString() 48 | link.text = post?.link 49 | } 50 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/di/PostListComponent.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.di 2 | 3 | import com.code.sliski.AppComponent 4 | import com.code.sliski.postlistscreen.ui.view.PostListFragment 5 | import dagger.Component 6 | 7 | @PostListScope 8 | @Component(modules = arrayOf(PostListModule::class), 9 | dependencies = arrayOf(AppComponent::class)) 10 | interface PostListComponent { 11 | 12 | fun inject(view: PostListFragment) 13 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/di/PostListModule.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.di 2 | 3 | import com.code.sliski.api.Client 4 | import com.code.sliski.api.StackoverflowApi 5 | import com.code.sliski.api.StackoverflowClient 6 | import com.code.sliski.postlistscreen.domain.PostListDomain 7 | import com.code.sliski.postlistscreen.ui.mapper.PostPresentationMapperImpl 8 | import com.code.sliski.postlistscreen.ui.presenter.PostListFragmentPresenter 9 | import com.code.sliski.preference.PreferencesManager 10 | import dagger.Module 11 | import dagger.Provides 12 | import io.reactivex.android.schedulers.AndroidSchedulers 13 | import io.reactivex.schedulers.Schedulers 14 | import retrofit2.Retrofit 15 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 16 | import retrofit2.converter.gson.GsonConverterFactory 17 | 18 | @Module 19 | class PostListModule { 20 | 21 | @Provides 22 | @PostListScope 23 | fun providePostListFragmentPresenter(client: Client, 24 | preferencesManager: PreferencesManager): PostListFragmentPresenter = 25 | PostListFragmentPresenter(PostListDomain(client, 26 | preferencesManager.getUserId(), 27 | Schedulers.io()), 28 | PostPresentationMapperImpl, 29 | emptyList(), 30 | AndroidSchedulers.mainThread()) 31 | 32 | @Provides 33 | @PostListScope 34 | fun client(api: StackoverflowApi): Client = StackoverflowClient(api) 35 | 36 | @Provides 37 | @PostListScope 38 | fun stackoverflowApi(): StackoverflowApi = 39 | Retrofit.Builder() 40 | .baseUrl("https://api.stackexchange.com/2.2/") 41 | .addConverterFactory(GsonConverterFactory.create()) 42 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 43 | .build() 44 | .create(StackoverflowApi::class.java) 45 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/di/PostListScope.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope annotation class PostListScope -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/domain/PostListDomain.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.domain 2 | 3 | import com.code.sliski.api.Client 4 | import com.code.sliski.api.model.Post 5 | import com.code.sliski.postlistscreen.ui.presenter.PostListProvider 6 | import io.reactivex.Scheduler 7 | import io.reactivex.Single 8 | 9 | class PostListDomain(private val client: Client, 10 | private val userId: Long, 11 | private val schedulerIO: Scheduler) : PostListProvider { 12 | 13 | override fun postList(): Single> = client 14 | .getPosts(userId) 15 | .subscribeOn(schedulerIO) 16 | .map { it.posts } 17 | .map(::scoreMoreThanFive) 18 | .map(::scoreDescending) 19 | } 20 | 21 | private fun scoreMoreThanFive(posts: List) = posts.filter { it.score > 5 } 22 | private fun scoreDescending(posts: List) = posts.sortedBy(Post::score) -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/ui/mapper/PostPresentationMapper.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.ui.mapper 2 | 3 | import com.code.sliski.api.model.Post 4 | import com.code.sliski.postlistscreen.ui.model.PresentationPost 5 | 6 | interface PostPresentationMapper { 7 | fun map(posts: List): List 8 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/ui/mapper/PostPresentationMapperImpl.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.ui.mapper 2 | 3 | import com.code.sliski.api.model.Post 4 | import com.code.sliski.postlistscreen.ui.model.PresentationPost 5 | 6 | object PostPresentationMapperImpl : PostPresentationMapper { 7 | override fun map(posts: List): List = posts.map { PresentationPost(it) } 8 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/ui/model/PresentationPost.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.ui.model 2 | 3 | import android.os.Parcel 4 | import android.os.Parcelable 5 | import com.code.sliski.api.model.Post 6 | 7 | class PresentationPost(post: Post, 8 | val postId: Long = post.postId, 9 | val score: Int = post.score, 10 | val link: String = post.link) : Parcelable { 11 | 12 | 13 | constructor(parcel: Parcel) : this(Post(parcel.readLong(), 14 | parcel.readInt(), 15 | parcel.readString())) 16 | 17 | companion object { 18 | @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { 19 | override fun createFromParcel(parcel: Parcel): PresentationPost { 20 | return PresentationPost(parcel) 21 | } 22 | 23 | override fun newArray(size: Int): Array { 24 | return arrayOf() 25 | } 26 | } 27 | } 28 | 29 | override fun writeToParcel(dest: Parcel, flags: Int) { 30 | dest.writeLong(postId) 31 | dest.writeInt(score) 32 | dest.writeString(link) 33 | } 34 | 35 | override fun describeContents() = 0 36 | 37 | override fun toString() = "$link $score" 38 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/ui/presenter/PostListFragmentPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.ui.presenter 2 | 3 | import com.code.sliski.mvp.Presenter 4 | import com.code.sliski.postlistscreen.ui.mapper.PostPresentationMapper 5 | import com.code.sliski.postlistscreen.ui.model.PresentationPost 6 | import com.code.sliski.postlistscreen.ui.view.View 7 | import io.reactivex.Scheduler 8 | import io.reactivex.SingleObserver 9 | import io.reactivex.disposables.Disposable 10 | 11 | class PostListFragmentPresenter(provider: PostListProvider, 12 | private val mapper: PostPresentationMapper, 13 | private var postList: List, 14 | schedulerUI: Scheduler) : Presenter() { 15 | 16 | private val single = provider.postList() 17 | .map { mapper.map(it) } 18 | .observeOn(schedulerUI) 19 | 20 | private lateinit var disposable: Disposable 21 | 22 | fun present() = single.subscribe(PostSubscriber()) 23 | 24 | fun onItemClick(position: Int, isTablet: Boolean) = if (isTablet) 25 | view?.notifyOnPostClicked(postList[position]) 26 | else 27 | view?.showPostDetailsScreen(postList[position]) 28 | 29 | fun dispose() { 30 | if (!disposable.isDisposed) disposable.dispose() 31 | } 32 | 33 | inner class PostSubscriber : SingleObserver> { 34 | override fun onError(e: Throwable) { 35 | view?.showError(e.message!!) 36 | } 37 | 38 | override fun onSuccess(result: List) { 39 | postList = result 40 | view?.showPosts(postList) 41 | } 42 | 43 | override fun onSubscribe(d: Disposable) { 44 | disposable = d 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/ui/presenter/PostListProvider.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.ui.presenter 2 | 3 | import com.code.sliski.api.model.Post 4 | import io.reactivex.Single 5 | 6 | interface PostListProvider { 7 | 8 | fun postList(): Single> 9 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/ui/view/PostListFragment.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.ui.view 2 | 3 | import android.R.layout.simple_list_item_1 4 | import android.os.Bundle 5 | import android.support.v4.app.ListFragment 6 | import android.widget.AdapterView 7 | import android.widget.ArrayAdapter 8 | import com.code.sliski.App 9 | import com.code.sliski.R.id.list_container 10 | import com.code.sliski.extension.addToBackStack 11 | import com.code.sliski.extension.application 12 | import com.code.sliski.extension.isTablet 13 | import com.code.sliski.extension.showMessage 14 | import com.code.sliski.postdetailsscreen.OnPostClickListener 15 | import com.code.sliski.postdetailsscreen.PostDetailsFragment 16 | import com.code.sliski.postlistscreen.ui.model.PresentationPost 17 | import com.code.sliski.postlistscreen.ui.presenter.PostListFragmentPresenter 18 | import javax.inject.Inject 19 | import android.view.View as AndroidView 20 | 21 | class PostListFragment : ListFragment(), View, AdapterView.OnItemClickListener { 22 | 23 | @Inject 24 | lateinit var presenter: PostListFragmentPresenter 25 | 26 | lateinit var onPostClickListener: OnPostClickListener 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | application().postListComponent().inject(this) 31 | } 32 | 33 | override fun onActivityCreated(savedInstanceState: Bundle?) { 34 | super.onActivityCreated(savedInstanceState) 35 | listView.onItemClickListener = this 36 | presenter.attach(this) 37 | presenter.present() 38 | } 39 | 40 | override fun onStart() { 41 | super.onStart() 42 | presenter.attach(this) 43 | } 44 | 45 | override fun onStop() { 46 | super.onStop() 47 | presenter.detach() 48 | } 49 | 50 | override fun onDestroy() { 51 | super.onDestroy() 52 | if (activity.isFinishing) { 53 | presenter.dispose() 54 | application().releasePostListComponent() 55 | } 56 | } 57 | 58 | override fun showPosts(posts: List) { 59 | listAdapter = ArrayAdapter(activity, simple_list_item_1, posts) 60 | } 61 | 62 | override fun showPostDetailsScreen(post: PresentationPost) { 63 | addToBackStack(list_container, PostDetailsFragment.getInstance(post)) 64 | } 65 | 66 | override fun notifyOnPostClicked(post: PresentationPost) { 67 | onPostClickListener.onPostClick(post) 68 | } 69 | 70 | override fun onItemClick(pv: AdapterView<*>, v: android.view.View, p: Int, id: Long) { 71 | presenter.onItemClick(p, isTablet()) 72 | } 73 | 74 | override fun showError(message: String) { 75 | showMessage(message) 76 | } 77 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/postlistscreen/ui/view/View.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.postlistscreen.ui.view 2 | 3 | import com.code.sliski.postlistscreen.ui.model.PresentationPost 4 | 5 | interface View { 6 | fun showPosts(posts: List) 7 | fun showPostDetailsScreen(post: PresentationPost) 8 | fun notifyOnPostClicked(post: PresentationPost) 9 | fun showError(message: String) 10 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/preference/PreferencesManager.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.preference 2 | 3 | interface PreferencesManager { 4 | 5 | fun saveUserId(userId: Long) 6 | fun getUserId(): Long 7 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/preference/PreferencesManagerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.preference 2 | 3 | import android.content.SharedPreferences 4 | import com.tale.prettysharedpreferences.PrettySharedPreferences 5 | 6 | class PreferencesManagerImpl(sharedPreferences: SharedPreferences) : 7 | PrettySharedPreferences(sharedPreferences), 8 | PreferencesManager { 9 | 10 | override fun saveUserId(userId: Long) { 11 | getLongEditor("userId").put(userId).commit() 12 | } 13 | 14 | override fun getUserId(): Long = getLongEditor("userId").getOr(0L) 15 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/userinfoscreen/di/UserInfoComponent.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.userinfoscreen.di 2 | 3 | import com.code.sliski.userinfoscreen.ui.UserInfoFragment 4 | 5 | import dagger.Component 6 | 7 | @UserInfoScope 8 | @Component(modules = arrayOf(UserInfoModule::class)) 9 | interface UserInfoComponent { 10 | 11 | fun inject(view: UserInfoFragment) 12 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/userinfoscreen/di/UserInfoModule.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.userinfoscreen.di 2 | 3 | import com.code.sliski.userinfoscreen.ui.UserInfoFragmentPresenter 4 | import dagger.Module 5 | import dagger.Provides 6 | 7 | @Module 8 | class UserInfoModule { 9 | 10 | @Provides @UserInfoScope 11 | fun userInfoFragmentPresenter() = UserInfoFragmentPresenter() 12 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/userinfoscreen/di/UserInfoScope.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.userinfoscreen.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope annotation class UserInfoScope -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/userinfoscreen/ui/UserInfoFragment.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.userinfoscreen.ui 2 | 3 | import android.os.Bundle 4 | import android.support.v4.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View as AndroidView 7 | import android.view.ViewGroup 8 | import com.code.sliski.R.id.list_container 9 | import com.code.sliski.R.id.preview_container 10 | import com.code.sliski.R.layout.user_info_fragment 11 | import com.code.sliski.extension.addChildFragment 12 | import com.code.sliski.extension.isTablet 13 | import com.code.sliski.postdetailsscreen.PostDetailsFragment 14 | import com.code.sliski.postlistscreen.ui.view.PostListFragment 15 | import com.code.sliski.userinfoscreen.di.DaggerUserInfoComponent 16 | import com.code.sliski.userinfoscreen.di.UserInfoModule 17 | import javax.inject.Inject 18 | 19 | class UserInfoFragment : Fragment(), View { 20 | 21 | @Inject 22 | lateinit var presenter: UserInfoFragmentPresenter 23 | 24 | private lateinit var postListFragment: PostListFragment 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | DaggerUserInfoComponent.builder() 29 | .userInfoModule(UserInfoModule()) 30 | .build() 31 | .inject(this) 32 | } 33 | 34 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): AndroidView = 35 | inflater.inflate(user_info_fragment, container, false) 36 | 37 | override fun onActivityCreated(savedInstanceState: Bundle?) { 38 | super.onActivityCreated(savedInstanceState) 39 | presenter.attach(this) 40 | presenter.present(savedInstanceState, isTablet()) 41 | } 42 | 43 | override fun onStart() { 44 | super.onStart() 45 | presenter.attach(this) 46 | } 47 | 48 | override fun onStop() { 49 | super.onStop() 50 | presenter.detach() 51 | } 52 | 53 | override fun showPostListScreen() { 54 | postListFragment = PostListFragment() 55 | addChildFragment(list_container, postListFragment) 56 | } 57 | 58 | override fun showPostDetailsScreen() { 59 | val detailsFragment = PostDetailsFragment.getInstance(null) 60 | postListFragment.onPostClickListener = detailsFragment 61 | addChildFragment(preview_container, detailsFragment) 62 | } 63 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/userinfoscreen/ui/UserInfoFragmentPresenter.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.userinfoscreen.ui 2 | 3 | import android.os.Bundle 4 | import com.code.sliski.extension.ifNull 5 | import com.code.sliski.mvp.Presenter 6 | 7 | class UserInfoFragmentPresenter : Presenter() { 8 | 9 | fun present(savedInstanceState: Bundle?, isTablet: Boolean) = 10 | savedInstanceState.ifNull { 11 | view?.showPostListScreen() 12 | if (isTablet) { 13 | view?.showPostDetailsScreen() 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/kotlin/com/code/sliski/userinfoscreen/ui/View.kt: -------------------------------------------------------------------------------- 1 | package com.code.sliski.userinfoscreen.ui 2 | 3 | interface View { 4 | fun showPostListScreen() 5 | fun showPostDetailsScreen() 6 | } -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sliskiCode/Robust-unit-testing-in-Android/9573455833525dac1c74e6aebc43181a3f157745/RobustUnitTestingInAndroid/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/res/layout-large/user_info_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 15 | 16 | 25 | 26 | -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/res/layout/details_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 15 | 22 | -------------------------------------------------------------------------------- /RobustUnitTestingInAndroid/src/main/res/layout/user_id_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 18 | 19 | 24 | 25 | 32 | 33 |