-
-
Notifications
You must be signed in to change notification settings - Fork 122
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
Concept of separating update cycles. Again... #678
Comments
@vshapenko That's interesting. Are you trying to have one "program" per page? I kind of did a similar thing in #677 So I declared a typed record for representing a single Sample (with its own type Sample<'Msg, 'CmdMsg, 'ExternalMsg, 'Model> =
{ Title: string
Init: unit -> 'Model
Update: 'Msg -> 'Model -> 'Model * 'CmdMsg list * 'ExternalMsg option
View: 'Model -> ('Msg -> unit) -> ViewElement
MapToCmd: 'CmdMsg -> Cmd<'Msg> } (Note: I wanted to support the ExternalMsg and CmdMsg patterns also) This allowed me to register my samples like that: // Sample 1
{ Title = "RefreshView"
Init = Controls.RefreshView.init
Update = Controls.RefreshView.update
View = Controls.RefreshView.view
MapToCmd = Controls.RefreshView.mapToCmd }
// Sample 2
{ Title = "SkiaSharp"
Init = Extensions.SkiaSharp.init
Update = Extensions.SkiaSharp.update
View = Extensions.SkiaSharp.view
MapToCmd = Extensions.SkiaSharp.mapToCmd } But since they have different types and I wanted to store them in a single list, I had to downcast them to obj. So I created another type for that - replacing types with type SampleDefinition =
{ Title: string
Init: unit -> obj
Update: obj -> obj -> obj * obj list * obj option
View: obj -> (obj -> unit) -> ViewElement
MapToCmd: obj -> Cmd<obj> }
let boxSampleDefinition (sample: Sample<'Msg, 'CmdMsg, 'ExternalMsg, 'Model>): SampleDefinition =
{ Title = sample.Title
Init = sample.Init >> box
Update = fun msg model ->
let newModel, cmdMsgs, externalMsg = sample.Update (unbox msg) (unbox model)
(box newModel), (cmdMsgs |> List.map box), (externalMsg |> Option.map box)
View = fun model dispatch ->
sample.View (unbox model) (fun msg -> dispatch (unbox msg))
MapToCmd = fun cmdMsg ->
sample.MapToCmd (unbox cmdMsg) |> Cmd.map box } Notice the usage of That way, I was able to register them all in a single list let Samples = [
// Sample 1
({ Title = "RefreshView"
Init = Controls.RefreshView.init
Update = Controls.RefreshView.update
View = Controls.RefreshView.view
MapToCmd = Controls.RefreshView.mapToCmd } |> boxSampleDefinition)
// Sample 2
({ Title = "SkiaSharp"
Init = Extensions.SkiaSharp.init
Update = Extensions.SkiaSharp.update
View = Extensions.SkiaSharp.view
MapToCmd = Extensions.SkiaSharp.mapToCmd } |> boxSampleDefinition)
] Then I had to implement the application root program to handle that list. So first off, I needed a way to represent the current state of a single sample that could be stored in a list with the other samples states. // Store the sample definition (init, update, view) with its current model
type SampleState =
{ Definition: SampleDefinition
Model: obj } Then the module App =
type Msg =
| SampleMsg of obj // One sample sent a Msg
| NavigateToRequested of Node // One sample requested to navigate to another sample
| NavigationPopped // The user tapped the back button
// The App model stores the stack of currently running samples (in my example, I had intermediate pages)
type Model =
{ SampleStates: SampleState list }
let init () =
// Get the first sample of the registered list. It will be our home
let definition = Samples.head
// Create a state for it
let sampleState =
{ Definition = definition
Model = definition.Init() }
// Store that state in the app's model
{ SampleStates = [sampleState] }, []
// The ExternalMsg pattern is supported so there could be inter-samples interactions. Like navigation for instance
let mapExternalMsg (externalMsg: obj option) =
match externalMsg with
| Some (:? RefreshView.ExternalMsg as msg) -> // Match on the ExternalMsg type to determine from which sample it comes
match msg with
| RefreshView.ExternalMsg.NavigateToSkiaSharp ->
let skiaSharpDefinition = Samples |> List.... // Do a lookup to find the SkiaSharp registration
Cmd.ofMsg (NavigateToRequested skiaSharpDefinition)
| _ ->
Cmd.none
let update msg model =
match msg with
| SampleMsg sampleMsg ->
// Received a Msg from a Sample, so we need to call the appropriate `update` function. Here I go with the assumption only the most recent sample sends Msgs so I take the head (list reversed).
match model.SampleStates with
| [] -> model, []
| sampleState::rest ->
let newSampleModel, newCmdMsgs, externalMsg = sampleState.Definition.Update sampleMsg sampleState.Model
let newSampleState = { sampleState with Model = newSampleModel }
let newCmd = newCmdMsgs |> List.map (sampleState.Definition.MapToCmd >> (Cmd.map SampleMsg)) |> Cmd.batch
let newExternalCmd = mapExternalMsg externalMsg
// Replace the head with a new SampleState with the updated sample's model
{ model with SampleStates = newSampleState :: rest }, Cmd.batch [newCmd; newExternalCmd]
| NavigateToRequested definition ->
let sampleState =
{ Definition = definition
Model = definition.Init() }
{ model with SampleStates = sampleState :: model.SampleStates }, []
| NavigationPopped ->
{ model with SampleStates = model.SampleStates.Tail }, []
let view model dispatch =
View.NavigationPage(
popped = (fun _ -> dispatch NavigationPopped),
pages = [
for sampleState in List.rev model.SampleStates ->
sampleState.Definition.View sampleState.Model (SampleMsg >> dispatch)
]
)
type App () as app =
inherit Application ()
let runner =
Program.mkProgram App.init App.update App.view
|> Program.withConsoleTrace
|> XamarinFormsProgram.run app So, of course it takes a lot of assumptions on how the app should behave, but it's a pretty generic way of handling multiple independent "programs" in a single app. Also, the pattern exhibited in FabulousContacts, despite being verbose, has the advantage to prevent exceptions like |
@TimLariviere , in this case i am trying to avoid all these complex DU's of messages or boxing (which is akka way). The main idea is that i have separate update cycles, one for each model, and each cycle has inner model state:
As we see, we have a create function, which is a 'glue' for our app. There are three important components in there :
Also we have subs, which are for subscribing for internal events. init function invokes each time we show the page. It should have all needed logic (i.e. update state by getting data from db) before showing the page. I think, such(or similar) implementation could be built in into Program. Pros are: Cons are: I am using such code in production (messenger app), and it really speeds up the process of app development / refactoring. So i hope it also can be used to improve development experience when using Fabulous |
Hello! I am having a working concept of separating views update cycles without writing tons of boilerplate DUs. Code is here ,any appreciation is welcome. Concept is based on "pushing" of messages to main loop cycle through unit->unit function. @TimLariviere ,what do you think about such way of simplification?
The text was updated successfully, but these errors were encountered: