Elmish Xamarin Forms from Fable point of View

Explore Elmish Xamarin Forms through Fable's perspective. Compare pros and cons of EXF for cross-platform mobile development with practical examples.

12 min read 2,346 words

Elm and Elmish patterns are kind of new cool in the web world. Good people in F# put that Elmish in Xamarin Forms.

Immutable UI for Xamarin Forms.

That is quite bold in itself. We will look at the pros and cons of it in this post and also try to look at EXF from the angle of other contemporary technologies for creating cross-platform mobile applications.

One thing I like to clarify before moving forward:

The objective of the application I have created is solely to match with Fable - Elmish applications.

So, there is a chance that you might miss a few Xamarin-specific things. Those are not included knowingly. After working in Fable Elmish for more than a year, I just wanted to know how much of my Elmish knowledge I can bring to creating mobile applications without thinking about the mobile part of it. Also, what kind of development experience am I getting from the platform compared to Fable - Elmish.

Application in Brief

You can find the application Elmish Star Wars on GitHub. I have created a UI for the Star Wars API. You have a couple of tabs, pull the data, and then see the detail view. In the details view you can click a button and get the list as well. Pretty routine stuff, nothing fancy.

You can always fetch the code and run it. In most cases it will work.

Let's go directly to the pros and cons of selecting EXF. I will let readers decide what they want to do. I will also put out a gist at the end of the post, so please read on.

Pros

No XAML

Sorry to all XAML fans out there. But for most of the F# developers I know, XAML was and is kind of a complicated issue. They always had a love-hate relationship with XML-based design tools. For me, things would be harder to reuse when there is XML-based UI. EXF solves that problem by allowing you to write the view in F#. In that way, the view becomes just another F# function. You can do many things when you have flexible and powerful functions at your disposal, even for views.

Take a look at the piece of code below:


module View =
    open Types
    open CommonViews

    let root (model:Model) dispatch =
        View.ScrollView(
                content = View.StackLayout (
                    padding = 20.0,
                    children = [
                        if model.Length > 0 then
                            yield View.Label(text = sprintf "no of films : %A" model.Length)
                            yield View.ListView(
                               items = [
                                   for i in model do
                                       yield View.Label i.Title
                                       ],
                                itemSelected=(fun idx -> dispatch (FilmSelectedItemChanged idx)),
                                horizontalOptions=LayoutOptions.CenterAndExpand)
                        else yield loadingView "Press to Load Films. And press again to load more Films"
                    ]
                )
            )

Now this piece of view I am using in the tab view on the home page and also in a separate view where I need to show the list. I am not saying this might not be possible in XAML, but it might not be as natural as it is here.

View Reuse

As mentioned above, views in F# make much more sense when creating a large application. I had a wonderful experience with Fable - Elmish and getting the same thing with EXF to create native views is surely a wonderful experience. It seems like not a big deal when creating a toy application, but when it comes to enterprise applications, view reuse can reduce code and the possibility of errors. As developers, we always emphasize code reuse, but in most cases we forget that our view is also a piece of code that we can and should reuse. Elmish-style web apps or even EXF apps make it easy to reuse view code.

