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:
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 Msg
s: 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