Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sample app to demonstrate integration of Koin with MvRx #432

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion buildSrc/src/main/java/dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object Versions {
const val daggerAssisted = "0.5.2"
const val debugDb = "1.0.4"
const val epoxy = "3.9.0"
const val koin = "2.0.1"
const val koin = "2.1.6"
const val lottie = "3.4.0"
const val moshi = "1.9.2"
const val multidex = "2.0.1"
Expand Down Expand Up @@ -70,6 +70,8 @@ object Libraries {
const val fragmentTesting = "androidx.fragment:fragment-testing:${Versions.fragment}"
const val junit = "junit:junit:${Versions.junit}"
const val koin = "org.koin:koin-android:${Versions.koin}"
const val koinExt = "org.koin:koin-androidx-ext:${Versions.koin}"
const val koinViewModelExt = "org.koin:koin-android-viewmodel:${Versions.koin}"
const val kotlin = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}"
const val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}"
const val lifecycleCommon = "androidx.lifecycle:lifecycle-common-java8:${Versions.lifecycle}"
Expand All @@ -95,10 +97,12 @@ object InstrumentedTestLibraries {
const val core = "androidx.test:core:${Versions.testCore}"
const val espresso = "androidx.test.espresso:espresso-core:${Versions.espresso}"
const val junit = "androidx.test.ext:junit:${Versions.junitExt}"
const val junitExt = "androidx.test.ext:junit-ktx:${Versions.junitExt}"
}