In my application, I have reused the code above. Let's see how I did it.

 module View =
    open Types
    open CommonViews


    let root (model: Model) dispatch =
        View.TabbedPage(
                useSafeArea = true,
                children = [
                    yield
                        View.ContentPage(
                            title = "People",
                            content =
                                View.StackLayout(
                                    children = [
                                        PeopleList.View.root model.People (PeopleListMsg >> dispatch)
                                        View.Button(text = "Fetch People",command = (fun _ -> LoadPeople |> dispatch))
                                    ]
                                )
                            )
                    yield
                        View.ContentPage(
                            title = "Film",
                            content =
                                View.StackLayout(
                                    children = [
                                        FilmList.View.root model.Films (FilmListMsg >> dispatch)
                                        View.Button(text = "Fetch Films",command = (fun _ -> LoadFilms |> dispatch))
                                    ]
                                )
                            )
                            //code removed for readability purpose

And the same code in another view:

module View =
    open Types
    let root (model: Model) dispatch =
        View.ContentPage(
            title = "Films",
            content =
                View.StackLayout(
                    children = [
                        FilmList.View.root model.Films (FilmListMsg >> dispatch)
                    ]
                )
            ).HasNavigationBar(true).HasBackButton(true)

Familiar Libraries

The USP of using Xamarin or Xamarin Forms for most .NET developers is that they are in familiar terrain. They can use all C# libraries. Now, with .NET Core support, they can also use all .NET Core libraries. The same is true for EXF. All .NET goodies are available—well, most of them, whether C# or F#. There are some obvious hiccups, like FSharp.Data still not working for me. I'm sure it will be solved soon. Other than that, most obvious things like JSON.net or RestSharp work just as they should with any .NET application.

module Helpers
    open RestSharp
    open Elmish.XamarinForms
    open Elmish.XamarinForms.DynamicViews
    open Newtonsoft.Json


    let getData (url:string)=
        async {
            do! Async.SwitchToThreadPool()
            let client = new RestClient(url)
            let req = RestRequest(Method.GET)
            return! Async.Catch (client.ExecuteTaskAsync (req) |> Async.AwaitTask)
        }

    let getCmd (res : Async<Choice<IRestResponse,exn>>) success failure =
       async {
           let! data = res
           match data with
           | Choice1Of2 r -> return success (JsonConvert.DeserializeObject<_> r.Content)
           | Choice2Of2 exn -> return failure exn
       } |> Cmd.ofAsyncMsg

As good as it can get. Async, RestClient, and Json.net all seamlessly integrated together with EXF - Command.

No React

Even if you skip the license issue, React can be painful for developers. People who worked in web or Fable Elmish have experienced it in one way or another (a reason they switched to Preact or similar libraries). But here you don't have React and still have the same functionality. Trust me on this: it is good not to have React around your development process.

Cons

No React

I will start with the same thing. There are times when I don't like React, but it is a powerful library that does the rendering task like a champ. While developing with EXF, I do feel sometimes that the view is getting rendered again and again when it is not required. It was adding flicker to the page. That definitely doesn't look good. Maybe some clever library can help in that case.

Hot Reload

If you are coming from web, this is kind of a must and much-needed thing while doing development. Hot Reload is provided by EXF, but first you need to do a little bit of work to make it work, and then it will again not work in the case of multiple files. In the Elmish architecture, you tend to have a lot of files for sure. So, not having hot reload does hurt from time to time.

No URLs

This was a big bummer for me when I was starting with EXF. You might not feel the need for the same, but if you are moving from web-elmish, URLs are the things I missed most. On the web, URLs are the hook for your views. Things always start with URLs. When the URL changes, the view changes. Some search-based cases or doing web requests based on URLs as well. In EXF, there are many options like hard-coding pattern-matched pages if you have fixed navigation, and if you are having dynamic navigation, you might need something to hold your pages and models so you can navigate back (if you like your back button).

Here is code I wrote for the same:

module State

open Global
open Types
open Elmish.XamarinForms
open Elmish.XamarinForms.DynamicViews

let init () =
    let (a, aCmd) = Application.State.init()
    {
        PageStack = [(ApplicationPage, ApplicationModel a)]
    }, Cmd.map ApplicationMsg aCmd


let removeFirst (items : 'a list) =
    match items with
    | [] -> []
    | [_] -> []
    | h::t -> t

let peopleListProc msg model res =
    match msg with
    | PeopleList.Types.Msg.SelectedPerson i ->
     let (a,aCmd) = Person.State.init(i)
     {model with PageStack = (PersonPage,PersonModel a) :: model.PageStack ; }, Cmd.map PersonMsg aCmd
    | _ -> res

let filmListProc msg model res =
    match msg with
    | FilmList.Types.Msg.SelectedFilm i ->
     let (a,aCmd) = Film.State.init(i)
     {model with PageStack = (FilmPage,FilmModel a) :: model.PageStack ; }, Cmd.map FilmMsg aCmd
    | _ -> res

let starshipListProc msg model res =
    match msg with
    | StarshipList.Types.Msg.SelectedStarship i ->
     let (a,aCmd) = Starship.State.init(i)
     {model with PageStack = (StarshipPage,StarshipModel a) :: model.PageStack ; }, Cmd.map StarshipMsg aCmd
    | _ -> res

let vehicleListProc msg model res =
    match msg with
    | VehicleList.Types.Msg.SelectedVehicle i ->
     let (a,aCmd) = Vehicle.State.init(i)
     {model with PageStack = (VehiclePage,VehicleModel a) :: model.PageStack ; }, Cmd.map VehicleMsg aCmd
    | _ -> res

let speciesListProc msg model res =
    match msg with
    | SpeciesList.Types.Msg.SelectedSpecies i ->
     let (a,aCmd) = Species.State.init(i)
     {model with PageStack = (SpeciesPage,SpeciesModel a) :: model.PageStack ; }, Cmd.map SpeciesMsg aCmd
    | _ -> res

let planetsListProc msg model res =
    match msg with
    | PlanetList.Types.Msg.SelectedPlanet i ->
     let (a,aCmd) = Planet.State.init(i)
     {model with PageStack = (PlanetPage,PlanetModel a) :: model.PageStack ; }, Cmd.map PlanetMsg aCmd
    | _ -> res

let update (msg : Msg) (model : Model) =
    let (_,pageModel) = model.PageStack.Head
    match msg, pageModel with
    | ApplicationMsg msg , ApplicationModel m ->
        let (a, aCmd) = Application.State.update msg m
        let i = (ApplicationPage, ApplicationModel a) :: removeFirst model.PageStack
        let (res, resCmd) = {model with PageStack = i}, Cmd.map ApplicationMsg aCmd

        match msg with
        | Application.Types.PeopleListMsg p -> peopleListProc p model (res, resCmd)
        | Application.Types.FilmListMsg p -> filmListProc p model (res, resCmd)
        | Application.Types.StarshipListMsg p -> starshipListProc p model (res, resCmd)
        | Application.Types.VehicleListMsg p -> vehicleListProc p model (res, resCmd)
        | Application.Types.SpeciesListMsg p -> speciesListProc p model (res, resCmd)
        | Application.Types.PlanetListMsg p -> planetsListProc p model (res, resCmd)
        | _ -> (res,resCmd)

    | PersonMsg msg, PersonModel m ->
        let (a, aCmd) = Person.State.update msg m
        let i = (PersonPage, PersonModel a) :: removeFirst model.PageStack
        let (res,resCmd) = {model with PageStack = i}, Cmd.map PersonMsg aCmd

        match msg with
        | Person.Types.Msg.ShowFilms links ->
            let (a,aCmd) = Films.State.init(links)
            {model with PageStack = (FilmsPage,FilmsModel a) :: model.PageStack ; }, Cmd.map FilmsMsg aCmd
        | Person.Types.Msg.ShowSpecies links ->
            let (a,aCmd) = SpeciesMore.State.init(links)
            {model with PageStack = (SpeciesMorePage,SpeciesMoreModel a) :: model.PageStack ; }, Cmd.map SpeciesMoreMsg aCmd
        | Person.Types.Msg.ShowStarships links ->
            let (a,aCmd) = Starships.State.init(links)
            {model with PageStack = (StarshipsPage,StarshipsModel a) :: model.PageStack ; }, Cmd.map StarshipsMsg aCmd
        | Person.Types.Msg.ShowVehicles links ->
            let (a,aCmd) = Vehicles.State.init(links)
            {model with PageStack = (VehiclesPage,VehiclesModel a) :: model.PageStack ; }, Cmd.map VehiclesMsg aCmd


    | FilmMsg msg , FilmModel m ->
        let (a, aCmd) = Film.State.update msg m
        let i = (FilmPage, FilmModel a) :: removeFirst model.PageStack
        {model with PageStack = i}, Cmd.map FilmMsg aCmd
    | StarshipMsg msg, StarshipModel m ->
        let (a, aCmd) = Starship.State.update msg m
        let i = (StarshipPage, StarshipModel a) :: removeFirst model.PageStack
        {model with PageStack = i}, Cmd.map StarshipMsg aCmd
    | VehicleMsg msg, VehicleModel m ->
        let (a, aCmd) = Vehicle.State.update msg m
        let i = (VehiclePage, VehicleModel a) :: removeFirst model.PageStack
        {model with PageStack = i}, Cmd.map VehicleMsg aCmd
    | SpeciesMsg msg, SpeciesModel m ->
        let (a, aCmd) = Species.State.update msg m
        let i = (SpeciesPage, SpeciesModel a) :: removeFirst model.PageStack
        {model with PageStack = i}, Cmd.map SpeciesMsg aCmd
    | PlanetMsg msg, PlanetModel m ->
        let (a, aCmd) = Planet.State.update msg m
        let i = (PlanetPage, PlanetModel a) :: removeFirst model.PageStack
        {model with PageStack = i}, Cmd.map PlanetMsg aCmd
    | FilmsMsg msg, FilmsModel m ->
        let (a, aCmd) = Films.State.update msg m
        let i = (FilmsPage, FilmsModel a) :: removeFirst model.PageStack
        let (res, resCmd) = {model with PageStack = i}, Cmd.map FilmsMsg aCmd

        match msg with
        | Films.Types.FilmListMsg p -> filmListProc p model (res, resCmd)
        | _ -> (res, resCmd)

    | SpeciesMoreMsg msg, SpeciesMoreModel m ->
        let (a, aCmd) = SpeciesMore.State.update msg m
        let i = (SpeciesMorePage, SpeciesMoreModel a) :: removeFirst model.PageStack
        let (res, resCmd) = {model with PageStack = i}, Cmd.map SpeciesMoreMsg aCmd

        match msg with
        | SpeciesMore.Types.SpeciesListMsg p -> speciesListProc p model (res, resCmd)
        | _ -> (res, resCmd)

    | VehiclesMsg msg, VehiclesModel m ->
        let (a, aCmd) = Vehicles.State.update msg m
        let i = (VehiclesPage, VehiclesModel a) :: removeFirst model.PageStack
        let (res, resCmd) = {model with PageStack = i}, Cmd.map VehiclesMsg aCmd

        match msg with
        | Vehicles.Types.VehicleListMsg p -> vehicleListProc p model (res, resCmd)
        | _ -> (res, resCmd)

    | StarshipsMsg msg, StarshipsModel m ->
        let (a, aCmd) = Starships.State.update msg m
        let i = (StarshipPage, StarshipsModel a) :: removeFirst model.PageStack
        let (res, resCmd) = {model with PageStack = i}, Cmd.map StarshipsMsg aCmd

        match msg with
        | Starships.Types.StarshipListMsg p -> starshipListProc p model (res, resCmd)
        | _ -> (res, resCmd)

    | PagePopped, _ -> {model with PageStack = removeFirst model.PageStack}, Cmd.none
    | _,_ -> failwithf "%A and %A is not a valid msg model pair" msg model

Here is a caveat: you might not like | _,_ -> failwithf "%A and %A is not a valid msg model pair" msg model as it will break the compiler's help. If you do have some missing pattern, it will not show because of the wildcard pattern match at the end.

But that is the only thing I found out when I like to arrange multiple pages for Fable - Elmish applications (ref: Safe - Book Store), and here I also tried the same thing. So, if there is any better way to do it for web and EXF, I am all ears for suggestions.

Personal Take

EXF is good. Like seriously, a good step in the right direction. But I guess it can and should learn a few tips and tricks from other platforms like Flutter (way better reload and faster development cycle), Ionic (latest one) / Cordova (it is web and everything just works). I know that comparison is not possible, but tooling can surely be improved. I don't know about Xamarin Forms, but I surely can give EXF another try for large applications given improved tooling in the near future.

Frequently Asked Questions

What is Elmish Xamarin Forms and how does it differ from traditional Xamarin development?

Elmish Xamarin Forms (EXF) brings the Elm architecture pattern to mobile development with an immutable UI approach. Unlike traditional Xamarin Forms that uses XAML for UI design, EXF allows you to write views in F# as functions, making the UI more composable and reusable while maintaining the same Elmish patterns you'd use in Fable web applications.

Why would F# developers prefer Elmish Xamarin Forms over XAML-based approaches?

Many F# developers struggle with XML-based design tools like XAML due to reusability constraints. EXF solves this by letting views become regular F# functions, giving you access to F#'s powerful functional capabilities for UI creation. This creates a more cohesive development experience where the entire application logic and UI can be written in functional F#.

Can I use my Elmish knowledge from Fable when developing with Xamarin Forms?

Yes, Elmish Xamarin Forms is specifically designed to allow Fable-Elmish developers to apply their existing knowledge to mobile development. The architecture, state management, and functional patterns you use in Fable-Elmish translate directly to EXF, making the transition between web and mobile development more seamless.

What kind of applications can you build with Elmish Xamarin Forms?

You can build standard cross-platform mobile applications with EXF, including those with tabs, data fetching, list views, and detail views. The Star Wars API example demonstrates typical mobile UI patterns like pull-to-refresh data, list navigation, and detail screens that you'd build in any production mobile app.

Is Elmish Xamarin Forms ready for production use?

EXF is a functional and usable framework for building mobile applications. However, the author notes that the focus is on matching Fable-Elmish patterns rather than implementing all Xamarin-specific features, so you may need to research platform-specific requirements depending on your application needs.

Share this article