spotify / mobius Goto Github PK
View Code? Open in Web Editor NEWA functional reactive framework for managing state evolution and side-effects.
Home Page: https://spotify.github.io/mobius/
License: Apache License 2.0
A functional reactive framework for managing state evolution and side-effects.
Home Page: https://spotify.github.io/mobius/
License: Apache License 2.0
There appears to be a race condition between MobiusLoop#dispatchEvent
and MobiusLoop#dispose
where eventDispatcher
can accept a new event after it's been disposed because dispatchEvent()
is not synchronized with dispose()
.
Sample app to reproduce the issue: https://github.com/larryng/MobiusBug
I think a possible fix may be to simply set disposed = true
before disposing everything in dispose()
instead of after.
cc @anawara
@pettermahlen @togi it seems the github wiki page can only be modified by owners, do you guys mind updating to include the rxjava3 links and update the docs. Tack in advance!
Please, provide an example of what would be the right way of unit testing Mobius.
I've seen a few Hamcrest matchers in your presentation but, for example, I stopped using Hamcrest in favor of Atrium (fluent assertions). Probably, it would be beneficial to provide a generic way of testing Mobius, not bound to any particular framework. Here is an example of a simple test, that tries to assert the initial model in a stream.
import com.spotify.mobius.First
import com.spotify.mobius.Mobius
import com.spotify.mobius.MobiusLoop
import com.spotify.mobius.Next
import com.spotify.mobius.runners.WorkRunners
import com.spotify.mobius.rx2.RxConnectables
import com.spotify.mobius.rx2.RxMobius
import io.reactivex.Observable
import org.junit.Test
class MobiusUnitTest {
private val loop: MobiusLoop.Factory<Model, Event, Effect> = RxMobius.loop(::update, ::effectHandler)
.init { First.first(InitModel, setOf(InitEffect)) }
.eventRunner { WorkRunners.immediate() }
.effectRunner { WorkRunners.immediate() }
private var controller: MobiusLoop.Controller<Model, Event> = Mobius.controller(loop, InitModel)
private fun update(model: Model, event: Event): Next<Model, Effect> = when (event) {
is InitEvent -> Next.next(InitModel)
}
private fun effectHandler(effects: Observable<Effect>): Observable<Event> = effects.flatMap { effect ->
when (effect) {
is InitEffect -> Observable.just(InitEvent)
}
}
private fun bind(modelToEvent: (Observable<Model>) -> Observable<Event>) {
controller.connect(RxConnectables.fromTransformer(modelToEvent))
controller.start()
}
@Test
fun should_receive_init_model() {
bind { models: Observable<Model> ->
val observer = models.test()
// assert received model?? No model is received...
Observable.empty<Event>()
}
}
}
sealed class Model
object InitModel : Model()
sealed class Event
object InitEvent : Event()
sealed class Effect
object InitEffect : Effect()
Would you be so kind to point out what's missing? Seems like effectHandler
and update
methods are being executed, but I'm still not getting the model in my receiver.
Hi!
First of all, thank you for a great library!
One topic that I didn't find from the Wiki is how to handle errors from Effects properly.
For example, if I use RxMobius, do I need to use .onErrorReturn operator from rx, and put a throwable into the Event, instead of resulting data?
is GetSavedUserEffect -> prefs.getUserSavedCredentials() .map { (login, pass) -> UserCredentialsLoadedEvent(login, pass) } .onErrorReturn { return@onErrorReturn UserCredentialsLoadedEvent("", "", err = it) }
The current suggested way to setup Mobius is to call start() in onStart and stop() in onStop. This lifecycle doesn't allow the system to receive onActivityResult calls since it takes place before onStart calls start()
This will eliminate the need to pass in a set of effects and make the code less verbose.
Will submit a PR with how this can look like.
Hi. Let me preface this question by saying I'm still learning this library as well as RxJava.
My question is: say you've got an effect that performs some fake work on a separate thread, e.g.
fun doFakeWork(work: Observable<DoWorkEffect>): Observable<Event> {
return work
.delay(5, TimeUnit.SECONDS)
.map { DoneLoading }
}
and that's hooked up neatly into an effects handler. And say this effect was a consequence of an event that puts the model into a loading state. If the user rotates their phone and destroys the hosting activity during this loading state, the model's state can be saved and restored via the savedInstanceState bundle -- great. But I'm noticing I have nothing to restore and continue the observable stream from where it left off, meaning the fake work being done in that above effect doesn't go beyond the delay
operator, and thus the DoneLoading
event won't fire to clear the model of its loading state.
How should I be better handling this scenario? I thought to put the effects handler and loop factory into an AAC ViewModel, but the observable stream still ceases upon rotation. I'm not entirely sure why, but I'm guessing it's because the controller still needs to be in an activity/fragment (to bind to the view events, e.g. clicks) and the stream is only subscribed to in that class.
Great work and thank you for releasing this library. Is there an iOS counterpart for mobius in the works?
Great framework but I see this example in wiki
private Connection<MyModel> connectViews(Consumer<MyEvent> eventConsumer) {
// send events to the consumer when the button is pressed
button.setOnClickListener(view ->
eventConsumer.accept(MyEvent.buttonPressed()));
return new Connection<MyModel>() {
public void accept(MyModel model) {
// this will be called whenever there is a new model
textView.setText(model.getValue());
}
public void dispose() {
// don't forget to remove listeners when the UI is disconnected
button.setOnClickListener(null);
}
};
}
But in my case i have complex view and every time accept get called i have to rebind and render the view again which i want to avoid
Currently the way we can subscribe to event source is on the creating Loop stage.
What if I want to subscribe(or unsubscribe) to event source only after specific state in the Model?
First of all, thank you for open sourcing Mobius (and new Swift version also!), it's a great experience to introduce so many pure functions into own code.
I wanted to ask something related to FAQ mentioned in Wiki - Is Mobius an alternative to MV*?. Assuming that we will use ViewModel
from AAC as a VM layer, I was wondering how proper configuration of controller would look like in this layer. I found that it would be a good practice to separate the Mobius from the view elements completely and transfer it to the VM layer. It would have many benefits: view can observe only view model parts, not the whole model, events from UI can be transformed to business logic events or even effect handlers would be separated from the view layer. So I made a simple ViewModel:
class MyViewModel : ViewModel() {
private lateinit var controller: MobiusLoop.Controller<MyModel, MyEvent>
fun onCreate() {
controller = // initialize controller here
controller.connect(RxConnectables.fromTransformer { /* handle model here */ })
}
fun onStart() {
controller.start()
}
fun onStop() {
controller.stop()
}
fun onDestroy() {
controller.disconnect()
}
}
As you can see ViewModel needs to be aware of view lifecycle in order to properly connect/disconnect or start/stop the loop (method names could be different, but I named in this way to reflect the lifecycle methods in this example). It looks a little bit like a boiler plate to put these loop management in every ViewModel. I'm wondering, is there any drawback to extend the loop management to ViewModel lifecycle methods (init and onCleared) as it survives configuration changes? Or maybe it's not a good idea to setup controller inside ViewModel at all? I realize that Mobius gives a lot of flexibility and it's not a scope of the library to force any architecture constraints, but any opinions/help would be very appreciated in this topic.
When I try to sync the downloaded Mobius project, my sync fails with the following error message.
ERROR: No toolchains found in the NDK toolchains folder for ABI with prefix: mips64el-linux-android
Installed version of Android Studio
Android Studio 3.3.2
Build #AI-182.5107.16.33.5314842, built on February 16, 2019
JRE: 1.8.0_152-release-1248-b01 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
macOS 10.13.6
I was able to fix this problem by changing the android build tools Gradle plugin version from 3.0.1
to 3.1.0
.
Can send a PR if required.
When building the app using Proguard I get the following errors. Any hints on what to add in my proguard file?
com.spotify.mobius.First: can't find referenced class com.google.auto.value.AutoValue |
-- | --
com.spotify.mobius.Next: can't find referenced class com.google.auto.value.AutoValue |
com.spotify.mobius.extras.Program: can't find referenced class com.google.auto.value.AutoValue |
com.spotify.mobius.extras.patterns.InnerUpdate: can't find referenced class com.google.auto.value.AutoValue |
org.slf4j.LoggerFactory: can't find referenced class org.slf4j.impl.StaticLoggerBinder |
org.slf4j.MDC: can't find referenced class org.slf4j.impl.StaticMDCBinder |
org.slf4j.MarkerFactory: can't find referenced class org.slf4j.impl.StaticMarkerBinder |
there were 17 unresolved references to classes or interfaces. |
Exception while processing task java.io.IOException: Please correct the above warnings first. |
Correct me if I'm wrong, but judging by the wiki, one has to override Activity/Fragment lifecycle methods in order to start/stop/disconnect mobius controller. This boilerplate code could be avoided with a Lifecycle
support. So far I ended up with a simple kotlin extension function:
class MobiusLifecycleObserver<MODEL, EVENT>(
lifecycle: Lifecycle,
private val controller: MobiusLoop.Controller<MODEL, EVENT>
) : LifecycleObserver {
init {
lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun start() = controller.start()
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun stop() = controller.stop()
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun disconnect() = controller.disconnect()
}
fun <MODEL, EVENT>MobiusLoop.Controller<MODEL, EVENT>.bind(lifecycle: Lifecycle) {
MobiusLifecycleObserver(lifecycle, this)
}
that could be used like this:
controller.connect(...)
controller.bind(lifecycle)
It works like a charm but I'm wondering if a built-in framework support would be beneficial.
While using Mobius in more complex features I come upon scenarios where I am not sure how to optimally define my model. For instance:
I keep a boolean flag in my model in order to know what side effect to trigger (Effect1 vs Effect2) and this boolean flag is updated based on a particular event. However, whenever I get the event I need to update the flag (and thus the model) and this triggers a re-rendering of the view, although it's not needed. Can I avoid this?
I need to update only parts of the view that really changed because some views are costly to re-render (markers on a map). What's the best approach here? If I create a boolean flag indicating what has changed, I will have then to update the flag back to false everywhere I am modifying the model as a result of events coming. This doesn't seem right especially if I have several boolean flags for various things. Alternatively, I thought of triggering a side effect which clears the flag immediately after setting it to true. Thoughts?
To sum up, I need some best practices on how to selectively render only parts of the view, how to deal with one-time boolean flags and how to update some model fields without re-rendering of the view.
What would be the recommended approach to tackle RecylerView infinite scrolling with Mobius? I'm wondering if you guys have any experience/tips to share on the subject.
Any plans to support coroutines and flow?
I have made a module with coroutines and flow support for some of the components. If this seems fine I can finish up the rest of the components and add tests before opening a PR.
https://github.com/msasikanth/mobius-ktx/tree/master/mobius-ktx
Thoughts about adopting Kotlin for mobius? I understand Kotlin can be used with mobius. It'd be nice if mobius was written in Kotlin.
@pettermahlen We are mostly using mobius loops with viewmodel across our app.
What is the use case for a mobius controller?
Can Mobius Controller be thought of an equivalent to VM with loop?
Could you give me actual example use cases for Using Mobius controller over VM with loop?
Originally posted by @rajdheepak in #157 (comment)
Hi!
Thanks for this wonderful library. Can you guys help with proguard rules for the dependencies in this library?
I have this warning in my logs and I was wondering if I should be concerned. Unfortunately I could not reproduce it. What would trigger such a warning and how can I mitigate this?
Long monitor contention with owner main (27632) at void java.lang.Object.wait!(long, int)(Object.java:-2) waiters=0 in void com.spotify.mobius.MobiusLoop$3.accept(java.lang.Object) for 109ms
Are there any plans on integrating the 'new' Architecture components into this library?
I can imagine using LifecycleObserver in the controller could simplify calling all those lifecycle methods mentioned in the wiki
I'm succesfully using Mobius in an Android Kotlin project with MobiusLoopViewModel and Fragments.
For a new project, I am trying to get Mobius running with Kotlin, Jetpack Compose and MobiusLoopViewModel.
The loop is placed within the Viewmodel. Instead of using a Fragment as a host for the ViewModel, a Composable is used.
I created two loops in two Composables, let's call them LoopOne and LoopTwo.
LoopOne is inititialized first, followed by LoopTwo.
Model updates are to be received in the Composable like this:
val modelState: LoopTwoModel by viewModel.models.observeAsState(initialModel)
On the first model update, a class cast exception is thrown in the Composable at that position:
"LoopOneModel cannot be cast to LoopTwoModel"
It seems that the combination of Compose and Mobius leads to some strange type mix-up.
When I move LoopTwo to an Activity it all works as expected, but in a Composable it does not.
I added mobius-android as a local library for better insights.
In the MobiusLoopViewModel class, when observing the loop (Line 100) the wrong LoopOne type is received instead of LoopTwo.
Line 100:
loop.observe(this::onModelChanged);
This wrong type is then posted as LiveData:
Line 230:
private void onModelChanged(M model) {
modelData.postValue(model);
}
This is probably caused by Compose and Java interoperabilty issues.
But I'd still like to share my results and ask for suggestions here.
Would it be possible/feasible to remove final
modifier from MobiusLoopViewModel.onCleared
and replace it with @CallSuper
?
It would preserve the behaviour of the MobiusLoopViewModel
class while allowing easier subclassing and clearing of any non-mobius related resources (i.e. rx bus / room subscriptions).
Hello All.. I have Presenter with two lifcycle OnAttachToView and onDetachFromView which will called for Fragment Lifecycle OnStart and OnStop. The question is "Do i really need the on Pause and On Resume Events for starting and stopping the loop?"
Hi! I've been using this awesome library for over two years now for work (and sometimes for personal use). One of my former colleagues even went through all of the trouble and red-tape of open sourcing Flowbius 🤯
We've been using the wiki as our main point of reference when working with Mobius and for when we onboard folks into the team. It's great. I have no complaints. The wiki is very well organized and does the job just fine. However, I think we can improve on it 😁
Why "fix" something that is not broken? This is one of the all-time most frequently asked questions in software development (IMO). As I mentioned, the wiki is fine. I am not saying the wiki "needs to be fixed". So then why?
MkDocs, in particular the Material for MkDocs...
I will not talk about the pros and cons of GitHub wiki vs GitHub Pages. It's up to you to do that research if you care. It's mostly just preference and opinions, IMO anyways 🤭
I think that you are all aware of MkDocs and have probably considered or are considering using it. Maybe ya'll just don't have the time to do it or just can't be bothered to fix something that ain't broke. Or maybe you even prefer the wiki and are consciously making the decision to use it instead of GitHub Pages + MkDocs.
Either way, I've already done the work for you 🤗. It's been a pleasure to be able to have the opportunity to do this work for one of my all-time favorite FOSS projects. I had fun doing it. As a bonus, I learned even more about Mobius in the process. Regardless of whether you merge my PR or not, thanks for everything 🍻
If you do decide to migrate, you can just merge my PR (or just use it as reference). More info including demo videos and next steps are in the PR.
Here is what it looks like in my fork; https://vestrel00.github.io/mobius/
I noticed there isn't a Mobius channel in the kotlinlang Slack. Where are users/developers of Mobius hanging out?
Thanks for this awesome library! We are now heavily using Mobius in all our apps. Currently, we render views like this,
RxConnectables.fromTransformer(
{ upstreamModelObs: Observable<M> ->
val modelDisposable = upstreamModelObs.distinctUntilChanged().subscribe {
renderView(it)
}
this.eventsObservable()
.doOnDispose { modelDisposable.dispose() }
}
)
Is there any better way to subscribe to upstream model observable to render views with Rx? I'm not sure if subscribing to observable inside transformer is the right way to consume models.
In mobius-android-sample
, I can see that views are rendered this way,
What is the proper way to render views when using Rx library?
add a sample project with kotlin
I've encountered some issues with dispatching effects from Init function.
Let's say that Init looks like this:
override fun init(model: MyModel): First<MyModel, MyEffect> {
return first(model, effects(SomeEffect))
}
and we add Consumer like this:
RxMobius.subtypeEffectHandler<MyEffect, MyEvent>()
.addConsumer(SomeEffect::class.java, { doSomething() }, mainThread())
.build()
Now the problem is that doSomething()
is never called. Interestingly when I use io()
scheduler everything works fine. Can someone give a clue what's the reason?
There is crash in MutableLiveQueue, please look at the stack trace below
java.lang.NullPointerException: Attempt to invoke interface method 'void androidx.lifecycle.Observer.onChanged(java.lang.Object)' on a null object reference
at com.spotify.mobius.android.MutableLiveQueue.lambda$post$0(MutableLiveQueue.java:124)
at com.spotify.mobius.android.-$$Lambda$MutableLiveQueue$vOf0H-vBAqo2vnbWBOWfiBCGWLc.run
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:198)
at android.app.ActivityThread.main(ActivityThread.java:6732)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.