Skip to content
This repository has been archived by the owner on Apr 30, 2024. It is now read-only.

Add RxRedux sample #33

Merged
merged 5 commits into from
Aug 29, 2018
Merged

Add RxRedux sample #33

merged 5 commits into from
Aug 29, 2018

Conversation

artem-zinnatullin
Copy link
Contributor

@artem-zinnatullin artem-zinnatullin commented Aug 27, 2018

PR adds sample of Domic integration with RxRedux made by @sockeqwe at Freelitics

Please note that while I seem to understand ideas of Redux and some pros/cons, it might be not very idiomatic.

General thoughts on Domic + Redux/MVI:

  • Domic's efficient rendering with automatic state diffing is playing really well with Redux, you can run the app and see yourself
  • Domic becomes nice unified way of providing observables for inputActions produced by View layer, however not much profit here compared to RxBinding
  • Domic's test module becomes useless unless you want to use it for functional in-memory tests for your Redux app on JVM

General thoughts on Redux:

  • I've faced quite a lot of boilerplate/productivity problems, mainly this is due to not being able to copy() previous state to produce new one
  • Sharing some logic for similar actions becomes strange, see ChangeEmail and ChangePassword action handling in state machine
  • Redux generally is very verbose
  • I do feel pretty safe because compiler forces me to check a lot of states because of sealed classes everywhere
  • However it's still not checking all combinations of possible states and if you try to write code that way it just becomes crazy to reason about state combinations

Some thoughts on RxRedux:

  • It's nice, I find it easier to reason about than Mobius, I guess mainly because I'm comfortable with RxJava and RxRedux is built directly on top of it
  • I'm still not sure if StateAccessor was necessary, compared to just passing current state as parameter, I'll have to try in more complicated scenarios when something can happen in parallel to async action I guess
  • Kotlin's typealiases are finally useful lol

cc @sockeqwe, code review from you would be reaaaaaly appreciated :)

@artem-zinnatullin artem-zinnatullin added the enhancement New feature or request label Aug 27, 2018
@artem-zinnatullin artem-zinnatullin added this to the v0.1.0 milestone Aug 27, 2018
@artem-zinnatullin artem-zinnatullin self-assigned this Aug 27, 2018
@artem-zinnatullin
Copy link
Contributor Author

Added a bit more about Redux in general

private val signInButton = AndroidButton(root.findViewById(R.id.sign_in_button), renderer)
private val resultTextView = AndroidTextView(root.findViewById(R.id.sign_result_text_view), renderer)

override val actions: Observable<SignInAction> = Observable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn’t it just a fancy .merge though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, definitely, I was like there is a way, but didn't want to spend too much time on it, will refactor, thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foxed, thx

data class ChangePassword(val password: CharSequence) : SignInAction()
object SignIn : SignInAction()
object ShowSigningInUi : SignInAction()
object ShowSignInSuccessfulUi : SignInAction()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, *Ui* postfixes — good old days.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not happy about it either : (

Do you have other names on mind?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just SigningIn , SignInSuccessful, SignInFailure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, I thought Actions should be verbs, but ok

object SignIn : SignInAction()
object ShowSigningInUi : SignInAction()
object ShowSignInSuccessfulUi : SignInAction()
data class ShowSignInFailureUi(val cause: Throwable) : SignInAction()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never got a good reason why it is necessary to pass Throwable around.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be logged or transformed to particular text (then it should be done not in View layer though)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While passing around Throwable adds some value for logging and so on it also has some downsides: Usually you are not in control how the Throwable is created and hence testing via `assertEquals(expected = ShowSignInFailureUi(mockedException), actual) is hard.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair, I'll keep it for now because it's how it's done in other samples, created issue to track it #34

@Test
fun `displays success ui`() {
inputActions.accept(SignInAction.ChangeEmail("test@email"))
inputActions.accept(SignInAction.ChangePassword("passw0rd"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can at least use JUnit 5 nesting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I don't have time to deal with JUnit5 setup

@jamolkhon
Copy link

If you simply pass the state it may change by the time you use it. StateAccessor is needed to access the current version of the state.

@artem-zinnatullin
Copy link
Contributor Author

Yeah I understand, that's why I said that I probably need more real life experience with long running async actions

At the same time this kinda breaks the idea of functional pureness a bit, at least for me, maybe it's pragmatic tho

@@ -0,0 +1,10 @@
package com.lyft.domic.samples.redux.rxredux.signin

sealed class SignInAction {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could move this action class in SignInStateMachine.kt file. The advantage is that you can make some Actions like ShowSigningInUi and ShowSignInSuccessfulUi that are actually just used internally for SideEffect private and not expose it to the public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's a nice idea

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could get the actions private atm, but moving them to the StateMachine allowed to have better class name, so they're now just State and Action which looks much cleaner with all other advices you and Artur gave

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*Could not get

data class ChangePassword(val password: CharSequence) : SignInAction()
object SignIn : SignInAction()
object ShowSigningInUi : SignInAction()
object ShowSignInSuccessfulUi : SignInAction()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just SigningIn , SignInSuccessful, SignInFailure?

object SignIn : SignInAction()
object ShowSigningInUi : SignInAction()
object ShowSignInSuccessfulUi : SignInAction()
data class ShowSignInFailureUi(val cause: Throwable) : SignInAction()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While passing around Throwable adds some value for logging and so on it also has some downsides: Usually you are not in control how the Throwable is created and hence testing via `assertEquals(expected = ShowSignInFailureUi(mockedException), actual) is hard.

package com.lyft.domic.samples.redux.rxredux.signin

sealed class SignInState(
open val email: CharSequence,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open val email: CharSequence creates private fileds in SignInstate but you also create the same fields in subclasses like SignInState.Idle with override val email: CharSequence. I think the correct way would be:

sealed class SignInState {
   abstract val email: CharSequence
   abstract val password: CharSequence
   abstract val signInButtonEnabled: Boolean

   data class Idle(
             override val email: CharSequence,
             override val password: CharSequence,
             override val signInButtonEnabled: Boolean
     ) : SignInState()

   ... 
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair


inputActions.accept(SignInAction.SignIn)

service.signIn.accept(SignInResult.Success)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking since it's not the aim of this repo to show best practices on testing but this feels super strange to me in combination with

class TestSignInService : SignInService()
       ...

        override fun signIn(credentials: SignInService.Credentials): Observable<SignInService.SignInResult> {
            signInCredentialsRelay.accept(credentials)
            return signIn
        }
}

especially this part:

signInCredentialsRelay.accept(credentials)
return signIn

Theoretically, if you run your application with TestSignInService instead of RealSignInService, does this even work? I'm not sure if I like it but again, it's about domic not about testing :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, what's wrong with it? How would you simulate an emission from the rx based business logic?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I just realiized that I commented on the wrong source file.
There is nothing wrong with service.signIn.accept(SignInResult.Success). Just TestSignInService implementation feels strange (not wrong, just strange 😄 )

is SignInService.SignInResult.Error -> SignInAction.ShowSignInFailureUi(result.cause)
}
}
.startWith(SignInAction.ShowSigningInUi)
Copy link

@sockeqwe sockeqwe Aug 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro optimization: Not sure if the startWith() is needed because Reducer could already produce the right state just by getting SignInAction.SignIn. See duplicated code in reducer:

is SignInAction.SignIn -> SignInState.SigningIn(
                email = state.email,
                password = state.password,
                signInButtonEnabled = false
        )
is SignInAction.ShowSigningInUi -> SignInState.SigningIn(
                email = state.email,
                password = state.password,
                signInButtonEnabled = false
        )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I wrote this side-effect before the reducer and didn't notice that logic was duplicated after that

Interestingly, both distinctUntilChanged() on the reduxStore and Domic's diffing make stuff like this work without screen flickering and go unnoticed

@sockeqwe
Copy link

sockeqwe commented Aug 27, 2018

Great job 👍

Some thoughts on your thoughts 😸

  • Domic's test module becomes useless unless you want to use it for functional in-memory tests for your Redux app on JVM

As I (usually) prefer integration test over unit tests, I still see some value in having functional in-memory tests. But yeah, you could just trigger actions directly, it just a matter of where (at which layer) your functional in-memory tests start. Same for "rendering state". Of course you could just test your state machine but if its easy and cheap to test rendering on UI too (which seems to be the case with domic's test), then why not include this layer too? I think its MainActivityTest (or whatever you would like to call it that includes SignInView and SignInStateMachine) vs. SignInStateMachineTest where MainActivityTest also includes actually testing real SignInStateMachine.

  • I've faced quite a lot of boilerplate/productivity problems, mainly this is due to not being able to copy() previous state to produce new one

Not sure what you mean? I mean, you do that in reducer in you code?!?

  • Sharing some logic for similar actions becomes strange, see ChangeEmail and ChangePassword action handling in state machine
  • Redux generally is very verbose

I feel your pain. I think this sample (it's essentially just doing a http request) is simple enough that it actually does not necessary need previous state to compute next state and therefore no state machine or redux store (or .scan() operator) at all. Hence it might feel a bit more boilerplate or verbose or even overkill compared to your mvvm implementation. Imho its because RxJava actually is already somehow a state machine with onNext and onError and thats all you need in this example.

  • I'm still not sure if StateAccessor was necessary, compared to just passing current state as parameter, I'll have to try in more complicated scenarios when something can happen in parallel to async action I guess

Right, we weren't happy with this either and it caused me holding back open sourcing RxRedux for quite some time. We were looking for a better solution to that problem but eventually we didn't find a better way (if you know a better way I would be very happy to hear it). Eventually we decided to open source it with StateAccessor ...
Passing just current state in as a parameter might be enough in most cases but yeah in more complex / async scenarios you might need a way to grab the latest state at any given point in time which you can do with StateAccessor.

  • Kotlin's typealiases are finally useful lol

🙏

@artem-zinnatullin
Copy link
Contributor Author

I've addressed your feedback and actually like it much more now, especially due to abstract instead of open properties in State class and some renamings, boilerplate reduced quite a lot. It still bothers me in the reducer(), but I might get used to it.

Overall I like the integration, it definitely doesn't feel anywhere near as native as MVVM + Domic, but it's not bad at all. Especially after addressing your feedback.

I must try the DSL-based approach as well though, to better understand the feel of React + Redux. A lot of people are (and were) super hyped about that exact combination for some reason, right.

Thanks for review, @sockeqwe and @ming13, you know how much I appreciate that 😽

@vanniktech I'll keep it open for a day or two, it's not a functional update so there is no pressure in merging asap, you might have some comments I would assume :)

@vanniktech
Copy link

Yeah wanted to wait until you address the feedback of others so we don't get too crazy here.

Copy link

@vanniktech vanniktech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting

override fun render(signInState: Observable<State>): Disposable {
val state = signInState
.replayingShare()
.observeOn(Schedulers.computation())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this need to be the main thread? 4 lines below you do what looks like an operation that needs to run on the main thread.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's one of the major points of Domic, you're supposed to run your code off the main thread, Domic's rendering will take care of switching to main thread as efficient as we can


override fun render(signInState: Observable<State>): Disposable {
val state = signInState
.replayingShare()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need replayingShare()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, you need share or replayingShare to allow multiple streams down below in this method to observe same state

otherwise state Observable will run multiple times if it's Cold

}
}


Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: empty lines

emailEditText
.observe
.textChanges
.map { Action.ChangeEmail(it) },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emailEditText
             .observe
            .textChanges
            .map { Action.ChangeEmail(it) },

Duplicate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yis

password = state.password,
signInButtonEnabled = false
)
is Action.SigningIn -> State.SigningIn(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can group is Action.SignIn and is Action.SigningIn together:

when(action) {
 ...
  is Action.SignIn,
  is Action.SigningIn -> State.SigningIn( 
                 email = state.email,
                 password = state.password,
                 signInButtonEnabled = false
         )
  ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I was actually using multi is statement for ChangeEmail and ChangePassword, but quickly learned that Kotlin compiler doesn't narrow the type variance there and still requires me to check all the branches of is inside that block

gladly for this SignIn and SigningIn combination I don't need to look up into their fields



val state: Observable<State> = inputActions
.observeOn(computationScheduler)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that necessary?
With that the signInButtonEnabled = false will only apply to at least the next frame. The user could tap it many times?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that let's to move computations that don't need run on main thread from main thread thus making it more capable for handling other stuff like animations

observeOn(computationScheduler) on its own doesn't mean that rendering will happen on next frame, frame window is normally 16.6ms, computation scheduler can finish this particular code within 1ms

However, rendering in Domic (with standard renderer implementation) always happens on next frame anyway, but as efficient as we can otherwise.

Note that normally Handler.post() will give you similar behavior

This was discussed with Adam Powell (Android Framework UI lead and we agreed that one frame is nothing for non-game applications and is totally fine if stable 60 or more fps is more important)

There is also api for manual rendering asap: #19

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok nice

observeOn(computationScheduler) on its own doesn't mean that rendering will happen on next frame, frame window is normally 16.6ms, computation scheduler can finish this particular code within 1ms

Until very recently, observeOn(AndroidSchedulers.mainThread()) would have been using Handler.post() behind the scenes so this would have been the cause for the application to the next frame.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, I know, you can always use API provided in #19, if you care about rendering in current frame (which most utility apps shouldn't really care about)

@sockeqwe
Copy link

@artem-zinnatullin I think you should just merge this to master and people will / can provide further improvement by sending a PR ...

@artem-zinnatullin
Copy link
Contributor Author

Sounds good 👍

@artem-zinnatullin artem-zinnatullin merged commit deb2889 into master Aug 29, 2018
@artem-zinnatullin artem-zinnatullin deleted the az/add-rxredux-sample branch August 29, 2018 16:00
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants