├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── nyc │ │ └── friendlyrobot │ │ └── dispatcher │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── nyc │ │ │ └── friendlyrobot │ │ │ └── dispatcher │ │ │ ├── SampleApplication.kt │ │ │ ├── di │ │ │ ├── ActivityComponent.kt │ │ │ ├── AppComponent.kt │ │ │ ├── Injector.kt │ │ │ ├── modules │ │ │ │ ├── ActivityModule.kt │ │ │ │ ├── AppModule.kt │ │ │ │ ├── ScreenModule.kt │ │ │ │ └── StateModule.kt │ │ │ └── qualifiers │ │ │ │ └── ActivityScoped.kt │ │ │ ├── repository │ │ │ └── ItemStore.kt │ │ │ └── ui │ │ │ ├── Dispatcher.kt │ │ │ ├── RxState.kt │ │ │ ├── ScreenCreator.kt │ │ │ ├── State.kt │ │ │ ├── base │ │ │ ├── BasePresenter.kt │ │ │ ├── InjectorActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SlideInOnly.kt │ │ │ └── SlideOutOnly.kt │ │ │ ├── checkout │ │ │ ├── AddressView.kt │ │ │ ├── Cart.kt │ │ │ ├── CartView.kt │ │ │ └── NameView.kt │ │ │ └── search │ │ │ ├── LoadingView.kt │ │ │ ├── ResultsView.kt │ │ │ └── SearchView.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_cart.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_search.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── cart_item.xml │ │ ├── result_item.xml │ │ ├── view_address.xml │ │ ├── view_cart.xml │ │ ├── view_loading.xml │ │ ├── view_name.xml │ │ ├── view_results.xml │ │ └── view_search.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── nyc │ └── friendlyrobot │ └── dispatcher │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | /.idea 12 | /.idea/misc.xml 13 | .idea/ 14 | /.idea/gradle.xml 15 | /.idea/runConfigurations.xml 16 | /.idea/vcs.xml 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Droidcon Italy 2018 sample project 2 | ## slides https://www.slideshare.net/nakhimovich/mike-nakhimovich230leftstage 3 | 4 | Questions? Contact me on twitter @friendlyMikhail 5 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | apply plugin: 'kotlin-kapt' 7 | 8 | android { 9 | compileSdkVersion 27 10 | defaultConfig { 11 | applicationId "nyc.friendlyrobot.dispatcher" 12 | minSdkVersion 21 13 | targetSdkVersion 27 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation 'com.xwray:groupie:2.0.3' 28 | implementation 'com.xwray:groupie-kotlin-android-extensions:2.0.3' 29 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' 30 | 31 | implementation 'com.google.dagger:dagger:2.13' 32 | kapt 'com.google.dagger:dagger-compiler:2.13' 33 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 34 | implementation 'com.jakewharton.timber:timber:4.7.0' 35 | implementation 'io.reactivex.rxjava2:rxjava:2.1.12' 36 | implementation fileTree(dir: 'libs', include: ['*.jar']) 37 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" 38 | implementation 'com.android.support:appcompat-v7:27.1.0' 39 | implementation 'com.android.support:design:27.1.0' 40 | testImplementation 'junit:junit:4.12' 41 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 42 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 43 | } 44 | -------------------------------------------------------------------------------- /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/androidTest/java/nyc/friendlyrobot/dispatcher/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher 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("nyc.friendlyrobot.dispatcher", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/SampleApplication.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher 2 | 3 | import android.app.Application 4 | import nyc.friendlyrobot.dispatcher.di.Injector 5 | 6 | 7 | class SampleApplication : Application() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | Injector.createComponent(this) 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/di/ActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.di 2 | 3 | import dagger.Subcomponent 4 | import nyc.friendlyrobot.dispatcher.di.modules.ActivityModule 5 | import nyc.friendlyrobot.dispatcher.di.modules.ScreenModule 6 | import nyc.friendlyrobot.dispatcher.di.modules.StateModule 7 | import nyc.friendlyrobot.dispatcher.di.qualifiers.ActivityScoped 8 | import nyc.friendlyrobot.dispatcher.ui.base.MainActivity 9 | import nyc.friendlyrobot.dispatcher.ui.checkout.CartView 10 | import nyc.friendlyrobot.dispatcher.ui.checkout.CheckoutAddressView 11 | import nyc.friendlyrobot.dispatcher.ui.checkout.CheckoutNameView 12 | import nyc.friendlyrobot.dispatcher.ui.search.LoadingView 13 | import nyc.friendlyrobot.dispatcher.ui.search.ResultsView 14 | import nyc.friendlyrobot.dispatcher.ui.search.SearchView 15 | 16 | @Subcomponent(modules = arrayOf(ActivityModule::class, 17 | StateModule::class, 18 | ScreenModule::class)) 19 | @ActivityScoped 20 | interface ActivityComponent { 21 | fun inject(activity: MainActivity) 22 | fun inject(view: LoadingView) 23 | fun inject(searchView: SearchView) 24 | fun inject(resultsView: ResultsView) 25 | fun inject(cartView: CartView) 26 | fun inject(checkoutNameView: CheckoutNameView) 27 | fun inject(checkoutAddressView: CheckoutAddressView) 28 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.di 2 | 3 | import android.app.Application 4 | import dagger.BindsInstance 5 | import dagger.Component 6 | import nyc.friendlyrobot.dispatcher.di.modules.ActivityModule 7 | import nyc.friendlyrobot.dispatcher.di.modules.AppModule 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | @Component(modules = arrayOf(AppModule::class)) 12 | interface AppComponent { 13 | @Component.Builder 14 | interface Builder { 15 | fun build(): AppComponent 16 | @BindsInstance 17 | fun application(application: Application): AppComponent.Builder 18 | } 19 | 20 | fun plusActivityComponent(activityModule: ActivityModule): ActivityComponent 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/di/Injector.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.di 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.app.Application 6 | import android.content.Context 7 | import nyc.friendlyrobot.dispatcher.di.modules.ActivityModule 8 | 9 | private val ACTIVIVTY_COMPONENT = "ACTIVITY_COMPONENT" 10 | 11 | object Injector { 12 | 13 | lateinit var appComponent: AppComponent 14 | 15 | fun createComponent(application: Application) { 16 | appComponent = DaggerAppComponent.builder().application(application).build() 17 | } 18 | 19 | fun create(activity: Activity): ActivityComponent { 20 | return appComponent 21 | .plusActivityComponent(ActivityModule(activity)) 22 | } 23 | 24 | fun matchesService(name: String): Boolean { 25 | return ACTIVIVTY_COMPONENT == name 26 | } 27 | 28 | @SuppressLint("WrongConstant") 29 | fun obtain(context: Context): ActivityComponent { 30 | return context.getSystemService(ACTIVIVTY_COMPONENT) as ActivityComponent 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/di/modules/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.di.modules 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import dagger.Module 6 | import dagger.Provides 7 | import nyc.friendlyrobot.dispatcher.di.qualifiers.ActivityScoped 8 | import nyc.friendlyrobot.dispatcher.ui.base.InjectorActivity 9 | 10 | @Module 11 | class ActivityModule constructor(val activity: Activity) { 12 | @Provides 13 | @ActivityScoped 14 | fun provideActivity(): Activity = activity 15 | 16 | @Provides 17 | @ActivityScoped 18 | fun provideBundle(activity: Activity): Bundle = (activity as InjectorActivity).currentBundle 19 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/di/modules/AppModule.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.di.modules 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import java.util.* 6 | 7 | @Module 8 | abstract class AppModule { 9 | 10 | @Module 11 | companion object { 12 | 13 | @Provides 14 | @JvmStatic 15 | fun provideUUID():UUID { 16 | return UUID.randomUUID() 17 | } 18 | 19 | 20 | } 21 | } 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/di/modules/ScreenModule.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.di.modules 2 | 3 | import android.app.Activity 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.multibindings.ClassKey 7 | import dagger.multibindings.IntoMap 8 | import io.reactivex.subjects.PublishSubject 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | import nyc.friendlyrobot.dispatcher.R 11 | import nyc.friendlyrobot.dispatcher.di.qualifiers.ActivityScoped 12 | import nyc.friendlyrobot.dispatcher.ui.* 13 | import java.util.* 14 | 15 | @Module 16 | class ScreenModule { 17 | @Provides 18 | @IntoMap 19 | @ClassKey(Screen.Search::class) 20 | fun provideSearchLayout():Int= R.layout.view_search 21 | 22 | @Provides 23 | @IntoMap 24 | @ClassKey(Screen.Cart::class) 25 | fun provideCart():Int= R.layout.view_cart 26 | 27 | @Provides 28 | @IntoMap 29 | @ClassKey(Screen.CheckoutName::class) 30 | fun provideCheckoutNameLayout():Int= R.layout.view_name 31 | 32 | @Provides 33 | @IntoMap 34 | @ClassKey(Screen.CheckoutAddress::class) 35 | fun provideCheckoutAddressLayout():Int= R.layout.view_address 36 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/di/modules/StateModule.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.di.modules 2 | 3 | import android.app.Activity 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.multibindings.ClassKey 7 | import dagger.multibindings.IntoMap 8 | import io.reactivex.subjects.PublishSubject 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | import nyc.friendlyrobot.dispatcher.R 11 | import nyc.friendlyrobot.dispatcher.di.qualifiers.ActivityScoped 12 | import nyc.friendlyrobot.dispatcher.ui.* 13 | import java.util.* 14 | 15 | @Module 16 | class StateModule { 17 | 18 | @Provides 19 | @ActivityScoped 20 | fun provideScreenContainer(activity: Activity): ScreenContainer { 21 | return object : ScreenContainer { 22 | override fun inflateAndAdd(layout: Int) { 23 | activity.layoutInflater.inflate(layout, activity.container, true) 24 | } 25 | } 26 | } 27 | 28 | @Provides 29 | @ActivityScoped 30 | fun provideMessageSubject(): PublishSubject { 31 | return PublishSubject.create() 32 | } 33 | 34 | @Provides 35 | @ActivityScoped 36 | fun provideBackStack(): Stack { 37 | return Stack() 38 | } 39 | 40 | @Provides 41 | @ActivityScoped 42 | fun dispatcher(backstack: Stack, rxState: RxState): Dispatcher { 43 | return RealDispatcher(backstack = backstack,rxState = rxState) 44 | } 45 | 46 | @Provides 47 | @ActivityScoped 48 | fun rxState(stream: PublishSubject, backstack: Stack): RxState { 49 | return RealRxState(stream, backstack) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/di/qualifiers/ActivityScoped.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.di.qualifiers 2 | 3 | import java.lang.annotation.Documented 4 | import java.lang.annotation.Retention 5 | import java.lang.annotation.RetentionPolicy 6 | import javax.inject.Scope 7 | 8 | @Scope 9 | @Documented 10 | @Retention(RetentionPolicy.RUNTIME) 11 | annotation class ActivityScoped 12 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/repository/ItemStore.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.repository 2 | 3 | import io.reactivex.Observable 4 | import java.util.concurrent.TimeUnit 5 | import javax.inject.Inject 6 | import javax.inject.Singleton 7 | 8 | @Singleton 9 | class ItemStore @Inject constructor() { 10 | private val menuItems = listOf( 11 | "Cheese Pizza", 12 | "Sausage Pizza", 13 | "Pepper Pizza", 14 | "Greek Pizza", 15 | "Supreme Pizza", 16 | "Square Pizza", 17 | "Large Pizza", 18 | "Sprite", 19 | "Coke", 20 | "Pepsi", 21 | "Seltzer", 22 | "Tea" 23 | ) 24 | 25 | fun search(term: String): Observable> { 26 | return Observable 27 | .fromCallable { 28 | menuItems.filter { it.contains(term, ignoreCase = true) } 29 | } 30 | .delaySubscription(500, TimeUnit.MILLISECONDS) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/Dispatcher.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui 2 | 3 | import nyc.friendlyrobot.dispatcher.di.qualifiers.ActivityScoped 4 | import timber.log.Timber 5 | import java.util.* 6 | 7 | interface Dispatcher { 8 | fun dispatch(state: State) 9 | fun goBack() 10 | fun popScreensAndGoBack(vararg screenClasses: Class) 11 | fun clearBackstack() 12 | } 13 | 14 | 15 | @ActivityScoped 16 | class RealDispatcher 17 | constructor(val rxState: RxState, private val backstack: Stack) : Dispatcher { 18 | override fun dispatch(state: State) { 19 | when (state) { 20 | is Screen -> { 21 | state.forward = true 22 | rxState.push(Creating(state)) 23 | } 24 | is Showing -> { 25 | if (state.screen.replace && !backstack.empty()) backstack.pop() 26 | if (backstack.empty() || backstack.peek().screen != state.screen) backstack.push(state) 27 | rxState.push(state) 28 | Timber.d("pushing %s", backstack.peek().screen.toString()) 29 | } 30 | else -> rxState.push(state) 31 | } 32 | } 33 | 34 | override fun goBack() { 35 | dispatch(State.GoingBack) 36 | popLastShowingState()//current state is what we are currently showing 37 | reShowTopScreen() 38 | } 39 | 40 | override fun popScreensAndGoBack(vararg screenClasses: Class) { 41 | while (backstack.isNotEmpty()) { 42 | val topScreen = backstack.peek() 43 | if (topScreen.screen::class.java in screenClasses) { 44 | backstack.pop() 45 | } else { 46 | break 47 | } 48 | } 49 | reShowTopScreen() 50 | } 51 | 52 | override fun clearBackstack() { 53 | backstack.clear() 54 | } 55 | 56 | private fun reShowTopScreen() { 57 | val backTo = popLastShowingState() 58 | backTo.screen.forward = false 59 | dispatch(backTo) 60 | } 61 | 62 | private fun popLastShowingState(): Showing { 63 | 64 | return if (backstack.isEmpty()) { 65 | Showing.BackStackEmpty 66 | } else { 67 | Timber.d("popping " + backstack.peek()) 68 | backstack.pop() 69 | } 70 | } 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/RxState.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.subjects.PublishSubject 5 | import nyc.friendlyrobot.dispatcher.di.qualifiers.ActivityScoped 6 | import java.util.* 7 | import javax.inject.Inject 8 | 9 | interface RxState { 10 | fun showingNot(clazz: Class): Observable 11 | fun showing(clazz: Class): Observable 12 | fun showing(): Observable 13 | fun isBackStackEmpty(): Boolean 14 | fun push(state: State) 15 | fun creating(): Observable 16 | fun backStackEmpty(): Observable 17 | fun ofType(clazz: Class): Observable 18 | } 19 | 20 | @ActivityScoped 21 | class RealRxState @Inject 22 | constructor(private val stateSubject: PublishSubject, 23 | private val backstack: Stack) : RxState { 24 | override fun push(state: State) { 25 | stateSubject.onNext(state) 26 | } 27 | 28 | override fun showingNot(clazz: Class): Observable { 29 | return stateSubject 30 | .ofType(Showing::class.java) 31 | .filter { !clazz.isInstance(it.screen) } 32 | } 33 | 34 | override fun showing(clazz: Class): Observable { 35 | return showing() 36 | .filter { clazz.isInstance(it.screen) } 37 | } 38 | 39 | override fun showing(): Observable { 40 | return stateSubject.ofType(Showing::class.java) 41 | } 42 | 43 | override fun creating() = stateSubject.ofType(Creating::class.java) 44 | 45 | override fun ofType(clazz: Class):Observable = stateSubject.ofType(clazz) 46 | 47 | override fun backStackEmpty() = stateSubject.ofType(Showing.BackStackEmpty::class.java) 48 | 49 | override fun isBackStackEmpty(): Boolean { 50 | return backstack.isEmpty() || backstack.peek().screen == Screen.Empty 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/ScreenCreator.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui 2 | 3 | 4 | import android.annotation.SuppressLint 5 | import nyc.friendlyrobot.dispatcher.di.qualifiers.ActivityScoped 6 | import javax.inject.Inject 7 | 8 | 9 | interface ScreenContainer { 10 | fun inflateAndAdd(layout: Int) 11 | } 12 | 13 | @ActivityScoped 14 | class MyScreenCreator @Inject constructor(val rxState: RxState, 15 | val dispatcher: Dispatcher, 16 | val screenContainer: ScreenContainer, 17 | val layouts: @JvmSuppressWildcards Map, Int>) : ScreenCreator { 18 | 19 | val inflatedViews: MutableSet = HashSet() 20 | 21 | init { 22 | subscribeToDispatcher() 23 | } 24 | 25 | 26 | @SuppressLint("CheckResult") 27 | override fun subscribeToDispatcher() { 28 | rxState.creating() 29 | .subscribe({ 30 | createScreen(it) 31 | }) 32 | } 33 | 34 | override fun createScreen(screenToCreate: Creating) { 35 | layouts.get(screenToCreate.screen.javaClass)?.let { 36 | if (!inflatedViews.contains(it)) { 37 | inflatedViews.add(it) 38 | screenContainer.inflateAndAdd(it) 39 | } 40 | dispatcher.dispatch(Showing(screenToCreate.screen)) 41 | } 42 | } 43 | } 44 | 45 | interface ScreenCreator { 46 | fun subscribeToDispatcher() 47 | fun createScreen(screenToCreate: Creating) 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/State.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui 2 | 3 | sealed class State { 4 | object GoingBack : State() 5 | object Loading : State() 6 | data class Results(val items: List) : State() 7 | data class AddToCart(val item: String) : State() 8 | data class cartItems(val items: HashMap) : State() 9 | } 10 | 11 | data class Creating(val screen: Screen) : State() 12 | 13 | open class Showing(val screen: Screen) : State() { 14 | object BackStackEmpty : Showing(Screen.Empty) 15 | } 16 | 17 | sealed class Screen : State() { 18 | 19 | var forward = true 20 | var replace = false 21 | 22 | object Empty : Screen() 23 | 24 | object Search : Screen() 25 | object Cart:Screen() 26 | object CheckoutName : Screen() 27 | data class CheckoutAddress(val firstName:String, val lastName: String) : Screen() 28 | } 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/base/BasePresenter.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.base 2 | 3 | import android.arch.lifecycle.Lifecycle 4 | import android.arch.lifecycle.LifecycleObserver 5 | import android.arch.lifecycle.OnLifecycleEvent 6 | import io.reactivex.disposables.CompositeDisposable 7 | 8 | interface Presenter { 9 | 10 | fun attachView(mvpView: V) 11 | 12 | fun detachView() 13 | } 14 | 15 | open class BasePresenter : Presenter, LifecycleObserver { 16 | 17 | protected val disposables = CompositeDisposable() 18 | 19 | var mvpView: T? = null 20 | private set 21 | 22 | val isViewAttached: Boolean 23 | get() = mvpView != null 24 | 25 | override fun attachView(mvpView: T) { 26 | this.mvpView = mvpView 27 | } 28 | 29 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) 30 | override fun detachView() { 31 | disposables.clear() 32 | mvpView = null 33 | } 34 | 35 | fun checkViewAttached() { 36 | if (!isViewAttached) throw MvpViewNotAttachedException() 37 | } 38 | 39 | class MvpViewNotAttachedException : RuntimeException("Please save Presenter.attachView(MvpView) before" + " requesting data to the Presenter") 40 | } 41 | 42 | interface MvpView -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/base/InjectorActivity.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.base 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import nyc.friendlyrobot.dispatcher.di.ActivityComponent 6 | import nyc.friendlyrobot.dispatcher.di.Injector 7 | 8 | fun unsafeLazy(block: () -> T) = lazy(LazyThreadSafetyMode.NONE) { block() } 9 | 10 | abstract class InjectorActivity : AppCompatActivity() { 11 | 12 | 13 | 14 | val currentBundle: Bundle by unsafeLazy { 15 | intent.extras 16 | } 17 | 18 | val activityComponent: ActivityComponent by unsafeLazy { 19 | Injector.create(this) 20 | } 21 | 22 | override fun getSystemService(name: String): Any? { 23 | return if (Injector.matchesService(name)) { 24 | activityComponent 25 | } else super.getSystemService(name) 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/base/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.base 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.transition.TransitionManager 8 | import android.view.Gravity 9 | import android.view.ViewGroup 10 | import android.widget.Toast 11 | import kotlinx.android.synthetic.main.activity_main.* 12 | import nyc.friendlyrobot.dispatcher.R 13 | import nyc.friendlyrobot.dispatcher.ui.* 14 | import nyc.friendlyrobot.dispatcher.ui.checkout.Cart 15 | import javax.inject.Inject 16 | 17 | class MainActivity : InjectorActivity() { 18 | 19 | @Inject 20 | lateinit var rxState: RxState 21 | 22 | @Inject 23 | lateinit var dispatcher: Dispatcher 24 | 25 | @Inject 26 | lateinit var myScreenCreator: MyScreenCreator 27 | 28 | @Inject 29 | lateinit var cart: Cart 30 | 31 | @SuppressLint("CheckResult") 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | 35 | activityComponent.inject(this) 36 | setContentView(R.layout.activity_main) 37 | 38 | rxState.backStackEmpty().subscribe { setResult(Activity.RESULT_OK, Intent()); finish() } 39 | rxState.ofType(State.AddToCart::class.java).subscribe { Toast.makeText(this,"Adding ${it.item} ",Toast.LENGTH_SHORT).show() } 40 | 41 | dispatcher.dispatch(Screen.Search) 42 | 43 | } 44 | 45 | override fun onBackPressed() { 46 | TransitionManager.beginDelayedTransition(container as ViewGroup, SlideOutOnly(Gravity.RIGHT)) 47 | 48 | dispatcher.goBack() 49 | 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/base/SlideInOnly.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.base 2 | 3 | import android.animation.Animator 4 | import android.transition.Slide 5 | import android.transition.TransitionValues 6 | import android.view.ViewGroup 7 | 8 | class SlideInOnly(slideEdge: Int) : Slide(slideEdge) { 9 | override fun onDisappear(sceneRoot: ViewGroup?, startValues: TransitionValues?, startVisibility: Int, endValues: TransitionValues?, endVisibility: Int): Animator? { 10 | return null 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/base/SlideOutOnly.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.base 2 | 3 | import android.animation.Animator 4 | import android.transition.Slide 5 | import android.transition.TransitionValues 6 | import android.view.ViewGroup 7 | 8 | class SlideOutOnly(slideEdge: Int) : Slide(slideEdge) { 9 | override fun onAppear(sceneRoot: ViewGroup?, startValues: TransitionValues?, startVisibility: Int, endValues: TransitionValues?, endVisibility: Int): Animator? { 10 | return null 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/checkout/AddressView.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.checkout 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.support.constraint.ConstraintLayout 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import io.reactivex.android.schedulers.AndroidSchedulers 9 | import kotlinx.android.synthetic.main.view_address.view.* 10 | import nyc.friendlyrobot.dispatcher.di.Injector 11 | import nyc.friendlyrobot.dispatcher.ui.Dispatcher 12 | import nyc.friendlyrobot.dispatcher.ui.RxState 13 | import nyc.friendlyrobot.dispatcher.ui.Screen 14 | import nyc.friendlyrobot.dispatcher.ui.base.BasePresenter 15 | import nyc.friendlyrobot.dispatcher.ui.base.MvpView 16 | import javax.inject.Inject 17 | 18 | interface CheckoutAddressMVPView : MvpView { 19 | fun show(checkoutAddress: Screen.CheckoutAddress) 20 | fun hide() 21 | } 22 | 23 | class CheckoutAddressView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : 24 | ConstraintLayout(context, attrs, defStyle), CheckoutAddressMVPView { 25 | 26 | @Inject 27 | lateinit var presenter: CheckoutAddressPresenter 28 | 29 | 30 | init { 31 | Injector.obtain(getContext()).inject(this) 32 | } 33 | 34 | override fun onFinishInflate() { 35 | super.onFinishInflate() 36 | presenter.attachView(this) 37 | submitAddress.setOnClickListener { presenter.submitAddress(firstAddressView.text.toString(), lastAddressView.text.toString()) } 38 | } 39 | 40 | override fun show(checkoutAddress: Screen.CheckoutAddress) { 41 | visibility = View.VISIBLE 42 | nameDisplay.text = "Hello ${checkoutAddress.firstName} ${checkoutAddress.lastName}" 43 | } 44 | 45 | 46 | override fun hide() { 47 | visibility = View.GONE 48 | } 49 | } 50 | 51 | 52 | class CheckoutAddressPresenter @Inject constructor(val dispatcher: Dispatcher, 53 | val rxState: RxState) : BasePresenter() { 54 | 55 | @SuppressLint("CheckResult") 56 | override fun attachView(mvpView: CheckoutAddressMVPView) { 57 | super.attachView(mvpView) 58 | 59 | rxState.showing(Screen.CheckoutAddress::class.java) 60 | .observeOn(AndroidSchedulers.mainThread()) 61 | .subscribe { mvpView.show((it.screen as Screen.CheckoutAddress)) } 62 | 63 | rxState.showingNot(Screen.CheckoutAddress::class.java) 64 | .observeOn(AndroidSchedulers.mainThread()) 65 | .subscribe { mvpView.hide() } 66 | } 67 | 68 | fun submitAddress(firstAddress: String, lastAddress: String) { 69 | dispatcher.dispatch(Screen.CheckoutAddress(firstAddress, lastAddress)) 70 | } 71 | } 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/checkout/Cart.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.checkout 2 | 3 | import android.annotation.SuppressLint 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import nyc.friendlyrobot.dispatcher.di.qualifiers.ActivityScoped 6 | import nyc.friendlyrobot.dispatcher.ui.Dispatcher 7 | import nyc.friendlyrobot.dispatcher.ui.RxState 8 | import nyc.friendlyrobot.dispatcher.ui.State 9 | import javax.inject.Inject 10 | 11 | @ActivityScoped 12 | class Cart @Inject constructor(val rxState: RxState, 13 | val dispatcher: Dispatcher) { 14 | 15 | private val cartItems = HashMap() 16 | 17 | init { 18 | setup() 19 | } 20 | 21 | @SuppressLint("CheckResult") 22 | private fun setup() { 23 | rxState.ofType(State.AddToCart::class.java) 24 | .subscribe { incrementCount(it.item) } 25 | 26 | rxState.showing() 27 | .observeOn(AndroidSchedulers.mainThread()) 28 | .subscribe { dispatcher.dispatch(State.cartItems(cartItems)) } 29 | } 30 | 31 | private fun incrementCount(item: String) { 32 | if (cartItems.containsKey(item)) cartItems[item] = cartItems[item]!!.inc() 33 | else cartItems[item] = 1 34 | dispatcher.dispatch(State.cartItems(cartItems)) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/checkout/CartView.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.checkout 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Build 6 | import android.support.annotation.RequiresApi 7 | import android.support.constraint.ConstraintLayout 8 | import android.transition.Slide 9 | import android.transition.TransitionManager 10 | import android.util.AttributeSet 11 | import android.view.View 12 | import android.view.ViewGroup 13 | import com.xwray.groupie.GroupAdapter 14 | import com.xwray.groupie.ViewHolder 15 | import io.reactivex.android.schedulers.AndroidSchedulers 16 | import kotlinx.android.synthetic.main.cart_item.view.* 17 | import kotlinx.android.synthetic.main.view_cart.view.* 18 | import nyc.friendlyrobot.dispatcher.R 19 | import nyc.friendlyrobot.dispatcher.di.Injector 20 | import nyc.friendlyrobot.dispatcher.ui.Dispatcher 21 | import nyc.friendlyrobot.dispatcher.ui.RxState 22 | import nyc.friendlyrobot.dispatcher.ui.Screen 23 | import nyc.friendlyrobot.dispatcher.ui.State 24 | import nyc.friendlyrobot.dispatcher.ui.base.BasePresenter 25 | import nyc.friendlyrobot.dispatcher.ui.base.MvpView 26 | import javax.inject.Inject 27 | 28 | interface CartMVPView : MvpView { 29 | fun show() 30 | fun hide() 31 | fun populateAdapter(items: HashMap) 32 | } 33 | 34 | class CartView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : 35 | ConstraintLayout(context, attrs, defStyle), CartMVPView { 36 | 37 | @Inject 38 | lateinit var presenter: CartPresenter 39 | 40 | @Inject 41 | lateinit var dispatcher: Dispatcher 42 | 43 | private val groupAdapter = GroupAdapter() 44 | 45 | 46 | init { 47 | Injector.obtain(getContext()).inject(this) 48 | } 49 | 50 | override fun onFinishInflate() { 51 | super.onFinishInflate() 52 | presenter.attachView(this) 53 | setupRecycler() 54 | } 55 | 56 | override fun show() { 57 | visibility = View.VISIBLE 58 | } 59 | 60 | private fun setupRecycler() { 61 | cartRecycler.apply { 62 | adapter = groupAdapter 63 | } 64 | } 65 | 66 | @RequiresApi(Build.VERSION_CODES.KITKAT) 67 | override fun populateAdapter(items: HashMap) { 68 | groupAdapter.clear() 69 | items.forEach { groupAdapter.add(CartItem(it)) } 70 | groupAdapter.setOnItemClickListener { item, view -> presenter.addItem((item as CartItem).itemText) } 71 | checkout.setOnClickListener { 72 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 73 | TransitionManager.beginDelayedTransition(parent as ViewGroup, Slide()) 74 | } 75 | presenter.goToCheckout() 76 | } 77 | } 78 | 79 | override fun hide() { 80 | visibility = View.GONE 81 | } 82 | } 83 | 84 | class CartItem(val itemText: Map.Entry) : com.xwray.groupie.kotlinandroidextensions.Item() { 85 | override fun bind(viewHolder: com.xwray.groupie.kotlinandroidextensions.ViewHolder, position: Int) { 86 | viewHolder.itemView.cartItemName.text = itemText.key 87 | viewHolder.itemView.cartItemAmount.text = itemText.value.toString() 88 | } 89 | 90 | override fun getLayout() = R.layout.cart_item 91 | } 92 | 93 | 94 | 95 | class CartPresenter @Inject constructor(val dispatcher: Dispatcher, 96 | val rxState: RxState, 97 | val cart: Cart) : BasePresenter() { 98 | 99 | 100 | @SuppressLint("CheckResult") 101 | override fun attachView(mvpView: CartMVPView) { 102 | super.attachView(mvpView) 103 | 104 | rxState.showing(Screen.Cart::class.java) 105 | .observeOn(AndroidSchedulers.mainThread()) 106 | .subscribe { mvpView.show() } 107 | 108 | rxState.showingNot(Screen.Cart::class.java) 109 | .observeOn(AndroidSchedulers.mainThread()) 110 | .subscribe { mvpView.hide() } 111 | 112 | rxState.ofType(State.cartItems::class.java) 113 | .observeOn(AndroidSchedulers.mainThread()) 114 | .subscribe { mvpView.populateAdapter(it.items); } 115 | } 116 | 117 | fun addItem(itemText: Map.Entry) { 118 | dispatcher.dispatch(State.AddToCart(itemText.key)) 119 | } 120 | 121 | fun goToCheckout() { 122 | dispatcher.dispatch(Screen.CheckoutName) 123 | } 124 | } 125 | 126 | 127 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/checkout/NameView.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.checkout 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Build 6 | import android.support.constraint.ConstraintLayout 7 | import android.transition.Slide 8 | import android.transition.TransitionManager 9 | import android.util.AttributeSet 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import io.reactivex.Observable 13 | import io.reactivex.android.schedulers.AndroidSchedulers 14 | import kotlinx.android.synthetic.main.view_name.view.* 15 | import nyc.friendlyrobot.dispatcher.di.Injector 16 | import nyc.friendlyrobot.dispatcher.ui.Dispatcher 17 | import nyc.friendlyrobot.dispatcher.ui.RxState 18 | import nyc.friendlyrobot.dispatcher.ui.Screen 19 | import nyc.friendlyrobot.dispatcher.ui.base.BasePresenter 20 | import nyc.friendlyrobot.dispatcher.ui.base.MvpView 21 | import javax.inject.Inject 22 | import javax.inject.Singleton 23 | 24 | interface CheckoutNameMVPView : MvpView { 25 | fun show(it: Pair) 26 | fun hide() 27 | } 28 | 29 | class CheckoutNameView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : 30 | ConstraintLayout(context, attrs, defStyle), CheckoutNameMVPView { 31 | 32 | @Inject 33 | lateinit var presenter: CheckoutNamePresenter 34 | 35 | 36 | init { 37 | Injector.obtain(getContext()).inject(this) 38 | } 39 | 40 | override fun onFinishInflate() { 41 | super.onFinishInflate() 42 | presenter.attachView(this) 43 | submitName.setOnClickListener { 44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 45 | TransitionManager.beginDelayedTransition(parent as ViewGroup, Slide()) 46 | } 47 | presenter.submitName(firstNameView.text.toString(), lastNameView.text.toString()) 48 | } 49 | } 50 | 51 | override fun show(names: Pair) { 52 | visibility = View.VISIBLE 53 | firstNameView.setText(names.first) 54 | lastNameView.setText(names.second) 55 | } 56 | 57 | 58 | override fun hide() { 59 | visibility = View.GONE 60 | } 61 | } 62 | 63 | 64 | class CheckoutNamePresenter @Inject constructor(val dispatcher: Dispatcher, 65 | val rxState: RxState, 66 | val userRepository: UserRepository) : BasePresenter() { 67 | 68 | @SuppressLint("CheckResult") 69 | override fun attachView(mvpView: CheckoutNameMVPView) { 70 | super.attachView(mvpView) 71 | 72 | rxState.showing(Screen.CheckoutName::class.java) 73 | .flatMap { userRepository.getNames()} 74 | .observeOn(AndroidSchedulers.mainThread()) 75 | .subscribe { mvpView.show(it) } 76 | 77 | rxState.showingNot(Screen.CheckoutName::class.java) 78 | .observeOn(AndroidSchedulers.mainThread()) 79 | .subscribe { mvpView.hide() } 80 | } 81 | 82 | fun submitName(firstName: String, lastName: String) { 83 | userRepository.update(firstName, lastName) 84 | .observeOn(AndroidSchedulers.mainThread()) 85 | .subscribe { dispatcher.dispatch(Screen.CheckoutAddress(firstName, lastName)) } 86 | } 87 | } 88 | 89 | //Save data locally & sync to network 90 | @Singleton 91 | class UserRepository @Inject constructor() { 92 | var firstNameMockDBField: String = "" 93 | var lastNameMockDBField: String = "" 94 | 95 | fun update(firstName: String, lastName: String): Observable { 96 | firstNameMockDBField = firstName 97 | lastNameMockDBField = lastName 98 | return Observable.fromCallable { "success!" } 99 | } 100 | 101 | fun getNames(): Observable>? { 102 | return Observable.fromCallable {Pair(firstNameMockDBField,lastNameMockDBField)} 103 | } 104 | 105 | } 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/search/LoadingView.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.search 2 | 3 | import android.content.Context 4 | import android.support.constraint.ConstraintLayout 5 | import android.util.AttributeSet 6 | import android.view.View 7 | import io.reactivex.android.schedulers.AndroidSchedulers 8 | import nyc.friendlyrobot.dispatcher.di.Injector 9 | import nyc.friendlyrobot.dispatcher.ui.Dispatcher 10 | import nyc.friendlyrobot.dispatcher.ui.RxState 11 | import nyc.friendlyrobot.dispatcher.ui.State 12 | import nyc.friendlyrobot.dispatcher.ui.base.BasePresenter 13 | import nyc.friendlyrobot.dispatcher.ui.base.MvpView 14 | import javax.inject.Inject 15 | 16 | class LoadingView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : 17 | ConstraintLayout(context, attrs, defStyle), LoadingMVPView { 18 | 19 | @Inject 20 | lateinit var presenter: LoadingPresenter 21 | 22 | init { 23 | Injector.obtain(getContext()).inject(this) 24 | } 25 | 26 | override fun onFinishInflate() { 27 | super.onFinishInflate() 28 | presenter.attachView(this) 29 | } 30 | 31 | override fun show() { 32 | visibility= View.VISIBLE 33 | } 34 | 35 | override fun hide() { 36 | visibility=View.INVISIBLE 37 | } 38 | 39 | 40 | } 41 | 42 | class LoadingPresenter @Inject constructor(val dispatcher: Dispatcher, 43 | val rxState: RxState): BasePresenter() { 44 | 45 | override fun attachView(mvpView: LoadingMVPView) { 46 | super.attachView(mvpView) 47 | 48 | rxState.ofType(State.Loading::class.java) 49 | .observeOn(AndroidSchedulers.mainThread()) 50 | .subscribe { mvpView.show() } 51 | 52 | rxState.ofType(State.Results::class.java) 53 | .observeOn(AndroidSchedulers.mainThread()) 54 | .subscribe { mvpView.hide() } 55 | } 56 | } 57 | 58 | interface LoadingMVPView : MvpView { 59 | fun show() 60 | fun hide() 61 | } 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/search/ResultsView.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.search 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.support.constraint.ConstraintLayout 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import com.xwray.groupie.GroupAdapter 9 | import com.xwray.groupie.ViewHolder 10 | import io.reactivex.android.schedulers.AndroidSchedulers 11 | import kotlinx.android.synthetic.main.result_item.view.* 12 | import kotlinx.android.synthetic.main.view_results.view.* 13 | import nyc.friendlyrobot.dispatcher.R 14 | import nyc.friendlyrobot.dispatcher.di.Injector 15 | import nyc.friendlyrobot.dispatcher.ui.Dispatcher 16 | import nyc.friendlyrobot.dispatcher.ui.RxState 17 | import nyc.friendlyrobot.dispatcher.ui.Screen 18 | import nyc.friendlyrobot.dispatcher.ui.State 19 | import nyc.friendlyrobot.dispatcher.ui.base.BasePresenter 20 | import nyc.friendlyrobot.dispatcher.ui.base.MvpView 21 | import javax.inject.Inject 22 | 23 | class ResultsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : 24 | ConstraintLayout(context, attrs, defStyle), ResultsMVPView { 25 | 26 | @Inject 27 | lateinit var presenter: ResultsPresenter 28 | 29 | private val groupAdapter = GroupAdapter() 30 | 31 | 32 | init { 33 | Injector.obtain(getContext()).inject(this) 34 | } 35 | 36 | override fun onFinishInflate() { 37 | super.onFinishInflate() 38 | presenter.attachView(this) 39 | setupRecycler() 40 | } 41 | 42 | override fun show(items: List) { 43 | visibility = View.VISIBLE 44 | populateAdapter(items) 45 | } 46 | 47 | private fun setupRecycler() { 48 | myRecylcer.apply { 49 | adapter = groupAdapter 50 | } 51 | } 52 | 53 | private fun populateAdapter(items: List) { 54 | groupAdapter.clear() 55 | items.forEach { groupAdapter.add(ResultItem(it)) } 56 | groupAdapter.setOnItemClickListener { item, view -> presenter.addToCart((item as ResultItem).itemText) } 57 | } 58 | 59 | override fun hide() { 60 | visibility = View.INVISIBLE 61 | } 62 | } 63 | 64 | 65 | class ResultsPresenter @Inject constructor(val dispatcher: Dispatcher, 66 | val rxState: RxState) : BasePresenter() { 67 | 68 | @SuppressLint("CheckResult") 69 | override fun attachView(mvpView: ResultsMVPView) { 70 | super.attachView(mvpView) 71 | 72 | rxState.ofType(State.Results::class.java) 73 | .observeOn(AndroidSchedulers.mainThread()) 74 | .subscribe { mvpView.show((it as State.Results).items) } 75 | 76 | rxState.ofType(State.Loading::class.java) 77 | .observeOn(AndroidSchedulers.mainThread()) 78 | .subscribe { mvpView.hide() } 79 | 80 | rxState.showingNot(Screen.Search.javaClass).subscribe { mvpView.hide() } 81 | } 82 | 83 | fun addToCart(item: String) { 84 | dispatcher.dispatch(State.AddToCart(item)) 85 | } 86 | } 87 | 88 | interface ResultsMVPView : MvpView { 89 | fun show(items: List) 90 | fun hide() 91 | } 92 | 93 | 94 | class ResultItem(val itemText: String) : com.xwray.groupie.kotlinandroidextensions.Item() { 95 | override fun bind(viewHolder: com.xwray.groupie.kotlinandroidextensions.ViewHolder, position: Int) { 96 | viewHolder.itemView.resultItem.text = itemText 97 | } 98 | 99 | override fun getLayout() = R.layout.result_item 100 | 101 | 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/nyc/friendlyrobot/dispatcher/ui/search/SearchView.kt: -------------------------------------------------------------------------------- 1 | package nyc.friendlyrobot.dispatcher.ui.search 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Build 6 | import android.support.constraint.ConstraintLayout 7 | import android.text.Editable 8 | import android.text.TextWatcher 9 | import android.transition.Slide 10 | import android.transition.TransitionManager 11 | import android.util.AttributeSet 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import io.reactivex.android.schedulers.AndroidSchedulers 15 | import io.reactivex.schedulers.Schedulers 16 | import kotlinx.android.synthetic.main.view_search.view.* 17 | import nyc.friendlyrobot.dispatcher.di.Injector 18 | import nyc.friendlyrobot.dispatcher.repository.ItemStore 19 | import nyc.friendlyrobot.dispatcher.ui.Dispatcher 20 | import nyc.friendlyrobot.dispatcher.ui.RxState 21 | import nyc.friendlyrobot.dispatcher.ui.Screen 22 | import nyc.friendlyrobot.dispatcher.ui.State 23 | import nyc.friendlyrobot.dispatcher.ui.base.BasePresenter 24 | import nyc.friendlyrobot.dispatcher.ui.base.MvpView 25 | import javax.inject.Inject 26 | 27 | class SearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : 28 | ConstraintLayout(context, attrs, defStyle), SearchMVPView { 29 | 30 | 31 | @Inject 32 | lateinit var presenter: SearchPresenter 33 | 34 | 35 | init { 36 | Injector.obtain(getContext()).inject(this) 37 | } 38 | 39 | override fun onFinishInflate() { 40 | super.onFinishInflate() 41 | presenter.attachView(this) 42 | searchText.addTextChangedListener(object :TextWatcher{ 43 | override fun afterTextChanged(s: Editable?) { 44 | presenter.getResults(searchText.text.toString()) 45 | } 46 | 47 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { 48 | } 49 | 50 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 51 | } 52 | 53 | }) 54 | cartButton.setOnClickListener { 55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 56 | TransitionManager.beginDelayedTransition(parent as ViewGroup, Slide()) 57 | } 58 | 59 | presenter.openCart() 60 | } 61 | } 62 | 63 | override fun show() { 64 | visibility = View.VISIBLE 65 | } 66 | 67 | override fun itemAdded() { 68 | val count:Int=cartButton.text.toString().split(" ")[1].toInt(); 69 | cartButton.text = "Items: ${(count + 1)}" 70 | } 71 | 72 | override fun hide() { 73 | visibility = View.INVISIBLE 74 | } 75 | } 76 | 77 | class SearchPresenter 78 | @Inject constructor(val dispatcher: Dispatcher, 79 | val rxState: RxState, 80 | val itemStore: ItemStore) : BasePresenter() { 81 | 82 | @SuppressLint("CheckResult") 83 | override fun attachView(mvpView: SearchMVPView) { 84 | super.attachView(mvpView) 85 | 86 | rxState.showing(Screen.Search::class.java) 87 | .observeOn(AndroidSchedulers.mainThread()) 88 | .subscribe { mvpView.show() } 89 | 90 | 91 | rxState.ofType(State.AddToCart::class.java) 92 | .observeOn(AndroidSchedulers.mainThread()) 93 | .subscribe { mvpView.itemAdded() } 94 | } 95 | 96 | @SuppressLint("CheckResult") 97 | fun getResults(searchTerm: String) { 98 | itemStore.search(searchTerm) 99 | .doOnSubscribe { dispatcher.dispatch(State.Loading) } 100 | .subscribeOn(Schedulers.io()) 101 | .observeOn(AndroidSchedulers.mainThread()) 102 | .subscribe { dispatcher.dispatch(State.Results(it)) } 103 | 104 | } 105 | 106 | fun openCart() { 107 | dispatcher.dispatch(Screen.Cart) 108 | } 109 | } 110 | 111 | interface SearchMVPView : MvpView { 112 | fun show() 113 | fun hide() 114 | fun itemAdded() 115 | } 116 | 117 | -------------------------------------------------------------------------------- /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/res/drawable/ic_cart.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/cart_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 19 | 20 | 23 | 24 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/result_item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_address.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 28 | 29 | 37 | 38 | 46 | 47 | 48 | 56 | 57 | 64 | 65 | 66 |