├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── 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
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_repo.xml
│ │ │ │ ├── progress.xml
│ │ │ │ └── activity_main.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── azabost
│ │ │ │ └── simplemvvm
│ │ │ │ ├── net
│ │ │ │ ├── connectivity
│ │ │ │ │ ├── Connectivity.kt
│ │ │ │ │ ├── ConnectivityException.kt
│ │ │ │ │ ├── AndroidConnectivity.kt
│ │ │ │ │ └── ConnectivityModule.kt
│ │ │ │ ├── response
│ │ │ │ │ └── RepoResponse.kt
│ │ │ │ ├── GitHubService.kt
│ │ │ │ ├── ApiClient.kt
│ │ │ │ └── NetworkModule.kt
│ │ │ │ ├── utils
│ │ │ │ ├── View.kt
│ │ │ │ ├── logger.kt
│ │ │ │ ├── HasLifecycleScopeProvider.kt
│ │ │ │ ├── HttpErrors.kt
│ │ │ │ └── Observable.kt
│ │ │ │ ├── di
│ │ │ │ ├── AppModule.kt
│ │ │ │ ├── ViewModelFactory.kt
│ │ │ │ ├── AndroidInjectorsModule.kt
│ │ │ │ └── AppComponent.kt
│ │ │ │ ├── ui
│ │ │ │ ├── repo
│ │ │ │ │ ├── RepoViewModel.kt
│ │ │ │ │ ├── RepoActivityIntentModule.kt
│ │ │ │ │ └── RepoActivity.kt
│ │ │ │ ├── BaseActivity.kt
│ │ │ │ ├── BaseFragment.kt
│ │ │ │ └── main
│ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ └── MainViewModel.kt
│ │ │ │ └── App.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── azabost
│ │ │ └── simplemvvm
│ │ │ ├── net
│ │ │ └── connectivity
│ │ │ │ └── FakeConnectivity.kt
│ │ │ ├── utils
│ │ │ ├── HttpErrors.kt
│ │ │ └── MockitoUtils.kt
│ │ │ └── ui
│ │ │ └── main
│ │ │ └── MainViewModelTests.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── azabost
│ │ └── simplemvvm
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── .gitignore
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/azabost/simple-mvvm-example/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/net/connectivity/Connectivity.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.net.connectivity
2 |
3 | interface Connectivity {
4 | fun hasInternetAccess(): Boolean
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/net/response/RepoResponse.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.net.response
2 |
3 | import java.io.Serializable
4 |
5 | data class RepoResponse(val id: Long) : Serializable
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/utils/View.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.utils
2 |
3 | import android.view.View
4 |
5 | fun View.show() {
6 | this.visibility = View.VISIBLE
7 | }
8 |
9 | fun View.hide() {
10 | this.visibility = View.INVISIBLE
11 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jun 14 14:32:08 CEST 2018
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.4-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/test/java/com/azabost/simplemvvm/net/connectivity/FakeConnectivity.kt:
--------------------------------------------------------------------------------
1 | package pl.brightinventions.myparkinson.net.connectivity
2 |
3 | import com.azabost.simplemvvm.net.connectivity.Connectivity
4 |
5 | class FakeConnectivity(var isConnected: Boolean = true) : Connectivity {
6 | override fun hasInternetAccess(): Boolean = isConnected
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/utils/logger.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.utils
2 |
3 | import org.slf4j.Logger
4 | import org.slf4j.LoggerFactory
5 | import kotlin.reflect.KClass
6 |
7 | val Any.logger: Logger get() = javaClass.logger
8 | val KClass<*>.logger: Logger get() = java.logger
9 | val Class<*>.logger: Logger get() = LoggerFactory.getLogger(this)
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/net/connectivity/ConnectivityException.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.net.connectivity
2 |
3 | import android.support.annotation.StringRes
4 | import com.azabost.simplemvvm.R
5 |
6 | class ConnectivityException(
7 | @StringRes val reason: Int = R.string.error_no_internet_connection
8 | ) : Exception("No internet connection")
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.di
2 |
3 | import com.azabost.simplemvvm.net.NetworkModule
4 | import dagger.Module
5 |
6 | @Module(includes = [
7 | NetworkModule::class
8 | ])
9 | class AppModule {
10 | /* You can place something useful here, e.g.:
11 | @Provides
12 | fun providesResources(context: Context): Resources = context.resources
13 | */
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/ui/repo/RepoViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.ui.repo
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import com.azabost.simplemvvm.net.response.RepoResponse
5 | import javax.inject.Inject
6 |
7 | interface RepoVM {
8 | val repoResponse: RepoResponse
9 | }
10 |
11 | class RepoViewModel @Inject constructor(
12 | override val repoResponse: RepoResponse
13 | ) : ViewModel(), RepoVM
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/ui/repo/RepoActivityIntentModule.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.ui.repo
2 |
3 | import com.azabost.simplemvvm.net.response.RepoResponse
4 | import dagger.Module
5 | import dagger.Provides
6 |
7 | @Module
8 | class RepoActivityIntentModule {
9 | @Provides
10 | fun providesRepoResponse(activity: RepoActivity): RepoResponse {
11 | return activity.intent.getSerializableExtra(RepoActivity.REPO_RESPONSE_EXTRA) as RepoResponse
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/azabost/simplemvvm/utils/HttpErrors.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.utils
2 |
3 | import okhttp3.MediaType
4 | import okhttp3.ResponseBody
5 | import retrofit2.HttpException
6 | import retrofit2.Response
7 |
8 | fun HttpErrors.getHttpException(
9 | code: Int = 404,
10 | contentType: MediaType? = null,
11 | content: String = ""): HttpException {
12 | return HttpException(Response.error(code, ResponseBody.create(contentType, content)))
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/utils/HasLifecycleScopeProvider.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.utils
2 |
3 | import com.uber.autodispose.LifecycleScopeProvider
4 | import com.uber.autodispose.kotlin.autoDisposable
5 | import io.reactivex.Observable
6 |
7 | interface HasLifecycleScopeProvider {
8 | val scopeProvider: LifecycleScopeProvider<*>
9 |
10 | fun Observable.observeOnMainThreadAndAutoDispose() =
11 | this.observeOnMainThread().autoDisposable(scopeProvider)
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/net/connectivity/AndroidConnectivity.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.net.connectivity
2 |
3 | import android.net.ConnectivityManager
4 | import android.net.NetworkInfo
5 |
6 | class AndroidConnectivity(private val connectivityManager: ConnectivityManager) : Connectivity {
7 |
8 | override fun hasInternetAccess(): Boolean {
9 | val activeNetwork: NetworkInfo? = connectivityManager.activeNetworkInfo
10 | return activeNetwork?.isConnected ?: false
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/di/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.di
2 |
3 |
4 | import android.arch.lifecycle.ViewModel
5 | import android.arch.lifecycle.ViewModelProvider
6 | import dagger.Lazy
7 | import javax.inject.Inject
8 |
9 | class ViewModelFactory @Inject constructor(
10 | private val viewModel: Lazy
11 | ) : ViewModelProvider.Factory {
12 |
13 | @Suppress("UNCHECKED_CAST")
14 | override fun create(modelClass: Class): T {
15 | return viewModel.get() as T
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/net/GitHubService.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.net
2 |
3 | import com.azabost.simplemvvm.net.response.RepoResponse
4 | import io.reactivex.Observable
5 | import retrofit2.http.GET
6 | import retrofit2.http.Path
7 |
8 | interface GitHubService {
9 |
10 | @GET("repos/{owner}/{repo}")
11 | fun getRepo(
12 | @Path("owner") owner: String,
13 | @Path("repo") repo: String
14 | ): Observable
15 |
16 | companion object {
17 | val BASE_URL = "https://api.github.com/"
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_repo.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/net/connectivity/ConnectivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.net.connectivity
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import dagger.Module
6 | import dagger.Provides
7 | import javax.inject.Singleton
8 |
9 | @Module
10 | class ConnectivityModule {
11 |
12 | @Provides
13 | @Singleton
14 | fun providesConnectivity(context: Context): Connectivity {
15 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
16 | return AndroidConnectivity(cm)
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SimpleMvvm
3 | Your favorite meaningless error message
4 | No internet connection
5 | Repository name should be formatted like this: <owner>/<repo>
6 | <owner>/<repo>
7 | Get data
8 | There is no such repository. Check the provided name and try again.
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/di/AndroidInjectorsModule.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.di
2 |
3 | import com.azabost.simplemvvm.ui.main.MainActivity
4 | import com.azabost.simplemvvm.ui.repo.RepoActivity
5 | import com.azabost.simplemvvm.ui.repo.RepoActivityIntentModule
6 | import dagger.Module
7 | import dagger.android.ContributesAndroidInjector
8 |
9 | @Module
10 | abstract class AndroidInjectorsModule {
11 | @ContributesAndroidInjector
12 | abstract fun contributeMainActivity(): MainActivity
13 |
14 | @ContributesAndroidInjector(modules = [RepoActivityIntentModule::class])
15 | abstract fun contributeRepoActivity(): RepoActivity
16 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/utils/HttpErrors.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.utils
2 |
3 | import com.azabost.simplemvvm.R
4 | import retrofit2.HttpException
5 |
6 | typealias HttpErrorsMapper = (HttpException) -> Int?
7 |
8 | object HttpErrors {
9 | const val DEFAULT_HTTP_ERROR_MESSAGE = R.string.default_error_message
10 |
11 | fun httpExceptionToErrorMessage(
12 | exception: HttpException,
13 | httpErrorsMapper: HttpErrorsMapper? = this::defaultHttpErrorsMapper
14 | ): Int {
15 | return httpErrorsMapper?.invoke(exception) ?: defaultHttpErrorsMapper(exception)
16 | }
17 |
18 | private fun defaultHttpErrorsMapper(exception: HttpException): Int {
19 | return DEFAULT_HTTP_ERROR_MESSAGE
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/di/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.di
2 |
3 | import android.content.Context
4 | import com.azabost.simplemvvm.App
5 | import dagger.BindsInstance
6 | import dagger.Component
7 | import dagger.android.AndroidInjectionModule
8 | import dagger.android.AndroidInjector
9 | import javax.inject.Singleton
10 |
11 |
12 | @Singleton
13 | @Component(modules = arrayOf(
14 | AndroidInjectionModule::class,
15 | AppModule::class,
16 | AndroidInjectorsModule::class
17 | ))
18 | interface AppComponent : AndroidInjector {
19 |
20 | @Component.Builder
21 | abstract class Builder : AndroidInjector.Builder() {
22 | @BindsInstance abstract fun appContext(appContext: Context): Builder
23 | }
24 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/azabost/simplemvvm/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm
2 |
3 | import android.support.test.InstrumentationRegistry
4 | import android.support.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getTargetContext()
22 | assertEquals("com.azabost.simplemvvm", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/ui/repo/RepoActivity.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.ui.repo
2 |
3 | import android.os.Bundle
4 | import com.azabost.simplemvvm.R
5 | import com.azabost.simplemvvm.di.ViewModelFactory
6 | import com.azabost.simplemvvm.ui.BaseActivity
7 | import kotlinx.android.synthetic.main.activity_repo.*
8 | import javax.inject.Inject
9 |
10 | class RepoActivity : BaseActivity() {
11 |
12 | @Inject
13 | lateinit var vmFactory: ViewModelFactory
14 |
15 | lateinit var vm: RepoVM
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContentView(R.layout.activity_repo)
20 |
21 | vm = vmFactory.get()
22 |
23 | repoData.text = vm.repoResponse.id.toString()
24 | }
25 |
26 | companion object {
27 | const val REPO_RESPONSE_EXTRA = "REPO_RESPONSE_EXTRA"
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/net/ApiClient.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.net
2 |
3 | import com.azabost.simplemvvm.net.connectivity.Connectivity
4 | import com.azabost.simplemvvm.net.connectivity.ConnectivityException
5 | import com.azabost.simplemvvm.net.response.RepoResponse
6 | import io.reactivex.Observable
7 | import javax.inject.Inject
8 |
9 | class ApiClient @Inject constructor(
10 | private val gitHubService: GitHubService,
11 | private val connectivity: Connectivity
12 | ) {
13 | fun getRepo(owner: String, repo: String): Observable {
14 | return gitHubService.getRepo(owner, repo).checkConnectivity()
15 | }
16 |
17 | private fun Observable.checkConnectivity(): Observable {
18 | return if (connectivity.hasInternetAccess()) {
19 | this
20 | } else {
21 | Observable.error(ConnectivityException())
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/azabost/simplemvvm/utils/MockitoUtils.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.utils
2 |
3 | import io.reactivex.Observable
4 | import io.reactivex.schedulers.TestScheduler
5 | import org.mockito.Mockito
6 | import org.mockito.stubbing.OngoingStubbing
7 |
8 | object MockitoUtils {
9 |
10 | // Workaround for Mockito's any() in Kotlin
11 | // https://medium.com/@elye.project/befriending-kotlin-and-mockito-1c2e7b0ef791
12 | fun any(): T {
13 | Mockito.any()
14 | return null as T
15 | }
16 |
17 | fun mockApiResponse(
18 | stubbing: OngoingStubbing>,
19 | testScheduler: TestScheduler,
20 | response: T? = null,
21 | error: Throwable? = null
22 | ) {
23 | val r = if (response != null) Observable.just(response) else Observable.error(error)
24 | stubbing.thenReturn(r.subscribeOn(testScheduler).observeOn(testScheduler))
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/ui/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.ui
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import android.arch.lifecycle.ViewModelProviders
5 | import android.os.Bundle
6 | import android.support.v7.app.AppCompatActivity
7 | import com.azabost.simplemvvm.di.ViewModelFactory
8 | import com.azabost.simplemvvm.utils.HasLifecycleScopeProvider
9 | import com.uber.autodispose.LifecycleScopeProvider
10 | import com.uber.autodispose.android.lifecycle.scope
11 | import dagger.android.AndroidInjection
12 |
13 |
14 | abstract class BaseActivity : AppCompatActivity(), HasLifecycleScopeProvider {
15 |
16 | override val scopeProvider: LifecycleScopeProvider<*> by lazy { scope() }
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | AndroidInjection.inject(this)
19 | super.onCreate(savedInstanceState)
20 | }
21 |
22 | inline fun ViewModelFactory.get(): T =
23 | ViewModelProviders.of(this@BaseActivity, this)[T::class.java]
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/App.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.content.Context
6 | import android.support.multidex.MultiDex
7 | import com.azabost.simplemvvm.di.DaggerAppComponent
8 | import dagger.android.AndroidInjector
9 | import dagger.android.DispatchingAndroidInjector
10 | import dagger.android.HasActivityInjector
11 | import javax.inject.Inject
12 |
13 | class App : Application(), HasActivityInjector {
14 |
15 | @Inject
16 | lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector
17 |
18 | override fun activityInjector(): AndroidInjector = dispatchingAndroidInjector
19 |
20 | override fun onCreate() {
21 | super.onCreate()
22 |
23 | DaggerAppComponent
24 | .builder()
25 | .appContext(this)
26 | .create(this)
27 | .inject(this)
28 | }
29 |
30 | override fun attachBaseContext(base: Context) {
31 | super.attachBaseContext(base)
32 | MultiDex.install(this)
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/net/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.net
2 |
3 | import com.azabost.simplemvvm.net.connectivity.Connectivity
4 | import com.azabost.simplemvvm.net.connectivity.ConnectivityModule
5 | import com.google.gson.Gson
6 | import com.google.gson.GsonBuilder
7 | import dagger.Module
8 | import dagger.Provides
9 | import retrofit2.Retrofit
10 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
11 | import retrofit2.converter.gson.GsonConverterFactory
12 | import javax.inject.Singleton
13 |
14 | @Module(
15 | includes = [
16 | ConnectivityModule::class
17 | ]
18 | )
19 | class NetworkModule {
20 | @Provides
21 | @Singleton
22 | fun providesGitHubClient(gson: Gson, connectivity: Connectivity): ApiClient {
23 | val retrofit = Retrofit.Builder()
24 | .baseUrl(GitHubService.BASE_URL)
25 | .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
26 | .addConverterFactory(GsonConverterFactory.create(gson))
27 | .build()
28 |
29 | val gitHubService = retrofit.create(GitHubService::class.java)
30 | return ApiClient(gitHubService, connectivity)
31 | }
32 |
33 | @Singleton
34 | @Provides
35 | fun getGsonInstance(): Gson {
36 | val gsonBuilder = GsonBuilder()
37 | return gsonBuilder.create()
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
25 |
26 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/utils/Observable.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.utils
2 |
3 | import android.support.annotation.StringRes
4 | import com.azabost.simplemvvm.net.connectivity.ConnectivityException
5 | import io.reactivex.Observable
6 | import io.reactivex.android.schedulers.AndroidSchedulers
7 | import io.reactivex.subjects.PublishSubject
8 | import retrofit2.HttpException
9 |
10 | fun Observable.observeOnMainThread(): Observable =
11 | observeOn(AndroidSchedulers.mainThread())
12 |
13 | fun Observable.withProgress(
14 | progressSubject: PublishSubject
15 | ): Observable {
16 |
17 | return compose {
18 | it.doOnSubscribe {
19 | progressSubject.onNext(true)
20 | }.doAfterTerminate {
21 | progressSubject.onNext(false)
22 | }
23 | }
24 | }
25 |
26 | fun Observable.showErrorMessages(
27 | errorsSubject: PublishSubject,
28 | @StringRes default: Int,
29 | httpErrorsMapper: HttpErrorsMapper? = null
30 | ): Observable {
31 |
32 | return compose {
33 | it.doOnError {
34 | if (it is HttpException) {
35 | errorsSubject.onNext(HttpErrors.httpExceptionToErrorMessage(it, httpErrorsMapper))
36 | } else if (it is ConnectivityException) {
37 | errorsSubject.onNext(it.reason)
38 | } else {
39 | errorsSubject.onNext(default)
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/ui/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.ui
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import android.arch.lifecycle.ViewModelProviders
5 | import android.content.Context
6 | import android.support.v4.app.Fragment
7 | import com.azabost.simplemvvm.di.ViewModelFactory
8 | import com.azabost.simplemvvm.utils.HasLifecycleScopeProvider
9 | import com.uber.autodispose.LifecycleScopeProvider
10 | import com.uber.autodispose.android.lifecycle.scope
11 | import dagger.android.AndroidInjector
12 | import dagger.android.DispatchingAndroidInjector
13 | import dagger.android.support.AndroidSupportInjection
14 | import dagger.android.support.HasSupportFragmentInjector
15 | import javax.inject.Inject
16 |
17 | abstract class BaseFragment :
18 | Fragment(),
19 | HasSupportFragmentInjector,
20 | HasLifecycleScopeProvider {
21 |
22 | @Inject
23 | lateinit var childFragmentInjector: DispatchingAndroidInjector
24 |
25 | override val scopeProvider: LifecycleScopeProvider<*> by lazy { scope() }
26 | override fun onAttach(context: Context) {
27 | AndroidSupportInjection.inject(this)
28 | super.onAttach(context)
29 | }
30 |
31 | override fun supportFragmentInjector(): AndroidInjector = childFragmentInjector
32 |
33 | inline fun ViewModelFactory.get(): T =
34 | ViewModelProviders.of(this@BaseFragment, this)[T::class.java]
35 |
36 | inline fun ViewModelFactory.getForActivity(): T =
37 | ViewModelProviders.of(activity!!, this)[T::class.java]
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.ui.main
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.widget.Toast
6 | import com.azabost.simplemvvm.R
7 | import com.azabost.simplemvvm.di.ViewModelFactory
8 | import com.azabost.simplemvvm.ui.BaseActivity
9 | import com.azabost.simplemvvm.ui.repo.RepoActivity
10 | import com.azabost.simplemvvm.utils.hide
11 | import com.azabost.simplemvvm.utils.show
12 | import kotlinx.android.synthetic.main.activity_main.*
13 | import javax.inject.Inject
14 |
15 | class MainActivity : BaseActivity() {
16 |
17 | @Inject
18 | lateinit var vmFactory: ViewModelFactory
19 |
20 | lateinit var vm: MainVM
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.activity_main)
25 |
26 | vm = vmFactory.get()
27 |
28 | vm.progress.observeOnMainThreadAndAutoDispose().subscribe {
29 | if (it) progress.show() else progress.hide()
30 | }
31 |
32 | vm.errors.observeOnMainThreadAndAutoDispose().subscribe {
33 | Toast.makeText(this, it, Toast.LENGTH_LONG).show()
34 | }
35 |
36 | vm.data.observeOnMainThreadAndAutoDispose().subscribe {
37 | val intent = Intent(this, RepoActivity::class.java).apply {
38 | putExtra(RepoActivity.REPO_RESPONSE_EXTRA, it)
39 | }
40 | startActivity(intent)
41 | }
42 |
43 | getRepoDataButton.setOnClickListener {
44 | vm.getRepoData(repoNameInput.text.toString())
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/azabost/simplemvvm/ui/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.ui.main
2 |
3 | import android.arch.lifecycle.ViewModel
4 | import com.azabost.simplemvvm.R
5 | import com.azabost.simplemvvm.net.ApiClient
6 | import com.azabost.simplemvvm.net.response.RepoResponse
7 | import com.azabost.simplemvvm.utils.logger
8 | import com.azabost.simplemvvm.utils.showErrorMessages
9 | import com.azabost.simplemvvm.utils.withProgress
10 | import io.reactivex.Observable
11 | import io.reactivex.disposables.Disposable
12 | import io.reactivex.subjects.PublishSubject
13 | import javax.inject.Inject
14 |
15 | interface MainVM {
16 | val progress: Observable
17 | val errors: Observable
18 | val data: Observable
19 | fun getRepoData(repoPath: String)
20 | }
21 |
22 | class MainViewModel @Inject constructor(
23 | private val apiClient: ApiClient
24 | ) : ViewModel(), MainVM {
25 |
26 | private var getRepoDataDisposable: Disposable? = null
27 | private val log = logger
28 |
29 | override val progress: PublishSubject = PublishSubject.create()
30 | override val errors: PublishSubject = PublishSubject.create()
31 | override val data: PublishSubject = PublishSubject.create()
32 |
33 | override fun getRepoData(repoPath: String) {
34 | val pathSplits = repoPath.split("/")
35 |
36 | if (pathSplits.size == 2) {
37 | val owner = pathSplits.first()
38 | val repo = pathSplits.last()
39 | fetchRepoData(owner, repo)
40 | } else {
41 | errors.onNext(R.string.wrong_repo_format)
42 | }
43 | }
44 |
45 | private fun fetchRepoData(owner: String, repo: String) {
46 | getRepoDataDisposable?.dispose()
47 |
48 | getRepoDataDisposable = apiClient.getRepo(owner, repo)
49 | .withProgress(progress)
50 | .showErrorMessages(errors, R.string.default_error_message) {
51 | when (it.code()) {
52 | 404 -> R.string.no_such_repo
53 | else -> null
54 | }
55 | }
56 | .subscribe({
57 | data.onNext(it)
58 | }, {
59 | log.error("Fetching repo $owner / $repo failed", it)
60 | })
61 | }
62 |
63 | override fun onCleared() {
64 | getRepoDataDisposable?.dispose()
65 | }
66 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | android {
7 | compileSdkVersion 27
8 | defaultConfig {
9 | applicationId "com.azabost.simplemvvm"
10 | minSdkVersion 25
11 | targetSdkVersion 27
12 | versionCode 1
13 | versionName "1.0"
14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | testOptions {
23 | unitTests.returnDefaultValues = true
24 | }
25 | }
26 |
27 | dependencies {
28 | implementation fileTree(dir: 'libs', include: ['*.jar'])
29 |
30 | // Logging
31 | implementation "com.github.bright:slf4android:0.1.5"
32 |
33 | // Kotlin
34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
35 |
36 | // Android KTX
37 | implementation 'androidx.core:core-ktx:0.3'
38 |
39 | // Android Support
40 | final SUPPORT_VERSION = "27.1.1"
41 | implementation "com.android.support:appcompat-v7:$SUPPORT_VERSION"
42 | implementation "com.android.support:design:$SUPPORT_VERSION"
43 | implementation 'com.android.support.constraint:constraint-layout:1.1.2'
44 |
45 | // Reactive
46 | final RXJAVA_VERSION = "2.1.12"
47 | final AUTODISPOSE_VERSION = "0.8.0"
48 | implementation "io.reactivex.rxjava2:rxjava:$RXJAVA_VERSION"
49 | implementation "com.uber.autodispose:autodispose-kotlin:$AUTODISPOSE_VERSION"
50 | implementation "com.uber.autodispose:autodispose-android-archcomponents-kotlin:$AUTODISPOSE_VERSION"
51 |
52 | // Dagger
53 | final DAGGER_VERSION = "2.16"
54 | implementation "com.google.dagger:dagger:$DAGGER_VERSION"
55 | kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
56 | implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
57 | implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
58 | kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
59 |
60 | // Android Architecture Components
61 | final ARCH_COMP_VERSION = "1.1.1"
62 | implementation "android.arch.lifecycle:runtime:$ARCH_COMP_VERSION"
63 | implementation "android.arch.lifecycle:extensions:$ARCH_COMP_VERSION"
64 | kapt "android.arch.lifecycle:compiler:$ARCH_COMP_VERSION"
65 |
66 | // Multidex
67 | implementation 'com.android.support:multidex:1.0.3'
68 |
69 | //Retrofit
70 | final RETROFIT_VERSION = "2.4.0"
71 | implementation "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
72 | implementation "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
73 | implementation "com.squareup.retrofit2:adapter-rxjava2:$RETROFIT_VERSION"
74 |
75 | testImplementation 'org.mockito:mockito-core:2.18.0'
76 | testImplementation 'junit:junit:4.12'
77 | testImplementation 'com.github.bright:shouldko:0.1.5'
78 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
79 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/test/java/com/azabost/simplemvvm/ui/main/MainViewModelTests.kt:
--------------------------------------------------------------------------------
1 | package com.azabost.simplemvvm.ui.main
2 |
3 | import com.azabost.simplemvvm.R
4 | import com.azabost.simplemvvm.net.ApiClient
5 | import com.azabost.simplemvvm.net.GitHubService
6 | import com.azabost.simplemvvm.net.response.RepoResponse
7 | import com.azabost.simplemvvm.utils.HttpErrors
8 | import com.azabost.simplemvvm.utils.MockitoUtils
9 | import com.azabost.simplemvvm.utils.getHttpException
10 | import io.reactivex.observers.TestObserver
11 | import io.reactivex.schedulers.TestScheduler
12 | import org.junit.Test
13 | import org.mockito.Mockito
14 | import pl.brightinventions.myparkinson.net.connectivity.FakeConnectivity
15 |
16 | class MainViewModelTests {
17 |
18 | private val gitHubService = Mockito.mock(GitHubService::class.java)
19 | private val connectivity = FakeConnectivity()
20 | private val apiClient = ApiClient(gitHubService, connectivity)
21 | private val testScheduler = TestScheduler()
22 | private val vm = MainViewModel(apiClient)
23 |
24 | private val progressObserver = TestObserver.create().apply {
25 | vm.progress.subscribe(this)
26 | }
27 | private val errorObserver = TestObserver.create().apply {
28 | vm.errors.subscribe(this)
29 | }
30 | private val showDataObserver = TestObserver.create().apply {
31 | vm.data.subscribe(this)
32 | }
33 |
34 | @Test
35 | fun getRepoShouldShowProgress() {
36 | val data = RepoResponse(12345)
37 | mockGetRepoResponse(data)
38 |
39 | vm.getRepoData("any/thing")
40 | progressObserver.assertValue(true)
41 |
42 | testScheduler.triggerActions()
43 | progressObserver.assertValueSequence(listOf(true, false))
44 | }
45 |
46 | @Test
47 | fun getRepoShouldNotShowError() {
48 | val data = RepoResponse(12345)
49 | mockGetRepoResponse(data)
50 |
51 | vm.getRepoData("any/thing")
52 | testScheduler.triggerActions()
53 |
54 | errorObserver.assertEmpty()
55 | }
56 |
57 | @Test
58 | fun getRepoShouldShowData() {
59 | val data = RepoResponse(12345)
60 | mockGetRepoResponse(data)
61 |
62 | vm.getRepoData("any/thing")
63 | testScheduler.triggerActions()
64 |
65 | showDataObserver.assertValue(data)
66 | }
67 |
68 | @Test
69 | fun getRepoErrorShouldShowHttpError() {
70 | mockGetRepoResponse(error = HttpErrors.getHttpException(401))
71 |
72 | vm.getRepoData("any/thing")
73 | testScheduler.triggerActions()
74 |
75 | errorObserver.assertValue(HttpErrors.DEFAULT_HTTP_ERROR_MESSAGE)
76 | }
77 |
78 | @Test
79 | fun getRepoErrorShouldShowHttp404Error() {
80 | mockGetRepoResponse(error = HttpErrors.getHttpException(404))
81 |
82 | vm.getRepoData("any/thing")
83 | testScheduler.triggerActions()
84 |
85 | errorObserver.assertValue(R.string.no_such_repo)
86 | }
87 |
88 | @Test
89 | fun getRepoErrorShouldShowDefaultError() {
90 | mockGetRepoResponse(error = Exception("Failed"))
91 |
92 | vm.getRepoData("any/thing")
93 | testScheduler.triggerActions()
94 |
95 | errorObserver.assertValue(R.string.default_error_message)
96 | }
97 |
98 | @Test
99 | fun shouldShowParsingErrorIfWrongSplits() {
100 | vm.getRepoData("anything")
101 | val error1 = R.string.wrong_repo_format
102 |
103 | vm.getRepoData("any/thing/")
104 | val error2 = R.string.wrong_repo_format
105 |
106 | val errors = arrayOf(error1, error2)
107 | errorObserver.assertValues(*errors)
108 | }
109 |
110 | @Test
111 | fun shouldShowNoInternetConnectionError() {
112 | connectivity.isConnected = false
113 |
114 | vm.getRepoData("any/thing")
115 |
116 | errorObserver.assertValue(R.string.error_no_internet_connection)
117 | }
118 |
119 | private fun mockGetRepoResponse(
120 | response: RepoResponse? = null,
121 | error: Throwable? = null
122 | ) {
123 | MockitoUtils.mockApiResponse(
124 | Mockito.`when`(gitHubService.getRepo(Mockito.anyString(), Mockito.anyString())),
125 | testScheduler,
126 | response,
127 | error
128 | )
129 | }
130 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------