Fabulous
Od dość dawma spoglądam w stronę języka F#. Zawsze jednak miałem problem z wykorzystaniem go w aplikacjach desktopowych czy mobilnych. W większości przypadków pisanie kodu odpowiedzialnego za UI kończy się pisaniem kodu obiektowego, który moim zdaniem nie jest czytelny. Rozwiązaniem tego problemu moźe być uźycie wzorca MVU (Model View Update).
Wzorzec MVU
Wzorzec ten nazywany jest także Elm Architecture, ponieważ został rozpowszechniony przez język programowania Elm.
Schemat architektury możecie zobaczyć na obrazku poniżej:
Mamy tu wyróżnione trzy elemnty:
- UI - kontrolki, z którymi wchodzi w interakcje użytkownik. Każda interakcja może generować wiadomość.
- wiadomość zostaje przechwycona przez funkcję
update
. Na podstawie wiadomości oraz aktualnego modelu generowany jest nowy widok. - nowy model jest przekazywany do funkcji
view
, która zajmuję się generowaniem widoku.
Następnie widok jest prezentowany użytkownikowi. I pętla się powtarza.
Pierwszy program
Aby rozpocząć pracę, najpierw naleźy zainstalować szablon projektu. Do tego słuźy polecenie: dotnet new -i Fabulous.Templates
.
Gdy szablon się zainstaluje, naleźy wykonać polecenie dotnet new fabulous-app -o FabulousDemo
.
Wykonanie tej komendy spowoduje utworzenie solucji zawierającej trzy projekty: FabulousDemo.iOS
, FabulousDemo.Android
oraz FabulousDemo
. Jak widać stuktura projektu nie róźni się niczym od standardowego projektu Xamarin.Forms.
W trzecim z wymienionych projektów znajduje się plik FabulousDemo.fs
, który zawiera całą logikę aplikacji. Pierwszym znaczącym elementem jest deklaracja typu reprezentującego model aplikacji.
type Model =
{ Count : int
Step : int
TimerOn: bool }
Jak widać jest to rekord posiadający trzy pola.
Następnie zadeklarowano typ (dyscriminated union
- coś jak enum
w C#), który reprezentuje wiadomości na jakie aplikacja będzie reagować.
type Msg =
| Increment
| Decrement
| Reset
| SetStep of int
| TimerToggled of bool
| TimedTick
Kolejnym elementem jest funkcja init()
, która zostanie wywołana raz w momencie startu aplikacji. Moźe ona przyjmować dodatkowe parametry. Funkcja zwraca krotkę, na która składa się model oraz komenda jaka ma być wykonana zaraz po zakończeniu funkcji. W tym przypadku - Cmd.None
- oznacza brak akcji.
let initModel = { Count = 0; Step = 1; TimerOn=false }
let init () = initModel, Cmd.none
Logika aplikacji znajduje się w funkcji update
. Przyjmuje ona dwa parametry: wiadomość oraz model. Podobnie jak funkcja init
zwraca krotkę.
let timerCmd =
async { do! Async.Sleep 200
return TimedTick }
|> Cmd.ofAsyncMsg
let update msg model =
match msg with
| Increment -> { model with Count = model.Count + model.Step }, Cmd.none
| Decrement -> { model with Count = model.Count - model.Step }, Cmd.none
| Reset -> init ()
| SetStep n -> { model with Step = n }, Cmd.none
| 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
W większośco przypadków funkcja zwraca model oraz Cmd.none
. Inaczej jest w przypadku wiadomości TimerTick
. Jeźeli timer jest włączony, to zostanie wykonana funkcaj timerCmd
.
Ostatnim elementem jest funkcja view
:
let view (model: Model) dispatch =
View.ContentPage(
content = View.StackLayout(padding = 20.0, verticalOptions = LayoutOptions.Center,
children = [
View.Label(text = sprintf "%d" model.Count, horizontalOptions = LayoutOptions.Center, widthRequest=200.0, horizontalTextAlignment=TextAlignment.Center)
View.Button(text = "Increment", command = (fun () -> dispatch Increment), horizontalOptions = LayoutOptions.Center)
View.Button(text = "Decrement", command = (fun () -> dispatch Decrement), horizontalOptions = LayoutOptions.Center)
View.Label(text = "Timer", horizontalOptions = LayoutOptions.Center)
View.Switch(isToggled = model.TimerOn, toggled = (fun on -> dispatch (TimerToggled on.Value)), horizontalOptions = LayoutOptions.Center)
View.Slider(minimumMaximum = (0.0, 10.0), value = double model.Step, valueChanged = (fun args -> dispatch (SetStep (int (args.NewValue + 0.5)))), horizontalOptions = LayoutOptions.FillAndExpand)
View.Label(text = sprintf "Step size: %d" model.Step, horizontalOptions = LayoutOptions.Center)
View.Button(text = "Reset", horizontalOptions = LayoutOptions.Center, command = (fun () -> dispatch Reset), canExecute = (model <> initModel))
]))
Wszystkie wyźej wymienione elementy naleźy skleić w całość. Słuźy do tego poniźsze polecenie:
Program.mkProgram init update view
|> Program.runWithDynamicView app