A comprehensive guide to building cross-platform mobile applications using Fabulous MAUI with F#.
📖 New to this tutorial? Start with INDEX.md for a complete overview and learning path!
- Introduction
- What is Fabulous MAUI?
- Getting Started
- MVU Architecture
- Sample Application
- Project Structure
- Building the UI
- Custom Controls
- Navigation
- Best Practices
This tutorial demonstrates how to build a cross-platform mobile application for iOS and Android using Fabulous MAUI with F#. The sample application is a simple task management app that showcases the core concepts without exposing any sensitive business logic.
Fabulous MAUI is a declarative UI framework for building cross-platform mobile and desktop applications using F# and .NET MAUI. It uses the Model-View-Update (MVU) architecture pattern, inspired by Elm, to create predictable and maintainable applications.
- Declarative UI: Build UI with functional, composable components
- MVU Architecture: Clear separation of concerns with unidirectional data flow
- Type Safety: Leverage F#'s strong type system
- Cross-Platform: Write once, run on iOS, Android, macOS, and Windows
- Hot Reload: Fast development iteration
- .NET 9.0 SDK or later
- Visual Studio 2022 (Windows/Mac) or JetBrains Rider
- For iOS development: macOS with Xcode
- For Android development: Android SDK
- Install .NET MAUI workload:
dotnet workload install maui- Create a new F# MAUI project or use the sample provided in this tutorial.
The MVU (Model-View-Update) architecture consists of three main components:
The Model represents the application state. It's an immutable data structure that holds all the information needed to render the UI.
type Model = {
Tasks: MTask list
Filter: TaskFilter
IsLoading: bool
}The Update function processes messages (events) and returns a new model state. It's a pure function that takes the current model and a message, then returns the updated model.
let update msg model =
match msg with
| AddTask ->
let newTask = { Id = Guid.NewGuid(); Title = model.NewTaskTitle; IsCompleted = false }
{ model with Tasks = newTask :: model.Tasks; NewTaskTitle = "" }
| UpdateNewTaskTitle text ->
{ model with NewTaskTitle = text }
| ToggleTaskCompletion taskId ->
let updatedTasks =
model.Tasks
|> List.map (fun t ->
if t.Id = taskId then { t with IsCompleted = not t.IsCompleted }
else t)
{ model with Tasks = updatedTasks }The View is a pure function that takes the model and returns a description of the UI. It uses a declarative syntax to build the UI tree.
let view model =
ContentPage(
VStack() {
Entry(model.NewTaskTitle, UpdateNewTaskTitle)
Button("Add Task", AddTask)
for task in model.Tasks do
HStack() {
Label(task.Title)
CheckBox(task.IsCompleted, (fun _ -> ToggleTaskCompletion task.Id))
}
}
)The sample application included in this tutorial is a Task Manager app that demonstrates:
- Creating and displaying tasks
- Marking tasks as complete/incomplete
- Filtering tasks by status
- Using a custom radial slider for priority selection
- In-memory data storage (no external database)
- Clean architecture without business-specific logic
- Task Management: Add, view, and complete tasks
- Priority Slider: Use a radial slider control to set task priority
- Filtering: View all, active, or completed tasks
- Clean UI: Modern, responsive design
FabulousMauiTutorial/
├── README.md # This tutorial
├── TaskManagerApp/ # Sample application
│ ├── TaskManagerApp.fsproj # F# project file
│ ├── MauiProgram.fs # App initialization
│ ├── Domain.fs # Domain models
│ ├── MockData.fs # In-memory data store
│ ├── Controls/
│ │ └── RadialSlider.fs # Custom radial slider control
│ ├── Features/
│ │ ├── TaskList/
│ │ │ ├── Types.fs # Task list types
│ │ │ ├── State.fs # Task list state management
│ │ │ └── View.fs # Task list UI
│ │ └── TaskDetail/
│ │ ├── Types.fs # Task detail types
│ │ ├── State.fs # Task detail state management
│ │ └── View.fs # Task detail UI
│ ├── Root/
│ │ ├── Types.fs # Root types
│ │ ├── State.fs # Root state management
│ │ └── View.fs # Root view and navigation
│ ├── Resources/ # Images, fonts, etc.
│ └── Platforms/ # Platform-specific code
│ ├── Android/
│ └── iOS/
Fabulous MAUI provides builders for all standard MAUI controls:
// Labels
Label("Hello, World!")
.fontSize(24.0)
.textColor(Colors.Blue)
// Buttons
Button("Click Me", OnButtonClicked)
.backgroundColor(Colors.Green)
// Text Input
Entry(model.Text, TextChanged)
.placeholder("Enter text...")
// Lists
(VStack() {
for item in model.Items do
Label(item.Name)
}).spacing(10.0)// Vertical Stack
VStack() {
Label("Item 1")
Label("Item 2")
Label("Item 3")
}
// Horizontal Stack
HStack() {
Label("Left")
Label("Right")
}
// Grid
Grid(rowdefs = [Auto; Star; Auto], coldefs = [Star; Star]) {
Label("Header").gridRow(0).gridColumnSpan(2)
Label("Content 1").gridRow(1).gridColumn(0)
Label("Content 2").gridRow(1).gridColumn(1)
Label("Footer").gridRow(2).gridColumnSpan(2)
}The tutorial includes a custom Radial Slider control for selecting task priority. This demonstrates how to integrate custom SkiaSharp-based controls into Fabulous MAUI.
RadialSlider(
model.Priority,
fun args -> PriorityChanged args.NewValue
)
.minimum(0.0)
.maximum(10.0)
.trackColor(Colors.LightGray)
.trackProgressColor(Colors.Blue)
.knobColor(Colors.White)The radial slider is implemented in F# using SkiaSharp and wrapped with Fabulous bindings:
- F# SkiaSharp Control (
SkRadialSliderinRadialSlider.fs): Core rendering and touch handling - F# Wrapper (
CustomRadialSliderinRadialSlider.fs): CLIEvent bridge for Fabulous - Fabulous Bindings (
RadialSliderBuilderinRadialSlider.fs): Declarative widget integration
The sample app demonstrates navigation between pages:
// Navigation is managed in the Root update function, returning 3-tuples:
// (Model, CmdMsg list, Msg option)
let update msg model =
match msg with
| NavigateTo page ->
{ model with
CurrentPage = page
NavigationStack = model.CurrentPage :: model.NavigationStack
}, [], None
| NavigateBack ->
match model.NavigationStack with
| [] -> model, [], None
| prevPage :: rest ->
{ model with
CurrentPage = prevPage
NavigationStack = rest
TaskDetailModel = None
}, [], NoneNavigation is handled through the MVU pattern by updating the model state.
Always create new model instances instead of mutating existing ones.
// Good
{ model with NewField = value }
// Bad (don't do this)
model.NewField <- valueKeep update and view functions pure (no side effects).
// Pure update function
let update msg model =
match msg with
| Increment -> { model with Counter = model.Counter + 1 }Handle side effects (API calls, storage) using commands.
// CmdMsg defines side-effect commands; Msg handles their results
type CmdMsg =
| LoadTasks
| ToggleCompletion of TaskId
| DeleteTaskCmd of TaskId
let mapCmdMsg = function
| LoadTasks ->
Cmd.ofAsyncMsg (async {
let! tasks = TaskApi.loadTasks()
return TasksLoaded tasks
})
| ToggleCompletion taskId ->
Cmd.ofAsyncMsg (async {
let! result = TaskApi.toggleTaskCompletion taskId
return TaskUpdated result
})
| DeleteTaskCmd taskId ->
Cmd.ofAsyncMsg (async {
let! _ = TaskApi.deleteTask taskId
let! tasks = TaskApi.loadTasks()
return TasksLoaded tasks
})Structure your code by feature rather than by technical layer.
Features/
├── TaskList/
│ ├── Types.fs
│ ├── State.fs
│ └── View.fs
Use composition and higher-order functions instead of inheritance.
let taskCard task onToggle =
Border() {
HStack() {
Label(task.Title)
CheckBox(task.IsCompleted, onToggle)
}
}
.stroke(Colors.Gray)
.padding(10.0)Leverage F#'s type system to prevent errors.
type TaskId = TaskId of Guid
type Priority =
| Low
| Medium
| High
type MTask = {
Id: TaskId
Title: string
Description: string
Priority: Priority
IsCompleted: bool
CreatedAt: DateTime
}Important: See BUILD_NOTES.md for prerequisite setup (MAUI workload required).
- Navigate to the TaskManagerApp directory:
cd FabulousMauiTutorial/TaskManagerApp- Restore dependencies:
dotnet restore- Run on Android:
dotnet build -t:Run -f net9.0-android- Run on iOS (macOS only):
dotnet build -t:Run -f net9.0-ios- INDEX.md - Complete overview and learning path
- GETTING_STARTED.md - Detailed setup and build instructions
- ARCHITECTURE.md - Deep architectural insights
- BUILD_NOTES.md - Build requirements and troubleshooting
- Fabulous Documentation
- .NET MAUI Documentation
- F# Language Guide
- MVU Architecture
- The Elmish Book
- Guidance for .NET systems development
This tutorial provides a foundation for building cross-platform mobile applications with Fabulous MAUI and F#. The sample Task Manager app demonstrates core concepts while remaining generic and business-agnostic.
Key takeaways:
- MVU architecture provides a clear, predictable application structure
- Fabulous enables declarative UI with F#'s functional programming features
- Custom controls can be integrated seamlessly
- Type safety helps prevent runtime errors
- The framework supports modern mobile app development patterns
Happy coding! 🚀