Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to decouple view logic from update logic? #327

Closed
sum-elier opened this issue May 1, 2020 · 9 comments
Closed

How to decouple view logic from update logic? #327

sum-elier opened this issue May 1, 2020 · 9 comments
Labels
question Further information is requested

Comments

@sum-elier
Copy link
Contributor

First of all this an awesome project, thank you for publishing this.

I was wondering if it is possible to decouple the view logic from the update logic? Currently the update logic is only invoked in response to a message from the view logic, but if I were to implement some business logic which reads continuously from a TCP socket and pushes that to the UI, how could I implement this without having to send a dummy message from the view logic every time an event is consumed from the TCP socket to keep the stream going on?

Ideally I would like to drive the UI from the business logic, and if some user interaction happens it is handled as usual, but without having to drive the business logic necessarily from UI events.

@hecrj hecrj added the question Further information is requested label May 1, 2020
@hecrj
Copy link
Member

hecrj commented May 1, 2020

Event subscriptions can be used to listen to external events. See #29 and #122 for more details.

@sum-elier
Copy link
Contributor Author

Thank you for your fast response. It seems that indeed those examples suit my needs.

@sum-elier
Copy link
Contributor Author

sum-elier commented May 2, 2020

@hecrj Is it possible to also have some sort of subscription but for the view state?
Something like being able to generate asynchronously state events as a stream, then the framework subscribes to this stream, and then posts these results on the main thread to the widgets to update them.

I guess this is somewhat related to #241.

@hecrj
Copy link
Member

hecrj commented May 2, 2020

@sum-elier Yes, that's indeed how it works.

The messages produced by a Subscription returned in Application::subscription will be fed to Application::update, which can change state as desired. As a consequence, Application::view can then return a different set of widgets.

Check out the examples mentioned in the documentation.

@sum-elier
Copy link
Contributor Author

sum-elier commented May 2, 2020

Yes, that is correct, and I've been using those great examples you have 😃 ! But... what I see is the subscription generates events to be consumed by Application::update, not by Application::view since it is the framework that controls when is Application::view invoked, from what I understand. That's why I mentioned #241.

As an example of what I have and what I am trying to do, if that makes sense (pseudo code):

#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Message {
    FieldA(String),
    FieldB(String),
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct State {
    pub field_a: String,
    pub field_b: String,
}

pub struct BusinessLogic<'a> {
    external_events: BoxStream<'a, String>,
    state: MyEventPublisher<State>,
}

impl<'a> BusinessLogic<'a> {
    pub fn new(external_events: BoxStream<'a, String>) -> BusinessLogic<'a> {
        BusinessLogic {
            external_events,
            state: MyEventPublisher::new(State {
                field_a: "".to_string(),
                field_b: "".to_string(),
            }),
        }
    }

    pub fn update(&mut self, msg: Message) {
        match msg {
            Message::FieldA(field_a) => {
                self.update_field_a(new_field_a);
                self.emit_new_state();
            }
            Message::FieldB(new_field_b) => {
                self.update_field_b(new_field_b);
                self.emit_new_state();
            }
        };
    }

    pub fn state_changes<'a>(&self) -> BoxStream<'a, State> {
        let states = self.state.to_stream().boxed();
        self.external_events
            .join(states)
            .map(|(state, external_event)| combining_func(state, external_event))
    }
}

So I would like to subscribe to the stream returned by state_changes and pass each event to Application::view.

The purpose is to ideally have a fully reactive application using streams which works independently of the chosen UI framework.

@hecrj
Copy link
Member

hecrj commented May 2, 2020

It feels like you are trying to implement your own MVC architecture on top of iced, which uses the Elm Architecture. I think you will have a hard time fighting the library if you go this way.

Instead, process any state changes in update (your business logic) and produce your widgets from your state in view (your view logic). Try to keep a single source of truth for your application state.

There is no need to emit any events from update logic. view will always be called by the runtime after an update. In other words, you can access field_a and field_b directly from both.

@sum-elier
Copy link
Contributor Author

sum-elier commented May 3, 2020

Well not really, at least not intentionally. What I wanted was to have the state stored in streams instead of mutable variables that can be read outside these streams, more like FRP. So the update method receives messages, and pushes them into the streams which contain all the business logic and at the end there's a single output from a final stream with the view state.
Though I understand now that this contravenes the philosophy of the framework.

At the end I wanted to use from Iced only the GUI part of it since it knows how to render widgets given some data (state), independently of how my UI architecture is implemented, thus ignoring the lifecycle, command execution and so on.

Maybe this is related to #313

@hecrj
Copy link
Member

hecrj commented May 10, 2020

I don't think there is anything in particular stopping you from doing what you want. Just keep in mind that this is not the correct way to think about it:

So I would like to subscribe to the stream returned by state_changes and pass each event to Application::view.

Your whole Application represents the "view state", you update this state through Application::update and generate the according widgets in Application::view. View logic cannot react to messages, it's just a pure mapping of some state to some visual representation.

You can store your logic in different streams, notify your "final view" (the Application) and change the view state in update.

@sum-elier
Copy link
Contributor Author

sum-elier commented May 10, 2020

@hecrj Thanks for answering back, really appreciated. Sorry for the insistence but I love this project and Rust and just trying to make it work for me.

Maybe the problem is that I am a rookie at Rust. But what I want to achieve is have a completely modular stream oriented application which can reuse any kind of GUI library or framework with ease without depending on a specific paradigm.

My view state was represented inside a stream à la FRP which emitted events every time a message was processed in the update method of one of these modules. Afterwards I wanted to map it as you say to some visual representation, but since Iced doesn't work that way, that is: using a stream of view states (like push-based), but rather it reads from a view state (like poll-based), then I decided to simply store the view state in the module and have a method that reads the view state called from the framework's fn view(&mut self) -> Element<Message> method.

But now I have encountered a related (maybe?) problem while trying to have a subscription from a struct that also serves as the one that does the updates. I am unable to create a subscription that works as presented.
The code is as follows but I am not sure if this isn't possible because of how I structured my code and Rust forbids it or simply because Iced doesn't work that way. This is a simplification of the code I have but the idea remains more or less the same.

#[derive(Clone, PartialEq, Eq, Debug)]
pub enum LogicMessage {
    Value,
}

pub struct Logic;

impl Logic {
    pub fn update(&mut self, lmsg: LogicMessage) {}

    pub fn stream(&self) -> BoxStream<LogicMessage> {
        self.other_stream(self.result_from_update).boxed()
    }
}

pub struct View;

#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Message {
    FromLogic(LogicMessage),
}

pub struct MyApp {
    logic_and_view: LogicAndView,
}

pub struct LogicAndView {
    pub logic: Logic,
    pub view: View,
}

impl LogicAndView {
    pub fn new(logic: Logic) -> Self {
        Self {
            logic,
            view: View {},
        }
    }
}

impl Application for MyApp {
    type Executor = iced_futures::executor::AsyncStd;
    type Flags = ();
    type Message = Message;

    fn new(_flags: ()) -> (Self, Command<Message>) {
        let logic_and_view = LogicAndView::new(Logic {});

        (Self { logic_and_view }, Command::none())
    }

    fn title(&self) -> String {
        String::new()
    }

    fn update(&mut self, message: Message) -> Command<Message> {
        match message {
            Message::FromLogic(logic_msg) => {
                &mut self.logic_and_view.logic.update(logic_msg);
            }
        }
        Command::none()
    }

    fn view(&mut self) -> Element<Message> {
        self.logic_and_view
            .view
            .set_view(self.logic_and_view.logic.get_view_data())
            .map(Message::FromLogic)
    }

    fn subscription(&self) -> Subscription<Message> {
        self.logic_and_view.logic
             .stream()
             ./*somehow create a subscription that compiles*/
             .map(Message::FromLogic)
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants