Skip to content

Commit

Permalink
Switch to Redux style view presentation in ItemDetailView.
Browse files Browse the repository at this point in the history
  • Loading branch information
pardom committed Jul 14, 2016
1 parent 4f98ee8 commit 43f9f72
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import clean.news.presentation.inject.ApplicationScope
import clean.news.util.AndroidLogger
import dagger.Module
import dagger.Provides
import rx.Scheduler
import rx.android.schedulers.AndroidSchedulers

@Module(includes = arrayOf(DataModule::class, PresentationModule::class))
class ApplicationModule(private val application: Application) {
Expand All @@ -20,4 +22,10 @@ class ApplicationModule(private val application: Application) {
fun logger(): Logger {
return AndroidLogger()
}

@Provides
@ApplicationScope
fun observeScheduler(): Scheduler {
return AndroidSchedulers.mainThread()
}
}
53 changes: 20 additions & 33 deletions android/src/main/kotlin/clean/news/ui/item/detail/ItemDetailView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,26 @@ import butterknife.bindView
import clean.news.R
import clean.news.adapter.ItemDetailAdapter
import clean.news.app.util.addTo
import clean.news.core.entity.Item
import clean.news.flow.service.DaggerService
import clean.news.presentation.collections.streamMapOf
import clean.news.presentation.model.item.ItemDetailViewModel
import clean.news.presentation.model.item.ItemDetailViewModel.Sinks
import clean.news.presentation.model.item.ItemDetailViewModel.Sources
import clean.news.presentation.model.item.ItemDetailViewModel.Action
import clean.news.ui.item.detail.ItemDetailKey.ItemDetailComponent
import com.jakewharton.rxbinding.support.v7.widget.itemClicks
import com.jakewharton.rxbinding.support.v7.widget.navigationClicks
import com.jakewharton.rxbinding.widget.text
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import redux.Store
import rx.subscriptions.CompositeSubscription
import javax.inject.Inject

class ItemDetailView : RelativeLayout {
class ItemDetailView : RelativeLayout, Store.Subscriber {
@Inject
lateinit var model: ItemDetailViewModel

private val toolbar: Toolbar by bindView(R.id.toolbar)

private val titleTextView: TextView by bindView(R.id.title_text_view)
private val commentRecyclerView: RecyclerView by bindView(R.id.comment_recycler_view)

private val adapter: ItemDetailAdapter

private val subscriptions = CompositeSubscription()

@JvmOverloads
Expand All @@ -59,37 +55,20 @@ class ItemDetailView : RelativeLayout {
override fun onAttachedToWindow() {
super.onAttachedToWindow()

val sinks = model.attach(streamMapOf(
Sources.BACK_CLICKS to toolbar.navigationClicks(),
Sources.URL_CLICKS to Observable.empty<Unit>(),
Sources.SHARE_CLICKS to toolbar.itemClicks()
.filter { it.itemId == R.id.item_share }
.map { Unit }
))

Sinks.ITEM<Item>(sinks)
.map { it.title.orEmpty() }
.subscribe { titleTextView.text() }
toolbar.navigationClicks()
.subscribe { model.store.dispatch(Action.GoBack()) }
.addTo(subscriptions)

Sinks.CHILDREN<List<Item>>(sinks)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
adapter.setItems(it)
adapter.setLoading(true)
},
{},
{
adapter.setLoading(false)
}
)
toolbar.itemClicks().filter { it.itemId == R.id.item_share }
.subscribe { model.store.dispatch(Action.Share()) }
.addTo(subscriptions)

model.store.subscribe(this)
onStateChanged()
}

override fun onDetachedFromWindow() {
subscriptions.unsubscribe()
model.detach()
super.onDetachedFromWindow()
}

Expand All @@ -108,6 +87,14 @@ class ItemDetailView : RelativeLayout {
adapter.setCollapsedIds(savedState.collapsedIds)
}

override fun onStateChanged() {
val state = model.store.getState()
toolbar.title = state.item.title
titleTextView.text = state.item.title
adapter.setItems(state.children)
adapter.setLoading(state.loading)
}

class SavedState : BaseSavedState {
var collapsedCount = 0
var collapsedIds = LongArray(0)
Expand Down
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlinVersion = '1.0.1'
ext.kotlinVersion = '1.0.3'
repositories {
jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
Expand Down Expand Up @@ -91,6 +91,10 @@ ext {
rxBindingAppCompatV7 = "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxBindingVersion"
rxJava = "io.reactivex:rxjava:$rxJavaVersion"

reduxKotlin = "com.github.pardom:redux-kotlin:-SNAPSHOT"
reduxLoggerKotlin = "com.github.pardom:redux-logger-kotlin:-SNAPSHOT"
reduxObservableKotlin = "com.github.pardom:redux-observable-kotlin:-SNAPSHOT"

// Testing

assertJ = "org.assertj:assertj-core:$assertJVersion"
Expand Down
12 changes: 9 additions & 3 deletions presentation/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
apply plugin: 'java'
apply plugin: 'kotlin'

sourceCompatibility = 1.7
targetCompatibility = 1.7

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
Expand All @@ -16,4 +13,13 @@ dependencies {
compile javaxInject
compile kotlinStdLib
compile rxJava

compile(reduxKotlin) { changing = true }
compile(reduxLoggerKotlin) { changing = true }
compile(reduxObservableKotlin) { changing = true }
}

configurations.all {
resolutionStrategy.cacheDynamicVersionsFor 5, 'minutes'
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,112 @@
package clean.news.presentation.model.item

import clean.news.app.usecase.item.GetChildren
import clean.news.app.usecase.item.GetChildren.Request
import clean.news.core.entity.Item
import clean.news.presentation.collections.StreamMap
import clean.news.presentation.collections.streamMapOf
import clean.news.presentation.inject.ScreenScope
import clean.news.presentation.model.Model
import clean.news.presentation.model.item.ItemDetailViewModel.Sinks
import clean.news.presentation.model.item.ItemDetailViewModel.Sources
import clean.news.presentation.model.item.ItemDetailViewModel.Action.GoBack
import clean.news.presentation.model.item.ItemDetailViewModel.Action.GoToUrl
import clean.news.presentation.model.item.ItemDetailViewModel.Action.Share
import clean.news.presentation.model.item.ItemDetailViewModel.Action.ShowChildren
import clean.news.presentation.navigation.NavigationFactory
import clean.news.presentation.navigation.NavigationFactory.ItemDetailScreen
import clean.news.presentation.navigation.NavigationService
import redux.Dispatcher
import redux.Middleware
import redux.Reducer
import redux.Store
import redux.logger.Logger
import redux.logger.Logger.Event
import redux.logger.LoggerMiddleware
import redux.observable.Epic
import redux.observable.EpicMiddleware
import rx.Observable
import rx.Scheduler
import javax.inject.Inject

@ScreenScope(ItemDetailScreen::class)
class ItemDetailViewModel @Inject constructor(
private val observeScheduler: Scheduler,
private val navService: NavigationService,
private val navFactory: NavigationFactory,
private val getChildren: GetChildren,
private val item: Item) : Model<Sources, Sinks>() {
item: Item) {

private val children = getChildren.execute(Request(item))
.map { it.items }
.replay(1)
.autoConnect()
// State

override fun bind(sources: StreamMap<Sources>): StreamMap<Sinks> {
Sources.BACK_CLICKS<Unit>(sources)
.subscribe { navService.goBack() }
data class State(
val item: Item,
val children: List<Item>,
val loading: Boolean)

Sources.URL_CLICKS<Unit>(sources)
.subscribe { navService.replaceTo(navFactory.url(item)) }
private val initialState = State(item, listOf(item), false)

Sources.SHARE_CLICKS<Unit>(sources)
.subscribe { navService.goTo(navFactory.shareDetail(item)) }
// Actions

return streamMapOf(
Sinks.ITEM to Observable.just(item),
Sinks.CHILDREN to children
)
sealed class Action {
class GetChildren(val item: Item) : Action()
class ShowChildren(val children: List<Item>) : Action()
class GoBack() : Action()
class GoToUrl() : Action()
class Share() : Action()
}

enum class Sources : Key {
BACK_CLICKS, URL_CLICKS, SHARE_CLICKS
// Reducer

private val reducer = object : Reducer<State> {
override fun reduce(state: State, action: Any): State {
return when (action) {
is ShowChildren -> state.copy(children = action.children)
else -> state
}
}
}

// Middleware

private val logger = object : Logger<State> {
override fun log(event: Event, action: Any, state: State) {

}
}
private val epic = object : Epic<State> {
override fun map(actions: Observable<out Any>, store: Store<State>): Observable<out Any> {
return actions.ofType(Action.GetChildren::class.java)
.flatMap {
getChildren.execute(GetChildren.Request(it.item))
.observeOn(observeScheduler)
}
.map { Action.ShowChildren(it.items) }
}
}

enum class Sinks : Key {
ITEM, CHILDREN
private val loggerMiddleware = LoggerMiddleware.create(logger)
private val epicMiddleware = EpicMiddleware.create(epic)
private val navigationMiddleware = object : Middleware<State> {
override fun dispatch(store: Store<State>, action: Any, next: Dispatcher): Any {
when (action) {
is GoBack -> navService.goBack()
is GoToUrl -> navService.goTo(navFactory.url(item))
is Share -> navService.goTo(navFactory.shareDetail(item))
}
return action
}

}

// Store

val store = Store.create(
reducer,
initialState,
Middleware.apply(
loggerMiddleware,
epicMiddleware,
navigationMiddleware
)
)

init {
store.dispatch(Action.GetChildren(item))
}

}

0 comments on commit 43f9f72

Please sign in to comment.