thlorenz / rid

Rust integrated Dart framework providing an easy way to build Flutter apps with Rust.
64 stars 4 forks source link

Message and Reply for complex projects #40

Closed SecondFlight closed 2 years ago

SecondFlight commented 2 years ago

Hi! I've got some questions about Message and Reply, and best practices for creating messages.

Let's say I have an app with a Message enum like so:

pub enum Msg {
    MessageOne,
    MessageTwo,
    MessageThree,
}

Now, let's say I want to add a new module to my app with a whole new set of messages. I could see myself doing something like:

pub enum Msg {
    // Module one
    ModuleOne_MessageOne,
    ModuleOne_MessageTwo,
    ModuleOne_MessageThree,

    // Module two
    ModuleTwo_MessageOne,
    ModuleTwo_MessageTwo,
    ModuleTwo_MessageThree,

    // More modules?
    // ...
}

Is this considered correct, or is there possibly a way to split this up?

thlorenz commented 2 years ago

At this point what you're doing is the only option. Similarly to Elm/redux there is only one Msg type for the entire app. Ideally your message names from different modules wouldn't overlap so the prefix wouldn't be necessary and you'd just group them as you did via comments.

However if this becomes an issue in your case (I didn't run into any, but didn't build very large apps with rid yet) LMK and we can think of a solution.

Keep in mind though that it'd be tricky to introduce another Msg type as the store update method assumes exactly one. This also makes sure you handle all possible messages via the match statement inside update. Once you have 2 or more Msg types that becomes much less straight forward, error prone and I imagine in some cases also harder to understand.

Think of update as a dispatch for all kinds of messages. You could convert it to a per module type there before passing it on, similar to:

fn update(&mut self, req_id: u64, msg: Msg) {
    use Msg::*;
    match msg {
        AddTodo(id) => handleCrud(self, req_id, CrudMsg::Add(id)),
        RemoveTodo(id) => handleCrud(self, req_id, CrudMsg::Remove(id)),
       [..]
       // Non-Crud Messages

    }
}

That way you only deal with the entire space of messages in the Msg type and the update method, but once you delegate via a narrowed down message type to a handler that part doesn't know anything about Msg, but in this case only about CrudMsg.

SecondFlight commented 2 years ago

Thanks for the detailed response! I really appreciate it.

This makes a lot of sense to me. I'm very comfortable with Typescript + React reducers (similar to Redux) and I'm already starting to feel at home here.

My pattern is usually to lean on Typescript to neatly section things off for me and allow me to define everything in separate files:

// ModuleOneReducer.ts

enum ModuleOneActionType {
  ActionOne = 'ACTION_ONE',
  ActionTwo = 'ACTION_TWO',
  ActionThree = 'ACTION_THREE',
}

type ModuleOneAction =
  | {
      type: ModuleOneActionType.ActionOne;
      payload: {};
    }
  | {
      type: ModuleOneActionType.ActionTwo;
      payload: {};
    }
  | {
      type: ModuleOneActionType.ActionThree;
      payload: {};
    };

const ModuleOneReducer = (state: State, action: ModuleOneAction): State => {
  // ...
};

// MainReducer.ts

type Action = ModuleOneAction | ModuleTwoAction | ModuleThreeAction;

const Reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case ModuleOneActionType.ActionOne:
    case ModuleOneActionType.ActionTwo:
    case ModuleOneActionType.ActionThree: {
      ModuleOneReducer(state, action);
      break;
    }

    // ...
  }
};

My original thought was that it would be nice to be able to construct a msg enum like this:

// module_one_model.rs
pub enum ModuleOneMsg {
  MessageOne,
  MessageTwo,
  MessageThree,
}

// store.rs
pub enum Msg {
  ModuleOne(ModuleOneMsg),
  ModuleTwo(ModuleTwoMsg),
  ModuleThree(ModuleThreeMsg),
  // ...
}

But I need a single update where I'm going to enumerate everything anyway, so I'm not sure that would add a lot of value.