object TestLibraries {
const val junit = "junit:junit:${Versions.junit}"
const val koinTest = "org.koin:koin-test:${Versions.koin}"
const val mockito = "org.mockito:mockito-core:${Versions.mockito}"
const val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:${Versions.mockitoKotlin}"
const val mockk = "io.mockk:mockk:${Versions.mockk}"
Expand Down
1 change: 1 addition & 0 deletions hellokoin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
44 changes: 44 additions & 0 deletions hellokoin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
apply plugin: "com.android.application"
apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"
apply plugin: "kotlin-kapt"

android {

defaultConfig {
applicationId "com.airbnb.mvrx.helloKoin"
versionCode 1
versionName "0.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

multiDexEnabled true
}
}

dependencies {
implementation Libraries.koinExt
implementation Libraries.koinViewModelExt
implementation Libraries.appcompat
implementation Libraries.constraintlayout
implementation Libraries.navigationFragmentKtx
implementation Libraries.navigationUiKtx
implementation Libraries.coreKtx
implementation Libraries.fragmentKtx
implementation Libraries.rxJava
implementation Libraries.viewModelKtx
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation project(":mvrx")

debugImplementation Libraries.fragmentTesting

androidTestImplementation InstrumentedTestLibraries.core
androidTestImplementation InstrumentedTestLibraries.espresso
androidTestImplementation InstrumentedTestLibraries.junit
androidTestImplementation InstrumentedTestLibraries.junitExt

testImplementation TestLibraries.junit
testImplementation TestLibraries.mockk
testImplementation TestLibraries.roboeletric
testImplementation TestLibraries.koinTest
testImplementation project(":testing")
}
21 changes: 21 additions & 0 deletions hellokoin/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.airbnb.mvrx.hellokoin

import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.NavHostFragment

fun <A : Fragment> FragmentActivity.findFragmentById(@IdRes id: Int): A {
@Suppress("UNCHECKED_CAST")
return supportFragmentManager.findFragmentById(id) as A
}

inline fun <reified T> FragmentActivity.getCurrentFragment(): T {
val fragment = this.findFragmentById<NavHostFragment>(R.id.rootContainer)
return fragment.childFragmentManager.fragments.firstOrNull() as T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.airbnb.mvrx.hellokoin

import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.filters.MediumTest
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.hellokoin.screens.hello.HelloFragment
import com.airbnb.mvrx.hellokoin.screens.scopedhello.ScopedHelloFragment
import com.airbnb.mvrx.withState
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.TestScheduler
import java.util.concurrent.TimeUnit
import org.hamcrest.CoreMatchers.not
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test

/**
* UI tests for checking correctness of [Koin] and [MvRx] integration in sample app.
*/
@MediumTest
class HelloKoinFragmentsTest {

companion object {
val testScheduler = TestScheduler()

@BeforeClass
@JvmStatic
fun setup() {
RxJavaPlugins.reset()
RxJavaPlugins.setNewThreadSchedulerHandler { testScheduler }
RxJavaPlugins.setComputationSchedulerHandler { testScheduler }
RxJavaPlugins.setInitIoSchedulerHandler { testScheduler }
RxJavaPlugins.setSingleSchedulerHandler { testScheduler }
}
}

@get:Rule
var activityScenarioRule = activityScenarioRule<MainActivity>()

val scenario: ActivityScenario<MainActivity>
get() = activityScenarioRule.scenario

@Test
fun helloFragment_isInLoadingStateWhenCreated() {
onView(withId(R.id.helloButton)).perform(click())

scenario.onActivity { activity: MainActivity ->
val fragment = activity.getCurrentFragment<HelloFragment>()
withState(fragment.viewModel) {
assert(it.message is Loading)
}
}

onView(withId(R.id.messageTextView)).check(matches(withText(R.string.hello_fragment_loading_text)))
onView(withId(R.id.helloButton)).check(matches(not(isEnabled())))
}

@Test
fun scopedHelloFragment_isInLoadingStateWhenCreated() {
onView(withId(R.id.scopedHelloButton)).perform(click())

scenario.onActivity { activity: MainActivity ->
val fragment = activity.getCurrentFragment<ScopedHelloFragment>()
withState(fragment.viewModel) {
assert(it.message is Loading)
}
}

onView(withId(R.id.messageTextView)).check(matches(withText(R.string.hello_fragment_loading_text)))
onView(withId(R.id.helloButton)).check(matches(not(isEnabled())))
}

@Test
fun scopedHelloFragment_isInSuccessStateOnRestart() {
onView(withId(R.id.scopedHelloButton)).perform(click())
scenario.onActivity { activity -> activity.onBackPressed() }

testScheduler.advanceTimeBy(5, TimeUnit.SECONDS)

onView(withId(R.id.scopedHelloButton)).perform(click())

scenario.onActivity { activity: MainActivity ->
val fragment = activity.getCurrentFragment<ScopedHelloFragment>()
withState(fragment.viewModel) {
assert(it.message is Success)
}
}

onView(withId(R.id.messageTextView)).check(matches(withText("Hello, world!")))
onView(withId(R.id.helloButton)).check(matches(isEnabled()))
}
}
24 changes: 24 additions & 0 deletions hellokoin/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.airbnb.mvrx.hellokoin">

<application
android:name="com.airbnb.mvrx.hellokoin.HelloKoinApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.airbnb.mvrx.hellokoin

import android.app.Application
import com.airbnb.mvrx.hellokoin.di.allModules
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class HelloKoinApplication : Application() {

override fun onCreate() {
super.onCreate()

startKoin {
androidContext(this@HelloKoinApplication)
modules(allModules)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.airbnb.mvrx.hellokoin

import io.reactivex.Observable
import java.util.concurrent.TimeUnit

class HelloRepository {

fun sayHello(): Observable<String> {
return Observable
.just("Hello, world!")
.delay(2, TimeUnit.SECONDS)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.airbnb.mvrx.hellokoin

import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity(R.layout.activity_main)
26 changes: 26 additions & 0 deletions hellokoin/src/main/java/com/airbnb/mvrx/hellokoin/MainFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.airbnb.mvrx.hellokoin

import android.os.Bundle
import android.view.View
import androidx.annotation.IdRes
import androidx.navigation.fragment.findNavController
import com.airbnb.mvrx.BaseMvRxFragment
import kotlinx.android.synthetic.main.fragment_main.helloButton
import kotlinx.android.synthetic.main.fragment_main.scopedHelloButton

class MainFragment : BaseMvRxFragment(R.layout.fragment_main) {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
helloButton.setOnClickListener { navigateTo(R.id.action_mainFragment_to_helloFragment) }
scopedHelloButton.setOnClickListener { navigateTo(R.id.action_mainFragment_to_scopedHelloFragment) }
}

override fun invalidate() {
// No-op
}

protected fun navigateTo(@IdRes actionId: Int) {
findNavController().navigate(actionId, null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.airbnb.mvrx.hellokoin

/**
* Class, which instance may be accessed only from specific [Koin] scope.
*/
class ScopedObject
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.airbnb.mvrx.hellokoin.base

import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.hellokoin.BuildConfig

/**
* Base class for ViewModels.
*
* This class sets the 'Debug' mode in a [BaseMvRxViewModel] to the corresponding parameter
* in the [BuildConfig] class.
*/
abstract class BaseViewModel<S : MvRxState>(initialState: S) : BaseMvRxViewModel<S>(initialState, BuildConfig.DEBUG)
Loading