Name: Elmish.XamarinForms
Owner: F# Community Project Incubation Space
Description: Elmish for Xamarin.Forms
Created: 2018-02-05 17:00:17.0
Updated: 2018-05-23 09:30:01.0
Pushed: 2018-05-23 16:24:14.0
Homepage: https://github.com/fsprojects/Elmish.XamarinForms/blob/master/README.md
Size: 2261
Language: F#
GitHub Committers
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
Never write a ViewModel class again! Conquer the world with clean dynamic UIs!
This library uses a variation of elmish, an Elm architecture implemented in F#, to build Xamarin.Forms applications. Elmish was originally written for Fable applications, however it is used here for mobile applications using Xamarin.Forms. This is a sample and may change.
To quote @dsyme:
In my work for Xamarin, I'm asking myself “what will appeal to F# devs who want to do Xamarin programming?“. These devs are very code-oriented and know F#. People are liking Elm and React via Elmish and also React Native. Can we apply some of the lessons to Xamarin programming?
Elmish.XamarinForms
to to your shared code project.Here is a full example of an app:
The messages dispatched by the view
Msg =
| Pressed
The model from which the view is generated
Model =
{ Pressed: bool }
Returns the initial state
init() = { Pressed=false }
The funtion to update the view
update (msg:Msg) (model:Model) =
match msg with
| Pressed -> { model with Pressed = true }
The view function giving updated content for the page
view (model: Model) dispatch =
if model.Pressed then
Xaml.Label(text="I was pressed!")
else
Xaml.Button(text="Press Me!", command=(fun () -> dispatch Pressed))
App () =
inherit Application ()
let runner =
Program.mkSimple init update view
|> Program.withConsoleTrace
|> Program.withDynamicView
|> Program.run
The init function returns your initial state, and each model gets an update function for message processing. The view
function computes an immutable Xaml-like description. In the above example, the choice between a label and button depends on the model.Pressed
value.
Some advantages of using an immutable model are:
init
, update
and view
functionsThe sample CounterApp contains a slightly larger example of Button/Label/Slider controls.
The sample AllControls contains examples of instantiating most controls in Xamarin.Forms.Core
.
Screenshots from Anrdoid (Google Pixel):
Dynamic view
functions are written using an F# DSL, see Elmish.XamarinForms.DynamicViews
.
Dynamic Views excel in cases where the existence, characteristics and layout of the view depends on information in the model. React-style differential update is used to update the Xamarin.Forms display based on the previous and current view descriptions.
Notes:
Xaml.Button(...)
.button |> withText "Hello"
(note: you don't have
to use these, and the samples don't use them).Dynamic views are only efficient for large UIs if the unchanging parts of a UI are “memoized”, returning identical
objects on each invocation of the view
function. This must be done explicitly, currently using dependsOn
. Here is an example for a 6x6 Grid that depends on nothing, i.e. never changes:
view model dispatch =
...
dependsOn () (fun model () ->
Xaml.StackLayout(
children=
[ Xaml.Label(text=sprintf "Grid (6x6, auto):")
Xaml.Grid(rowdefs= [for i in 1 .. 6 -> box "auto"],
coldefs=[for i in 1 .. 6 -> box "auto"],
children = [ for i in 1 .. 6 do for j in 1 .. 6 ->
Xaml.BoxView(Color((1.0/float i), (1.0/float j), (1.0/float (i+j)), 1.0) )
.GridRow(i-1).GridColumn(j-1) ] )
])
Inside the function - the one passed to dependsOn
- the model
is rebound to be inaccessbile with a DoNotUseMe
type so you can't use it. Here is an example where some of the model is extracted:
view model dispatch =
...
dependsOn (model.CountForSlider, model.StepForSlider) (fun model (count, step) ->
Xaml.Slider(minimum=0.0, maximum=10.0, value= double step,
valueChanged=(fun args -> dispatch (SliderValueChanged (int (args.NewValue + 0.5)))),
horizontalOptions=LayoutOptions.Fill))
...
In the example, we extract properties CountForSlider
and StepForSlider
from the model, and bind them to count
and step
. If either of these change, the section of the view will be recomputed and no adjustments will be made to the UI.
If not, this section of the view will be reused. This helps ensure that this part of the view description only depends on the parts of the model extracted.
You can also use
fix
function for portions of a view that have no dependencies at all (besides the “dispatch” function)fixf
function for command callbacks that have no dependencies at all (besides the “dispatch” function)In Elmish.XamarinForms, resources dictionaries are just “simple F# programming”, e.g.
horzOptions = LayoutOptions.Center
vertOptions = LayoutOptions.CenterAndExpand
is basically the eqivalent of Xaml:
tentPage.Resources>
<ResourceDictionary>
<LayoutOptions x:Key="horzOptions"
Alignment="Center" />
<LayoutOptions x:Key="vertOptions"
Alignment="Center"
Expands="True" />
</ResourceDictionary>
ntentPage.Resources>
In other words, you can normally forget about resource dictionaries and just program as you would normally in F#.
Other kinds of resources like images need a little more attention and you may need to ship multiple versions of images etc. for Android and iOS. TBD: write a guide on these, in the meantime see the samples.
Multiple pages are just generated as part of the overall view.
Four multi-page navigation models are shown in AllControls
:
The basic principles are easy:
pages
property of NavigationPage
.HasNavigationBar
and HasBackButton
on each sub-page according to your desireupdate
adjusts the page stack in the modelview model dispatch =
Xaml.NavigationPage(pages=
[ for page in model.PageStack do
match page with
| "Home" ->
yield Xaml.ContentPage(...).HasNavigationBar(true).HasBackButton(true)
| "PageA" ->
yield Xaml.ContentPage(...).HasNavigationBar(true).HasBackButton(true)
| "PageB" ->
yield Xaml.ContentPage(...).HasNavigationBar(true).HasBackButton(true)
])
Return a TabbedPage
from your view:
view model dispatch =
Xaml.TabbedPage(children= [ ... ])
Return a CarouselPage
from your view:
view model dispatch =
Xaml.CarouselPage(children= [ ... ])
Principles:
MasterDetailPage
from your view
functionSee the AllControls
sample
The programming model supports several more bespoke uses of the underlying ListView
control from Xamarin.Forms.Core
. In the simplest form, just called ListView
, the items
property specifies a collection of visual elements:
Xaml.ListView(items = [ Xaml.Label "Ionide"
Xaml.Label "Visual Studio"
Xaml.Label "Emacs"
Xaml.Label "Visual Studio Code"
Xaml.Label "JetBrains Rider"],
itemSelected=(fun idx -> dispatch (ListViewSelectedItemChanged idx)))
In the underlying implementation, each visual item is placed in a ContentCell
.
Currently the itemSelected
callback uses integers indexes for
keys to identify the elements (NOTE: this may change in future updates).
There is also a ListViewGrouped
for grouped items of data.
“Infinite” (really “unbounded”) lists are created by using the itemAppearing
event to prompt a message which nudges the
underlying model in a direction that will then supply new items to the view.
For example, consider this pattern:
Model =
{ ...
LatestItemAvailable: int
}
Message =
...
| GetMoreItems of int
update msg model =
match msg with
| ...
| GetMoreItems n -> { model with LatestItemAvailable = n }
view model dispatch =
...
Xaml.ListView(items = [ for i in 1 .. model.LatestItemAvailable do
yield Xaml.Label("Item " + string i) ],
itemAppearing=(fun idx -> if idx >= max - 2 then dispatch (GetMoreItems (idx + 10) ) ) )
...
Note:
LatestItemAvailable
(normally it would really be a list of actual entities drawn from a data source)Item 1
onwardsitemAppearing
event is called for each item, e.g. when item 10
appearsSurprisingly even this naive technique is fairly efficient. There are numerous ways to make this more efficient (we aim to document more of these over time too). One simple one is to memoize each individual visual item using dependsOn
:
items = [ for i in 1 .. model.LatestItemAvailable do
yield dependsOn i (fun model i -> Xaml.Label("Item " + string i)) ]
With that, this simple list views scale to > 10,000 items on a modern phone, though your mileage may vary.
There are many other techniques (e.g. save the latest collection of visual element descriptions in the model, or to use a ConditionalWeakTable
to associate it with the latest model). We will document further techniques in due course.
Thre is also an itemDisappearing
event for ListView
that can be used to discard data from the underlying model and restrict the
range of visual items that need to be generated.
There are a few different kinds of list in view descriptions:
ListView
, see above)children
)pages
)The perf of incremental update to these is progressively less important as you go down that list above.
For all of the above, the typical, naive implementation of the view
function returns a new list
instance on each invocation. The incremental update of dynamic views maintains a corresponding mutable target
(e.g. the Children
property of a Xamarin.Forms.StackLayout
, or an ObservableCollection
to use as an ItemsSource
to a ListView
) based on the previous (PREV) list and the new (NEW) list. The list diffing currently does the following:
This means
Basically, incremental update is faster if lists are changing at their beginning, rather than their end.
The above is sufficient for many purposes, but care must always be taken with large lists and data sources, see ListView
above for example. Care must also be taken whenever data updates very rapidly.
Styling is a significant topic in Xamarin.Forms programming. See the extensive Xamarin.Forms documentation on styling.
One approach is to manually code up styling simply by using normal F# programming to abstract away commonality between various parts of your view logiv.
We do not give a guide here as it is routine application of F# coding. The Fulma approach to styling may also be of interest and provide inspiration.
There are many upsides to this approach. The downsides are:
klayout {
in: 20;
.mainPageTitle {
font-style: bold;
font-size: medium;
}
.detailPageTitle {
font-style: bold;
font-size: medium;
text-align: center;
}
e `stacklayout` referes to all elements of that type, and `.mainPageTitle` refers to a specific element style-class path.
dd the style sheet to your app as an `EmbeddedResource` node
oad it into your app:
type App () as app =
inherit Application ()
do app.Resources.Add(StyleSheet.FromAssemblyResource(Assembly.GetExecutingAssembly(),"MyProject.Assets.styles.css"))
et `StyleClass` for named elements, e.g.
Xaml.Label(text="Hello", styleClass=detailPageTitle")
...
Xaml.Label(text="Main Page", styleClass="mainPageTitle")
"Xaml" coding via explicit `Style` objects
can also use "Xaml styling" by creating specific `Style` objects using the `Xamarin.Forms` APIs directly
attaching them to your application. See [the Xamarin.Forms documentation](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/styles/xaml/). We don't go into details here
odels
Models: Messages and Validation
dation is generally done on updates to the model, storing error messages from validation logic in the model
hey can be correctly and simply displayed to the user. Here is an example of a typical pattern.
type Temperature =
| Value of double
| ParseError of string
type Model =
{ TempF: Temperature
TempC: Temperature }
/// Validate a temperature in Farenheit, can be shared between client/server
let validateF text = ... // return a Result
/// Validate a temperature in celcius, can be shared between client/server
let validateC text = // return a Result
let update msg model =
match msg with
| SetF textF ->
match validateF textF with
| Ok newF -> { model with TempF = Value newF }
| Error msg -> { model with TempF = ParseError msg }
| SetC textC ->
match validateC textC with
| Ok newC -> { model with TempC = Value newC }
| Error msg -> { model with TempC = ParseError msg }
that the same validation logic can be used in both your app and a service back-end.
Models: Saving Application State
ication state is very simple to save by serializing the model into `app.Properties`. For example, you can store as JSON as follows using [`FsPickler` and `FsPickler.Json`](https://github.com/mbraceproject/FsPickler), which use `Json.NET`:
open MBrace.FsPickler.Json
type Application() =
....
let modelId = "model"
override __.OnSleep() =
app.Properties.[modelId] <- FsPickler.CreateJsonSerializer().PickleToString(runner.Model)
override __.OnResume() =
try
match app.Properties.TryGetValue modelId with
| true, (:? string as json) ->
runner.SetCurrentModel(FsPickler.CreateJsonSerializer().UnPickleOfString(json), Cmd.none)
| _ -> ()
with ex ->
program.onError("Error while restoring model found in app.Properties", ex)
override this.OnStart() = this.OnResume()
essages, Commands and Control
Messages, Commands and Asynchronous Actions
chronous actions are triggered by having the `update` function return "commands", which can trigger later `dispatch` of further messages.
ange `Program.mkSimple` to `Program.mkProgram`
let program = Program.mkProgram App.init App.update App.view
ange your `update` function to return a pair of a model and a command. For most messages the command will be `Cmd.none` but for basic async actions use `Cmd.ofAsyncMsg`.
example, here is one pattern for a timer loop that can be turned on/off:
type Model =
{ ...
TimerOn: bool
}
type Message =
| ...
| TimedTick
| TimerToggled of bool
let timerCmd =
async { do! Async.Sleep 200
return TimedTick }
|> Cmd.ofAsyncMsg
let update msg model =
match msg with
| ...
| TimerToggled on -> { model with TimerOn = on }, (if on then timerCmd else Cmd.none)
| TimedTick -> if model.TimerOn then { model with Count = model.Count + model.Step }, timerCmd else model, Cmd.none
state-resurrection `OnResume` logic of your application (see above) should also be adjusted to restart
opriate `async` actions accoring to the state of the application.
: Making all stages of async computations (and their outcomes, e.g. cancellation and/or exceptions) explicit can add
tional messages and model states. This comes with pros and cons. Please discuss concrete examples by opening issues
his repository.
Messages: Global asynchronous event subscriptions
can also set up global subscriptions, which are events sent from outside the view or the dispatch loop. For example, dispatching `ClockMsg` messages on a global timer:
let timerTick dispatch =
let timer = new System.Timers.Timer(1.0)
timer.Elapsed.Subscribe (fun _ -> dispatch (ClockMsg System.DateTime.Now)) |> ignore
timer.Enabled <- true
timer.Start()
...
let runner =
...
|> Program.withSubscription timerTick
...
tatic Views and "Half Elmish"
ome circumstances there are advantages to using static Xaml, and static bindings from the model to those views. This is called "Half Elmish" and is the primary technique used by [`Elmish.WPF`](https://github.com/Prolucid/Elmish.WPF) at time of writing. (It was also the original technique used by this repo and the prototype `Elmish.Forms`).
[HALF-ELMISH.md](HALF-ELMISH.md)
oadmap
[ROADMAP.md](https://github.com/fsprojects/Elmish.XamarinForms/blob/master/ROADMAP.md), a list of TODOs.
uilding
[DEVGUIDE.md](DEVGUIDE.md).
ontributing
se contribute to this library through issue reports, pull requests, code reviews and discussion.
its
-
library is inspired by [Elmish.WPF](https://github.com/Prolucid/Elmish.WPF), [Elmish.Forms](https://github.com/dboris/elmish-forms) and [elmish](https://github.com/elmish/elmish), written by [et1975](https://github.com/et1975). This project technically has no tie to [Fable](http://fable.io/), which is an F# to JavaScript transpiler that is definitely worth checking out.