Fabulous: Going Beyond Hello World

Timothé Larivière

Timothé

This is a guest blog by Timothé Larivière. Timothé is the maintainer of Fabulous: Functional App Development and a Microsoft MVP. You can find him on Twitter @Tim_Lariviere.

In Fabulous: Functional App Development, we saw how to leverage functional programming and the Model-View-Update (MVU) architecture to build mobile and desktop apps with Fabulous. If you haven’t read it yet, I strongly encourage you to do so first as this blog post will build on it.

A quick reminder on the MVU architecture as used in Fabulous:
Image of MVU architecture
Credits: Beginning Elm (https://elmprogramming.com)

MVU uses a single state object (aka the Model) for all the data needed by your app. This state is immutable, no one can change it.
The state is created by the init function, and Fabulous passes it to the view function to know what to display on the UI.

When the user interacts with the UI, it dispatches messages (aka Msg) back to Fabulous to let it know that the state needs to change.
At this point, Fabulous call the update function with the current state and the received message to let you define a brand new state. Fabulous then proceeds to call the view function with the new state.

For more information on MVU in Fabulous, please read Fabulous: Functional App Development. By design, Fabulous guarantees that you won’t have any concurrency issue, even if multiple things go on simultaneously thanks to this Msg mechanism. If several messages are received at once, they will be queued and processed one by one.

To ensure reactivity when a new message arrives, the init, update and view functions are synchronous. You don’t want your app to stop processing messages while calling an API over an EDGE connection.

Querying an API, accessing the storage of the device, interacting with a local database, etc. All these things are necessary when writing real world applications. So how does one make async calls with Fabulous?

Short answer: Side effects.

Side effects in Fabulous

In object-oriented programming, side effects are considered bad because they change things outside of their scope and lead to unpredictable behaviors. Functional programming has a broader definition for side effect:

A function is considered pure of side effect when it always gives the same output for the same input without changing anything in the application. Everything else is considered a side effect, even just updating a property of its own class or logging something in the debug console; because the function would leave the app in a different state than it originally was.

Here, Fabulous allows you to run side effects in a controlled manner while keeping the init and update functions pure and reactive.
This is done in 2 ways: commands and subscriptions.

Commands

Command (shorten to Cmd) provides you with a way to tell Fabulous you’d like to run some side effects. Fabulous will run those for you outside of the main MVU loop, so it can continue to process messages.

At any point in time, you’ll have the possibility to dispatch a Msg to update your app state and your UI. Fabulous will treat this message like any other message coming from the view. Let’s see how to use Cmd. If you remember from the last blog post, we saw the following declaration to bring together all our functions and start the app:

let program = XamarinFormsProgram.mkSimple init update view

This declaration wants init and update to only return a Model.

Let’s change it to mkProgram:

let program = XamarinFormsProgram.mkProgram init update view

This time, init and update need to return a tuple of both a Model and a Cmd<Msg> (the side effect).

We’ll need to update those functions to return the correct type

let init () =
    // The comma between the model and the Cmd means we're constructing a Tuple (commonly noted as `A * B`)
    // Cmd.none means "no side effect"
    { Count = 0 }, Cmd.none 

let update msg model =
    match msg with
    | Increment -> { model with Count = model.Count + 1 }, Cmd.none
    | Decrement -> { model with Count = model.Count - 1 }, Cmd.none

Let’s change this code to handle the sign up of a user. We’ll need a button to let the user choose whether he wants to sign up or not as well as a spinner control to let him know that his request is pending.

The sign-up process can be complex and have multiple results like success and failure (especially when doing unreliable things like an API call). To represent that, we’ll need to declare 3 Msgs: SignUp (start the sign-up process), SignUpSuccessful, SignUpFailed.

Since we’re calling an API, we’ll need the sign-up function to be async. To do that, we’ll use Cmd.ofAsyncMsg to tell Fabulous we want to execute an async function that will return a message at the end. Cmd has a handful of functions that accept several types of functions, feel free to explore them.

type Msg =
    | SignUp
    | SignUpSuccessful
    | SignUpFailed of errorMsg: string

// let! and do! are the equivalent of using await in C#
let signUpAsync (email: string) =
    async {
        try
            let! isEmailAlreadyUsed = Api.isEmailInUseAsync email
            if isEmailAlreadyUsed then
                return SignUpFailed "Email already signed up"
            else
                do! Api.signUpAsync email
                return SignUpSuccessful    
        with
        | :? WebException as wex ->
            return SignUpFailed ("A network error occurred: " + wex.Message)
        | ex ->
            return SignUpFailed "An unknown error occurred"
    }

let update msg model =
    match msg with
    | SignUp ->
        // Sets the IsLoading flag to true (so we can display a spinner)
        // and tell Fabulous we want to run the `signUpAsync` function as a side effect
        { model with IsLoading = true }, Cmd.ofAsyncMsg (signUpAsync model.Email)

    | SignUpSuccessful ->
        // We arrive here when the sign up is successful
        { model with IsLoading = false; SignedUp = true }, Cmd.none

    | SignUpFailed errorMessage ->
        // Or here when it failed
        { model with IsLoading = false; ErrorMessage = errorMessage }, Cmd.none

let view model dispatch =
    View.ContentPage(
        (...)

        if model.IsLoading then
            View.ActivityIndicator()
        else
            View.Button(
                text = "Sign up",
                command = fun () -> dispatch SignUp
            )

    )

The exact same process is used for any asynchronous calls. You can find a few other examples in FabulousContacts like using Xamarin.Essentials to start a phone call, author an email, etc. or using SQLite.

Tips: Try to put all side effects in external functions and call them through Cmd, even when they are synchronous or could be inlined in the update function (like logging, sending metrics to AppCenter, etc.).

This will keep the init and update functions pure which will let you unit test them easily. We’ll see together in a later blog post how to unit test all parts of a Fabulous app; UI included!

Subscriptions

Subscription (shorten to Sub) is a variant of Cmd. Declared at the start of the application, it can send messages at any point in time.
This is typically used when listening to external events like push notifications or system events.

Listening to RequestedThemeChanged:

module App =
    type Model = { ... }
    type Msg = ThemeChanged of Xamarin.Forms.OSAppTheme

    let init () = ...
    let update msg model = ...
    let view model dispatch = ...

    let program = ...

type App() as app =
    inherit Xamarin.Forms.Application

    let themeChangedSub dispatch =
        Application.Current.RequestedThemeChanged.AddHandler(
            EventHandler<AppThemeChangedEventArgs>(fun _ args ->
                dispatch (App.Msg.ThemeChanged args.RequestedTheme)
            )
        )

    let runner =
        App.program
        |> Program.withSubscription themeChangedSub
        |> XamarinFormsProgram.run app

Conclusion

Commands and Subscriptions are an important part of writing real world apps with Fabulous.
They let you clearly separate the interface logic from the business logic, while ensuring you won’t encounter unpredictable behaviors when your app grows.

If you want to learn more about commands and subscriptions, please read the documentation of Fabulous.

Like last time, make sure to also visit our GitHub repository for all the latest bits: https://github.com/fsprojects/Fabulous.
We welcome all contributions, either through issues, discussions or pull requests.

0 comments

Leave a comment