ReactiveX / RxSwift

Reactive Programming in Swift
MIT License
24.36k stars 4.16k forks source link

BehaviorSubject vs Variable vs other subjects. Public and private read-write/read-only properties. #487

Closed zdnk closed 8 years ago

zdnk commented 8 years ago

Hello.

I will start with what I know.

If I understand it correctly, Variable is type that allows storing value (read-write access) and wraps BehaviorSubject which I have no idea what is for. And Observable is type that allows read-only access.

My issue is regarding combineLatest operator which in Rx.playground allows to combine multiple BehaviorSubjects, but not Variables.Variables can be also Observables vis asObservable() method. Is there any difference between BehaviorSubject and Variable? Which should I use for common storing of values and objects in my UIViewControllers? Or should I use other Subject? What if I want to have publicly read-only Variable/other subject/Observable and other read-write also public?

Another thing is Driver or other Units. Should I Always use Driver when working with UI elements? How? Should I have Variable or other Subject and use asDriver() every time I need all shit going on UI/main thread? In ReactiveWeatherExample there is not Driver used in UIViewController and it is instead manually ensured, the code executes on UI thread. For example in Bond you use Observable type to for all - storing values and subscribing for events/new values.

My current code with combineLatest:

        let varA = Variable(23)
        let varB = Variable(false)
        let button = UIButton()

        Observable.combineLatest(varA.asObservable(), varB.asObservable()) { (a, b) -> Bool in
            if a > 0 && b == true {
                return true
            }

            return false
        }
            .bindTo(button.rx_enabled)
            .addDisposableTo(disposeBag)
kzaher commented 8 years ago

*Subject are more intended as a way to customize behaviors of certain operators.

Variable exists because people usually have a hard time finding BehaviorSubject and Swift has additional complexity of memory management.

The reason why variable needs to be transformed using asObservable interface is because in that way we can assure that you can do this:

        let varA = Variable(23)
        let varB = Variable(false)
        let button = UIButton()

        _ = Observable.combineLatest(varA.asObservable(), varB.asObservable()) { (a, b) -> Bool in
            if a > 0 && b == true {
                return true
            }

            return false
        }
            .bindTo(button.rx_enabled)
            // .addDisposableTo(disposeBag) <-- no need to add to dispose bad for this particular case

This is a good and a bad thing.

So in short:

The reason why we have created Driver is to help people use the compiler to prove certain properties of their programs. I would personally use it to model stateful abstractions in the UI layer. That's why we've created it.

I would also suggest people to create their own abstractions that express properties they require and wrap observables. We've been recently asked will there be units like Single in this project. That's another excellent example of highly useful unit, but we won't implement it directly in project, although I can see myself creating a small Single unit, or someone else creating it, and publishing it to github.

My advice is to use the compiler as much as you can, but how individuals use it, it's up to them.

I wouldn't want to comment on approaches that external libraries (like Bond) or example apps (ReactiveWeatherExample) take because we haven't been related in any way with them.

We've tried to add a lot of example usages to RxExample app, so if there is some question about some of examples there, I would be happy to answer them.

zdnk commented 8 years ago

OK, I think I am a little bit closer to understanding the whole concept.

Another question popped in my head while reading your answer. What if I would like to implement something like Promise pattern? Meaning I have some async tasks that I need to have executed serially dependent on each other, obviously I can achieve this by concat I suppose? But how could I implement something a little bit cleaner for chaining tasks, or could you point in some direction or to some example of chaining Observables that run and complete serially?

I see, Variable is sort of storage type like variable or constant in Swift. It definitely looks better and more clean in code than BehaviorSubject or PublishSubject for example.

What is best practice in FRP with RXSwift then? Having all properties in classes, controllers, view models as Subjects and/or Units?

kzaher commented 8 years ago

The answer to first question is actually flatMap instead of concat :)

All of these things you have mentioned are equivalent to observable sequences, and thus composable. I would probably prefer Variable over BehaviorSubject because of the mentioned compile time guarantees.

We've tried to explain and provide examples how one could best use other concepts in RxExample app. I'm not sure there is a better way how I can explain where to use which concept other then those examples and all documentation we have.

The general rules of thumb are:

zdnk commented 8 years ago

Some exposed properties are supposed to have read-write access, Other read-only and the rest of them are private.

So possibly for public read-only case I could do something like:

private let _property = Variable(true)
public var property: Driver<Bool> {
  return _property.asDriver()
}

Would that be correct approach?

kzaher commented 8 years ago

Hi @zdenektopic ,

your suggestion will probably work, but this is not idiomatic for sure and not an approach I would suggest people to take.

This is one of the examples in the example app:

https://github.com/ReactiveX/RxSwift/blob/master/RxExample/RxExample/Examples/GitHubSignup/UsingDriver/GithubSignupViewModel2.swift

It also has an explanation

 /**
         Notice how no subscribe call is being made. 
         Everything is just a definition.
         Pure transformation of input sequences to output sequences.
        */

There is also the same example using vanilla observable sequences:

https://github.com/ReactiveX/RxSwift/blob/master/RxExample/RxExample/Examples/GitHubSignup/UsingVanillaObservables/GithubSignupViewModel1.swift

These are kind of ideal examples IMHO.

Important things:

If you are more into Redux kind of architectures, take a look at calculator example:

https://github.com/ReactiveX/RxSwift/tree/master/RxExample/RxExample/Examples/Calculator

zdnk commented 8 years ago

Thanks, helped me a lot to understand how ViewModels should be built using RxSwift! :)

If you have some commands/actions in view model that come from view controller, you have to have subscribe in the view model code, don't you?

kzaher commented 8 years ago

Hm, why do you think you need to subscribe in view model? I don't remember we've called subscribe, drive or bind in any view model in RxExample app.

Word never is a tricky word and I don't like to use it, maybe I should say almost never to be more precise :)

mingyeow commented 8 years ago

This article uses behavior subjects extensively. Would you say that is not the ideal implementation?

https://medium.com/cobe-mobile/implementing-mvvm-in-ios-with-rxswift-458a2d47c33d#.oucl7alcp

kzaher commented 8 years ago

@mingyeow I think that code could be improved, so yes, it's probably not the most ideal implementation IMHO.

kzaher commented 8 years ago

We can probably close this one and reopen if needed.

serejahh commented 7 years ago

Hi @kzaher! An example with no subscribe, bind, drive looks interesting, but one thing really embarrasses me - you must have a loaded view to configure your view model. For example, if I want to create a view model out side of a view controller (e.g. build a whole module using builder pattern), it will call loadView before it's really needed. Don't you think it's a problem?

Vasant-Patel commented 7 years ago

I will second to @serejahh, in real world application you most likely want to create ViewModel outside of the ViewController due to dependencies coming from somewhere else that needs to be DI'ed to the ViewModel itself before the viewDidLoad is called

For very simple applications the approach works fine when viewModel itself can be constructed without any external dependencies but thats not the case with most of the real world applications

I think for that either Subject or Variable seems the only way to model this use-case, although I would only expose them as observables and keep the rest internal to viewModel

iandundas commented 7 years ago

@serejahh @Vasant-Patel A good way to get around the need for a loaded view before initing your view model is described in this blog post - see the BaseStage implementation (the naming is different in that post but it's basically just ViewModels.)

It allows the ViewModel to be created in a closure passed to the init of the ViewController like so:

let viewController = AuthenticationLandingViewController.create { (viewController) -> AuthenticationLandingViewModelType in
   let viewModel = AuthenticationLandingViewModel(viewActions: viewController.actions)
   // any extra config of the VM here
   return viewModel
}
sergdort commented 7 years ago

@serejahh I see what you mean. If you guys interested I've came up with this idea

danielchristopher1 commented 7 years ago

@sergdort this is great.

jgongo commented 6 years ago

I'm a bit late to the conversation, and I haven't read the blog post from @iandundas or the gist from @sergdort , but I'd like to share my current approach: I'm using Swinject and passing down a Resolver, which the view controller uses to create a view model passing the arguments required by that view model. This way you create the view model once the view is already created, and have a view model created with its needed dependencies injected. You could even pass down a Resolver configured with different providers in case you want to implement some unit or integration tests for the view controller. What do you think?