Search code examples
xamarinxamarin.formsf#fabulous

F# Fabulous Xamarin: external event subscription


I'm new to Fabulous and MUV model, and I'm trying to implement application that works with BLE. I'm also a bit new to F#, mostly worked with erlang and C# in the past, so a bit lost with external events processing. CrossBluetoothLE.Current.Adapter has DeviceDiscovered event handler (IEvent). What's the most correct way of linking this event handler to the Fabulous update function?

E.g. after I will call CrossBluetoothLE.Current.Adapter.StartScanningForDevicesAsync(), I want that this event handler supply newly discovered devices to the update function.

And if I will do something like this (this is not working):

type MyApp () as app = 
    inherit Application ()

    let deviceDiscovered dispatch = 
        CrossBluetoothLE.Current.Adapter.DeviceDiscovered.Subscribe (fun x -> dispatch (App.Msg.Discovered x.Device) )

    let runner =
        App.program
        |> Program.withConsoleTrace
        |> Program.withSubscription (fun _ -> Cmd.ofSub deviceDiscovered)
        |> XamarinFormsProgram.run app

if it works, it will be ok for device discovery because CrossBluetoothLE.Current.Adapter is static. However after device will be discovered, I will need to work with (e.g. receive notifications or replies from it), and it will not be possible to include dynamic device handler into Program.withSubscription.

Not sure whether the Fabulous is applicable here.


Solution

  • Ok, I was able to find some solution and it works now, but the overall architecture looks a bit weird. So generic approach is to create an external mailbox, that will dispatch messages to the MUV loop.

    1. Describe all messages of the MUV in the external module, e.g.:
        type Msg = 
            | Scan
            | Discovered of IDevice
            | Connect of IDevice
            | ClockMsg of System.DateTime
            | TextMsg of string
    
    1. Create type that encapsulates mailbox:
        type DispatchFunc = Msgs.Msg -> unit
    
        type State = 
            | Initialized of DispatchFunc
            | NotInitialized
    
        type Mail = 
            | Dispatch of DispatchFunc
            | Msg of Msgs.Msg
            | None
    
        let rand = System.Random()
        let id = rand.NextDouble()
    
        let postbox = MailboxProcessor.Start(fun inbox -> 
            let rec messageLoop (state:State) = async{
                let! mail = inbox.Receive()
    
                let new_state = 
                    match mail with 
                    | None ->
                        state
                    | Msg msg -> 
                        match state with 
                        | NotInitialized -> NotInitialized
                        | Initialized df ->
                            df msg
                            state
                    | Dispatch df -> 
                        Initialized df
    
                return! messageLoop (new_state)
                }
    
            messageLoop (NotInitialized))
    
        let post(o) =
            postbox.Post o
    

    Here, mailbox starts with NotInitialized state and wait while application will start. When everything is done, mailbox received dispatch function, that will be used in further dispatching of the external messages to the MUV main loop.

    1. Pass dispatch handler to the mailbox:
    type MyApp () as app = 
        inherit Application ()
    
        // generate initial events + start threads + pass dispatch reference to the mailbox
        let initThreads dispatch =
            // init & start external (e.g. bluetooth receiver) threads here
            // or start them asynchronously from MUV loop
            Postbox.post (Postbox.Dispatch dispatch)
            ()
    
        let runner = 
            App.program
            |> Program.withConsoleTrace
            |> Program.withSubscription (fun _ -> Cmd.ofSub initThreads)  
            |> XamarinFormsProgram.run app
    

    So now, if you want to send event to the MUV from external thread, just start it inside initThreads (or, e.g. from within MUV loop) and use something like: Postbox.post (Postbox.Msg (Msgs.TextMsg "It works!")).

    E.g. for my purposes (BLE discovery) it will look like this:

        let update msg model =
            match msg with
            | Msgs.Scan -> 
                CrossBluetoothLE.Current.Adapter.StopScanningForDevicesAsync() |> Async.AwaitTask |> ignore
                CrossBluetoothLE.Current.Adapter.DeviceDiscovered.Subscribe (
                        fun (a) ->
                            Postbox.post (Postbox.Msg (Msgs.Discovered a.Device))
                            ()
                    ) |> ignore
                CrossBluetoothLE.Current.Adapter.StartScanningForDevicesAsync() |> Async.AwaitTask |> ignore
                model, Cmd.none
            | Msgs.ClockMsg msg ->
                { model with label = msg.ToString() }, Cmd.none
            | Msgs.TextMsg msg ->
                { model with label = msg }, Cmd.none
            | Msgs.Discovered d ->
                { model with gattDevices = d::model.gattDevices; label = "Discovered " + d.ToString() }, Cmd.none
            | Msgs.Connect d -> { model with connectedDevice = d }, Cmd.none
    

    This is for sure a very ugly solution, but I wasn't able to imagine something more beautiful :(.