ww-tech / roxie

Lightweight Android library for building reactive apps.
Apache License 2.0
482 stars 36 forks source link

Changes occurring independent of user interaction #7

Open ScottCooper92 opened 5 years ago

ScottCooper92 commented 5 years ago

Hey there, I've been reading the wiki to figure out if this library is right for me. I like the idea of mapping actions into changes and having the UI respond to state changes but I feel like the use case might be a bit narrow.

Quite often a change occurs as a result of something other than user interaction, as an example let's say my app is polling a server for live sports results, normally these changes would come through the backend of the app and update a property in the ViewModel/Presenter which the UI is observing.

Is it possible (or even necessary) to represent external events as actions and treat them the same as user interaction, especially if they aren't coming from the UI layer?

jshvarts commented 5 years ago

Hey @Glurt. You say that the change occurs as a result of something other than user interaction yet the UI is observing the result of that change? Then why not have an Action from UI initiating the lookup and emitting the States as Flowable or Observable streams so whenever polling a server results in new data, new State will be emitted to the UI.

Ethan1983 commented 5 years ago

@jshvarts Even though it works, that approach leads to some tightly coupled design. UI needs to be aware of the source which can influence the data like network etc.

A better design is for UI to just rely on the redux store as source of truth and not care about how the actions/events causes data change. Other user initiated UI actions, platform initiated events (broadcast, sensor, location update etc), backend server triggered events (domain logic, push notification etc) would just need to update the store. All consumers of the store (analytics, logging) including UI will just work out of the box.

jshvarts commented 5 years ago

Can you point me to some other redux implementations on Android that do something similar? We have not had the need to do this but PRs are welcome

Ethan1983 commented 5 years ago

Not a redux implementation per se but Room's LiveData update is an example. It doesn't matter how the data was updated in the database (network/UI or other background tasks), all registered observers (including UI if in resumed state) are notified of the new update via LiveData. It offers a better separation of concerns, makes testing easier and encourages a plug-in design.

As it looks many implementations of UDF works well when UI is the only beneficiary/driver of the data store but not with consumers like services, work manager tasks etc.

ScottCooper92 commented 5 years ago

I've just finished refactoring one of the screens in my app which is using Room as a datasource, the ViewModel is setup to receive the initial Action from a Fragment to load some data from the Repository, my Repo gives me back a Data Class containing multiple Observables, one of them is Observable<PagedList>, the others relate to the network state and errors encountered when requesting more data to load into Room.

Each of these Observables is mapped to a Change and they're all then merged into a single stream to pass through the reducer to then be mapped into a State. Any time one of them fires, the State changes and the UI is updated.

So in essence, we can treat external changes as Actions without explicitly defining them in the ViewModel, the ViewModel just needs to know how to map these external events to a Change.

I'm not sure if this is the best way of approaching this but it seems to work for my scenario, I'd imagine that if you needed to listen for broadcasts or notifications you could map them into Changes like I am.

jshvarts commented 5 years ago

So looks like we have a good solution here. Treat background/external changes as Actions just like we treat user-initiated events as Actions. After all the Action is just a simple data container. Whether an Action is user-initiated or an external, it will go thru generating Change(s) and State(s). If anyone has a different suggestion, please share.

curioustechizen commented 5 years ago

^ This seems to be the most logical approach. It lends itself to easy testing, and it keeps everything consistent.

My only concern with this approach is that it could end up exposing some Actions to the Activity that should not be.


//Inside MyActivity

dispatchAction(Action.UserInitiatedAction) //OK

dispatchAction(Action.ExternalAction) //Not all ExternalActions should be visible to Activity

Depending on how you declare your Actions, perhaps some visibility modifier magic might mitigate this.

ScottCooper92 commented 5 years ago

It may not even be necessary to define the Actions for external changes.

For UI it makes sense because the Activity/Fragment knows about the ViewModel and needs to send something to the ViewModel so that it knows what it needs to react to. UserInitiatedAction is a good way of representing that.

External events are probably going to come from multiple places that don't know about the ViewModel, like repositories, callbacks etc. therefore they don't know about the Actions defined within. If the ViewModel implements a listener it can simply emit a Change using a Subject and have that Subject feed into the the reducer like so

private val externalChange = PublishSubject.create<Change>()
....
init{
    .....
    disposables.add(
        Observables.merge(userChange1, userChange2, externalChange)
        ....
    )
}

override fun externalChangeListener(data: Any){
    externalChange.onNext(Change.External(data))
}
jshvarts commented 5 years ago

Interesting idea @Glurt. Will try it out

JotraN commented 5 years ago

@Glurt, that's something similar to what we've done - except rather than an explicit Change in the view model, we just bind it to the action's stream.


fun bindActions() {
     val load = actions.ofType<Action.Load>()
            .switchMap { 
                useCase.load() // Our normal load stuff.
                    .map<Change> { Change.Load(it) }
                    .onErrorReturn { Change.LoadError(it) }
                    .mergeWith(useCase.triggerSomePublisherEmitterThing().map<Change> { Change.External })  // Our external change stuff.
                    .onErrorReturn { Change.ErrorWithPublisherEmitterThing(it) }
                    ...
            }
}
curioustechizen commented 5 years ago

@Glurt This is how we have implemented it right now: ViewModel adds a listener for the external change, and in response to the change it emits a Change object. The problem is testing this is a little more involved than testing it if it were to dispatch an Action.