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

Flux architecture #3

Merged
merged 1 commit into from Feb 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions dependencies.gradle
Expand Up @@ -2,6 +2,8 @@ ext {
rx = 'io.reactivex.rxjava2:rxjava:2.1.9'
rxAndroid = 'io.reactivex.rxjava2:rxandroid:2.0.2'
rxKotlin = 'io.reactivex.rxjava2:rxkotlin:2.1.0'
autodisposeKotlin = 'com.uber.autodispose:autodispose-kotlin:0.6.1'
autodispose = 'com.uber.autodispose:autodispose-android-kotlin:0.6.1'

moshi = "com.squareup.moshi:moshi:1.5.0"
moshiKotlin = "com.squareup.moshi:moshi-kotlin:1.5.0"
Expand Down
2 changes: 2 additions & 0 deletions mobile/build.gradle
Expand Up @@ -37,6 +37,8 @@ dependencies {
implementation rootProject.ext.rx
implementation rootProject.ext.rxAndroid
implementation rootProject.ext.rxKotlin
implementation rootProject.ext.autodisposeKotlin
implementation rootProject.ext.autodispose

// data
implementation rootProject.ext.moshi
Expand Down
@@ -1,6 +1,8 @@
package me.soushin.sunshine

import dagger.android.support.DaggerApplication
import me.soushin.sunshine.di.DaggerApplicationComponent
import timber.log.Timber

class KotlinApplication : DaggerApplication() {

Expand All @@ -10,5 +12,9 @@ class KotlinApplication : DaggerApplication() {

override fun onCreate() {
super.onCreate()

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
}
@@ -0,0 +1,12 @@
package me.soushin.sunshine.data.api

import io.reactivex.Single
import me.soushin.sunshine.data.api.dto.Forecasts
import retrofit2.http.GET
import retrofit2.http.Query

interface OpenWeatherMapClient {

@GET("/data/2.5/forecast/daily?mode=json&units=metric&APPID=8fec1d6b295722570a4bc2736d6be386")
fun findForecastByDaily(@Query("q") query: Int = 94043, @Query("cnt") count: Int = 7): Single<Forecasts>
}

This file was deleted.

@@ -1,10 +1,10 @@
package me.soushin.sunshine.data.repository

import me.soushin.sunshine.data.api.OpenWeatherMapService


class OpenWeatherMapRepository(val openWeatherMapService: OpenWeatherMapService) {

fun findForecastByDaily() = openWeatherMapService.findForecastByDaily()
import me.soushin.sunshine.data.api.OpenWeatherMapClient
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class OpenWeatherMapRepository @Inject constructor(private val openWeatherMapClient: OpenWeatherMapClient) {
fun findForecastByDaily() = openWeatherMapClient.findForecastByDaily()
}
14 changes: 13 additions & 1 deletion mobile/src/main/kotlin/me/soushin/sunshine/di/AppModule.kt
Expand Up @@ -4,12 +4,24 @@ import android.content.Context
import dagger.Module
import dagger.Provides
import me.soushin.sunshine.KotlinApplication
import me.soushin.sunshine.ui.base.error.ErrorDispatcher
import me.soushin.sunshine.ui.base.forecasts.ForecastsDispatcher
import javax.inject.Singleton

@Module
internal object AppModule {
@Provides
@Singleton
@JvmStatic
fun provideApplication(app : KotlinApplication): Context = app
fun provideApplication(app: KotlinApplication): Context = app

@Provides
@Singleton
@JvmStatic
fun provideErrorDispatcher() = ErrorDispatcher()

@Provides
@Singleton
@JvmStatic
fun provideForecastsDispatcher() = ForecastsDispatcher()
}
@@ -1,10 +1,11 @@
package me.soushin.sunshine
package me.soushin.sunshine.di

import dagger.BindsInstance
import dagger.Component
import dagger.android.AndroidInjector
import dagger.android.support.AndroidSupportInjectionModule
import me.soushin.sunshine.di.AppModule
import me.soushin.sunshine.KotlinApplication
import me.soushin.sunshine.di.data.ApiModule
import me.soushin.sunshine.di.data.DataModule
import me.soushin.sunshine.di.ui.UiModule
import javax.inject.Singleton
Expand All @@ -13,9 +14,10 @@ import javax.inject.Singleton
@Singleton
@Component(modules = arrayOf(AndroidSupportInjectionModule::class,
AppModule::class,
ApiModule::class,
DataModule::class,
UiModule::class))
interface ApplicationComponent : AndroidInjector<KotlinApplication> {
interface ApplicationComponent : AndroidInjector<KotlinApplication>, ApplicationComponentModules {

@Component.Builder
interface Builder {
Expand Down
@@ -0,0 +1,18 @@
package me.soushin.sunshine.di

import me.soushin.sunshine.data.repository.OpenWeatherMapRepository
import me.soushin.sunshine.ui.base.error.ErrorStore
import me.soushin.sunshine.ui.base.forecasts.ForecastsAction
import me.soushin.sunshine.ui.base.forecasts.ForecastsStore

interface ApplicationComponentModules {

fun openWeatherMapRepository(): OpenWeatherMapRepository

// flux: errors
fun errorStore(): ErrorStore

// flux: forecasts
fun forecastsAction(): ForecastsAction
fun forecastsStore(): ForecastsStore
}
15 changes: 15 additions & 0 deletions mobile/src/main/kotlin/me/soushin/sunshine/di/data/ApiModule.kt
@@ -0,0 +1,15 @@
package me.soushin.sunshine.di.data

import dagger.Module
import dagger.Provides
import me.soushin.sunshine.data.api.OpenWeatherMapClient
import retrofit2.Retrofit
import javax.inject.Singleton

@Module
internal object ApiModule {
@Provides
@Singleton
@JvmStatic
fun provideOpenWeatherMapClient(retrofit: Retrofit) = retrofit.create(OpenWeatherMapClient::class.java)
}
10 changes: 2 additions & 8 deletions mobile/src/main/kotlin/me/soushin/sunshine/di/data/DataModule.kt
Expand Up @@ -5,13 +5,13 @@ import com.squareup.moshi.KotlinJsonAdapterFactory
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import me.soushin.sunshine.data.api.OpenWeatherMapService
import me.soushin.sunshine.data.api.OpenWeatherMapClient
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton

@Module(includes = arrayOf(RepositoryModule::class))
@Module
internal object DataModule {

@Provides
Expand All @@ -36,10 +36,4 @@ internal object DataModule {
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()

@Provides
@Singleton
@JvmStatic
fun provideOpenWeatherMapService(retrofit: Retrofit) = retrofit.create(OpenWeatherMapService::class.java)

}

This file was deleted.

@@ -0,0 +1,118 @@
package me.soushin.sunshine.ui.base

import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.View
import com.uber.autodispose.LifecycleEndedException
import com.uber.autodispose.LifecycleScopeProvider
import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.functions.Function
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.ATTACH
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.CREATE
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.CREATE_VIEW
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.DESTROY
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.DESTROY_VIEW
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.DETACH
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.PAUSE
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.RESUME
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.START
import me.soushin.sunshine.ui.base.AutoDisposeFragmentKotlin.FragmentEvent.STOP

abstract class AutoDisposeFragmentKotlin : Fragment(), LifecycleScopeProvider<AutoDisposeFragmentKotlin.FragmentEvent> {

private val lifecycleEvents = BehaviorSubject.create<FragmentEvent>()

enum class FragmentEvent {
ATTACH, CREATE, CREATE_VIEW, START, RESUME, PAUSE, STOP, DESTROY_VIEW, DESTROY, DETACH
}

override fun lifecycle(): Observable<FragmentEvent> {
return lifecycleEvents.hide()
}

override fun correspondingEvents(): Function<FragmentEvent, FragmentEvent> {
return CORRESPONDING_EVENTS
}

override fun peekLifecycle(): FragmentEvent? {
return lifecycleEvents.value
}

override fun onAttach(context: Context) {
super.onAttach(context)
lifecycleEvents.onNext(FragmentEvent.ATTACH)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleEvents.onNext(FragmentEvent.CREATE)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleEvents.onNext(FragmentEvent.CREATE_VIEW)
}

override fun onStart() {
super.onStart()
lifecycleEvents.onNext(FragmentEvent.START)
}

override fun onResume() {
super.onResume()
lifecycleEvents.onNext(FragmentEvent.RESUME)
}

override fun onPause() {
lifecycleEvents.onNext(FragmentEvent.PAUSE)
super.onPause()
}

override fun onStop() {
lifecycleEvents.onNext(FragmentEvent.STOP)
super.onStop()
}

override fun onDestroyView() {
lifecycleEvents.onNext(FragmentEvent.DESTROY_VIEW)
super.onDestroyView()
}

override fun onDestroy() {
lifecycleEvents.onNext(FragmentEvent.DESTROY)
super.onDestroy()
}

override fun onDetach() {
lifecycleEvents.onNext(FragmentEvent.DETACH)
super.onDetach()
}

companion object {

/**
* This is a function of current event -> target disposal event. That is to say that if event A
* returns B, then any stream subscribed to during A will autodispose on B. In Android, we make
* symmetric boundary conditions. Create -> Destroy, Start -> Stop, etc. For anything after
* Resume we dispose on the next immediate destruction event. Subscribing after Detach is an
* error.
*/
private val CORRESPONDING_EVENTS: Function<FragmentEvent, FragmentEvent> =
Function { lifecycleEvents ->
when (lifecycleEvents) {
ATTACH -> DETACH
CREATE -> DESTROY
CREATE_VIEW -> DESTROY_VIEW
START -> STOP
RESUME -> PAUSE
PAUSE -> STOP
STOP -> DESTROY_VIEW
DESTROY_VIEW -> DESTROY
DESTROY -> DETACH
else -> throw LifecycleEndedException("Cannot bind to Fragment lifecycle after detach.")
}
}
}
}
@@ -0,0 +1,3 @@
package me.soushin.sunshine.ui.base.error

data class Err(val message: String?)
@@ -0,0 +1,9 @@
package me.soushin.sunshine.ui.base.error

import io.reactivex.subjects.PublishSubject

class ErrorDispatcher {
val errors = PublishSubject.create<Err>().toSerialized()

fun onError(err: Err) = errors.onNext(err)
}
@@ -0,0 +1,9 @@
package me.soushin.sunshine.ui.base.error

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ErrorStore @Inject constructor(private val errorDispatcher: ErrorDispatcher) {
fun errors() = errorDispatcher.errors
}
@@ -0,0 +1,24 @@
package me.soushin.sunshine.ui.base.forecasts

import io.reactivex.schedulers.Schedulers
import me.soushin.sunshine.data.repository.OpenWeatherMapRepository
import me.soushin.sunshine.ui.base.error.Err
import me.soushin.sunshine.ui.base.error.ErrorDispatcher
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ForecastsAction @Inject constructor(private val forecastsDispatcher: ForecastsDispatcher,
private val errorDispatcher: ErrorDispatcher,
private val openWeatherMapRepository: OpenWeatherMapRepository) {
fun findByDaily() {
openWeatherMapRepository.findForecastByDaily()
.subscribeOn(Schedulers.io())
.subscribe({
forecastsDispatcher.forecastsProcessor.onNext(it)
}, {
errorDispatcher.onError(Err(it.message))
})
}
}
@@ -0,0 +1,9 @@
package me.soushin.sunshine.ui.base.forecasts

import io.reactivex.processors.PublishProcessor
import me.soushin.sunshine.data.api.dto.Forecasts
import me.soushin.sunshine.ui.base.error.ErrorDispatcher

class ForecastsDispatcher {
val forecastsProcessor = PublishProcessor.create<Forecasts>()
}