# .NET MAUI From Zero

> Learn Microsoft's cross-platform UI framework: one C# codebase that ships to Android, iOS, macOS, and Windows. XAML and layouts, controls and data binding, the MVVM pattern, navigation with Shell, calling APIs and local storage, and platform features and deployment. Write once, run native, taught mental-model-first.


---

# .NET MAUI From Zero

.NET MAUI (Multi-platform App UI) is how C# developers build native apps for **Android, iOS, macOS, and
Windows from one codebase**. It's the evolution of Xamarin.Forms, rebuilt on .NET: you write your UI and
logic once, and MAUI renders it with each platform's native controls. If you already know C#, MAUI is your
route into mobile and desktop without learning Swift, Kotlin, and a separate Windows stack.

The mental model is two layers tied by a pattern. The **UI** is a tree of controls, usually described in
**XAML** (a declarative markup) with a C# code-behind - a page holds layouts, layouts hold controls. The
**logic** lives in a **ViewModel**, and the two are joined by **data binding** (the MVVM pattern): the View
binds to properties and commands on the ViewModel, so UI and logic stay decoupled and testable. Hold "XAML
describes the UI, a ViewModel holds the state and behavior, and binding wires them together," and MAUI's
moving parts fall into place.

> 📝 This teaches the **framework** - it assumes you know **C#**: classes, properties, events,
> `async`/`await`, and interfaces ([C# From Zero](/guides/csharp-from-zero)). MVVM and binding echo other
> component UIs ([Blazor](/guides/blazor-from-zero) is the web sibling), and it consumes
> [ASP.NET Core](/guides/aspnet-core-from-zero) APIs. MAUI builds native apps, so examples are shown as
> XAML/C# rather than run on the page.

## How to read this

Read in order - it builds one small app (a cross-platform **notes** app: a list, a detail/edit page, save and
delete) from a single page to a navigable, MVVM-structured, API-aware app. Phases carry difficulty badges.

## The phases

**Part 1 - The UI (🟢 Basic → 🟡)**
1. **[What MAUI Is & Your First App](01-what-maui-is.md)** 🟢 - one codebase, native targets, XAML + code-behind, and a running app.
2. **[XAML & Layouts](02-xaml-and-layouts.md)** 🟡 - pages, `StackLayout`/`Grid`, and arranging controls.
3. **[Controls & Data Binding](03-controls-and-data-binding.md)** 🟡 - common controls, `{Binding}`, and `BindingContext`.

**Part 2 - Real structure (🟡 → 🔴)**
4. **[The MVVM Pattern](04-mvvm.md)** 🔴 - ViewModels, `INotifyPropertyChanged`, commands, and the CommunityToolkit.Mvvm.
5. **[Navigation with Shell](05-navigation-with-shell.md)** 🟡 - pages, routes, and moving between screens.
6. **[Data & Calling APIs](06-data-and-apis.md)** 🔴 - `HttpClient`, JSON, and local storage (Preferences/SQLite).

**Part 3 - Ship it (🟡 → 🟢)**
7. **[Platform Features & Deployment](07-platform-features-and-deployment.md)** 🟡 - sensors/permissions, per-platform code, and building for the stores.
8. **[Where to Go Next](08-where-to-go-next.md)** 🟢 - MAUI vs Flutter/React Native, Blazor Hybrid, and what to build.

> The throughline: **XAML describes the UI, a ViewModel holds state and behavior, and data binding wires
> them** - one codebase, native everywhere. Hold that and MAUI is approachable.


---

# What MAUI Is & Your First App

You know [C#](/guides/csharp-from-zero). You've written classes, awaited tasks, raised events.
But shipping a real mobile app used to mean Swift for iOS, Kotlin for Android, and yet another
stack for Windows - three languages and toolchains for what is, to the user, one app.

**.NET MAUI tears that wall down.** MAUI (Multi-platform App UI) lets you build **native apps for
Android, iOS, macOS, and Windows from a single C# codebase**. It's the evolution of Xamarin.Forms,
rebuilt on modern .NET. You describe the UI and write the logic once, and MAUI renders it using
each platform's *real, native* controls - a MAUI button is a genuine Android button on Android and
a genuine UIKit button on iOS, not a lookalike.

If you already write C#, MAUI is your route into mobile and desktop without picking up Swift and
Kotlin. It's a [framework](/guides/what-a-framework-even-is) - it calls *your* code at the right
moments (when a screen appears, when a button is tapped).

📝 If you've seen [Blazor](/guides/blazor-from-zero), MAUI's C# sibling for the web, the resemblance
is real: both lean on a component/MVVM mindset where state drives the UI. The difference is what
they render - Blazor produces **HTML** in a browser; MAUI produces **native controls** on a device.

## The one mental model to hold

Hold these three pieces and how they connect - everything in this guide hangs off them.

💡 **XAML describes the UI. A ViewModel holds the state and behavior. Data binding wires them
together.** That triangle is the **MVVM pattern** (Model-View-ViewModel):

- The **View** is your screen - a tree of controls, usually written in **XAML**, a declarative markup
  that says *what* the UI looks like without spelling out *how* to draw it.
- The **ViewModel** is a plain C# class holding the screen's data (the text in a box, the items in a
  list) and the things it can do (save, delete).
- **Data binding** is the wire between them: the View says "show whatever's in the ViewModel's `Title`
  property," and when that property changes, the displayed text updates automatically.

You don't need to *build* a ViewModel yet - that's Phases 3 and 4. For now, hold the shape:
**markup for the look, a C# class for the state, binding to connect them.** This phase uses the
simplest version, with logic right next to its markup in a **code-behind** file.

## The minimal page

A MAUI screen is a `ContentPage`. Here's one in XAML - a greeting and a button:

```xml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NotesApp.MainPage">
    <VerticalStackLayout Padding="20" Spacing="10">
        <Label Text="Hello, MAUI" FontSize="24" />
        <Button Text="Tap me" Clicked="OnTap" />
    </VerticalStackLayout>
</ContentPage>
```

*What just happened:* this is the **View** - pure markup, no logic. Read it top to bottom:

- `<ContentPage>` is the root: one full screen. The two `xmlns=` lines are namespace declarations - 
  boilerplate telling the XAML parser which vocabulary of controls (`Label`, `Button`, layouts) is in
  play. Copy them and forget them.
- `x:Class="NotesApp.MainPage"` is the important link: it ties this markup to a C# class named
  `MainPage`. The XAML and the code-behind are **two halves of one page**.
- `<VerticalStackLayout>` is a layout container - it stacks its children top to bottom, with `Padding`
  (space inside the edges) and `Spacing` (gaps between children). More on layouts in
  [Phase 2](02-xaml-and-layouts.md).
- `<Label>` shows text; `<Button>` is tappable. `Clicked="OnTap"` says "when this button is tapped,
  call the `OnTap` method" - living in the other half of the page.

Here's that other half - the **code-behind**, a file named `MainPage.xaml.cs`:

```csharp
public partial class MainPage : ContentPage
{
    public MainPage() => InitializeComponent();

    private void OnTap(object sender, EventArgs e) =>
        DisplayAlert("Hi", "You tapped", "OK");
}
```

*What just happened:* this is plain C#, the partner to the XAML above.

- `partial class MainPage` - `partial` means "this class is defined in more than one file." The other
  part is **generated from your XAML** at build time, fusing markup and code into a single class.
- `InitializeComponent()` reads your XAML and builds the control tree - the layout, the label, the
  button. Without it, the page would be blank.
- `OnTap` is the event handler the XAML pointed at. Its signature - `(object sender, EventArgs e)` - is
  the standard .NET event shape you already know. When the user taps, MAUI calls it, and `DisplayAlert`
  pops a native dialog with a title, a message, and an OK button.

💡 This small example is the View (XAML) and its immediate logic (code-behind) for one screen. As the
app grows, you'll lift that logic out into a ViewModel so it's testable and reusable - the mental
shape never changes.

## Create and run your first app

Let's get one running. The .NET SDK ships a MAUI template:

```bash
dotnet new maui -o NotesApp
cd NotesApp
```

*What just happened:* `dotnet new maui` scaffolded a complete cross-platform app named `NotesApp` - the
project file, a starter `MainPage.xaml` + `MainPage.xaml.cs` (like the pair above), app startup code,
and the per-platform plumbing for all four targets - one folder, every platform.

Now run it. Unlike a web app, you don't `dotnet run` into a browser - you build *for a specific
device target* and launch it there:

```bash
dotnet build -t:Run -f net8.0-android
```

*What just happened:* `-t:Run` says "build, then launch," and `-f net8.0-android` picks the **target
framework** - here, Android (it starts an emulator or uses a connected device). Swap that flag to
choose a different platform:

- `-f net8.0-windows` - runs as a native Windows app
- `-f net8.0-ios` - runs on the iOS simulator or a device
- `-f net8.0-maccatalyst` - runs as a native macOS app

The C# and XAML you wrote stay the same across all of them; only the target flag changes.

📝 **iOS and macOS builds require a Mac.** Apple's toolchain (and code signing) only runs on macOS, so
building the `ios` or `maccatalyst` targets needs a Mac - either as your dev machine or paired over the
network from Windows. Android and Windows build from a Windows PC directly. An Apple rule, not a MAUI
limitation.

⚠️ The first build is slow - it restores packages, spins up an emulator, and compiles for the whole
platform. That's a one-time cost; later runs are much quicker.

## The running example: a notes app

Snippets are fine for a first look, but you learn a framework by building something. Across this
guide we'll grow one small, real app in your `NotesApp` project: a **cross-platform notes app**.

By the end it will let you:

- see a **list** of your notes,
- tap one to open a **detail/edit** page,
- and **save** or **delete** a note.

It starts in [Phase 2](02-xaml-and-layouts.md) as a single laid-out screen, gains real controls and
binding in Phase 3, gets a proper ViewModel and the MVVM treatment in Phase 4, learns to navigate
between list and detail in Phase 5, and persists your notes to storage in Phase 6. Build along and
you'll finish with a working app that runs natively on a phone, a Mac, and a Windows desktop - not
just a pile of code you read once.

## Recap

1. **MAUI builds native Android, iOS, macOS, and Windows apps from one C# codebase** - it's the
   evolution of Xamarin.Forms on modern .NET, and it renders each platform's *real* native controls.
2. **For C# developers, MAUI is the route into mobile and desktop** without learning Swift or Kotlin.
   It's the native-app sibling of [Blazor](/guides/blazor-from-zero), which targets the web (HTML)
   instead.
3. **The mental model is MVVM:** XAML describes the UI (the View), a ViewModel holds state and
   behavior, and data binding wires them together. This phase used the simple form, with logic in a
   **code-behind** file.
4. **A screen is a `ContentPage`** - XAML markup plus a `partial` code-behind class joined by
   `x:Class`. `InitializeComponent()` builds the control tree from the XAML; `Clicked="OnTap"` wires a
   tap to a C# handler.
5. **Create with `dotnet new maui`, run with `dotnet build -t:Run -f net8.0-<target>`** (`android`,
   `windows`, `ios`, `maccatalyst`). 📝 iOS and macOS builds need a Mac.
6. **We'll grow one running example** - a cross-platform notes app (list, detail/edit, save/delete) - 
   across the whole guide.

## Quick check

Three questions on the ideas that have to stick - what MAUI is, the page's two halves, and how you
run it:

```quiz
[
  {
    "q": "What does .NET MAUI let you build?",
    "choices": [
      "Native apps for Android, iOS, macOS, and Windows from a single C# codebase",
      "Server-rendered web pages written in JavaScript",
      "A single Android-only app that can't target other platforms",
      "Desktop apps for Windows only, written in XAML"
    ],
    "answer": 0,
    "explain": "MAUI (Multi-platform App UI) is the evolution of Xamarin.Forms: one C# codebase that ships native apps to Android, iOS, macOS, and Windows, rendering each platform's real native controls."
  },
  {
    "q": "In a MAUI page, what links the XAML markup to its C# code-behind?",
    "choices": [
      "The x:Class attribute on the ContentPage, paired with a partial class; InitializeComponent() builds the control tree from the XAML",
      "A SignalR connection that streams the UI to the device at runtime",
      "Nothing - XAML and C# are compiled into two completely separate apps",
      "A JavaScript bridge file you have to write by hand"
    ],
    "answer": 0,
    "explain": "x:Class names the partial class the XAML belongs to. The code-behind is the other half of that partial class, and its constructor calls InitializeComponent() to read the XAML and build the page's controls."
  },
  {
    "q": "How do you run a MAUI app on Android, and what's the catch with iOS?",
    "choices": [
      "dotnet build -t:Run -f net8.0-android; building the iOS target requires a Mac",
      "dotnet run --browser; iOS just needs a different browser",
      "There's no CLI - you can only run MAUI apps from Visual Studio on a Mac",
      "dotnet build -f net8.0-android, and iOS builds run fine from Windows alone"
    ],
    "answer": 0,
    "explain": "You build for a specific target with -t:Run and -f net8.0-<target> (android, windows, ios, maccatalyst). Apple's toolchain and code signing only run on macOS, so the ios and maccatalyst targets need a Mac."
  }
]
```


---

# XAML & Layouts

Here's the mental model for this whole phase: **a page is one root layout holding controls.** A screen in MAUI is a `ContentPage`, it holds exactly one thing - a layout - and the layout's job is to arrange the controls inside it. Controls are the leaves: labels, buttons, text boxes. Layouts are the branches that decide where those leaves sit.

The markup you write to describe this tree is **XAML** - an XML dialect, not a separate magic language. Every XAML element is just a C# object: write `<Label Text="Hi" />` and MAUI creates a `Label` object with its `Text` property set to `"Hi"`. The page's `<x:Class>` attribute names a C# partial class, and the XAML becomes part of that class at build time - it compiles down to the same C# objects you could write by hand.

> 📝 You *can* build the entire UI in C# instead of XAML - `new ContentPage { Content = new Label { Text = "Hi" } }`. Most MAUI code uses XAML because a declarative tree is easier to read and tweak than nested constructor calls.

## The page and its one root layout

A bare page looks like this:

```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NotesApp.MainPage">

    <Label Text="Hello, notes." />

</ContentPage>
```

*What just happened:* The `xmlns` lines map XML namespaces to MAUI's control library and to XAML's own keywords (that's where `x:Class` comes from). `x:Class="NotesApp.MainPage"` ties this file to the C# partial class `MainPage` in your project, and the `ContentPage` holds a single child - here a lone `Label`. A `ContentPage` has exactly **one** `Content`; to show more than one control, that single child has to be a *layout* that holds the rest.

## The layouts you'll actually use

### Stack layouts - children in a line

`VerticalStackLayout` arranges its children top to bottom; `HorizontalStackLayout` does it left to right - the simplest containers, great for short, linear groups.

```xml
<VerticalStackLayout Spacing="12" Padding="20">
    <Label Text="Quick note" FontSize="20" />
    <Entry Placeholder="Type here..." />
    <Button Text="Save" />
</VerticalStackLayout>
```

*What just happened:* Three controls stacked vertically with a 12-unit gap between each (`Spacing`) and 20 units of breathing room around the whole group (`Padding`). `Padding` is space *inside* the container's edge; `Spacing` is the gap *between* children. The page has one root child (the stack), and the stack holds the three controls - the mental model in action.

### Grid - rows and columns, the real workhorse

Stacks are fine for a line of controls, but real screens have structure: a header pinned at the top, a body that fills the rest, a footer that hugs the bottom. That's a `Grid` - it defines `RowDefinitions` and `ColumnDefinitions`, and each child says which cell it lives in with `Grid.Row` and `Grid.Column` (both default to `0`).

```xml
<Grid RowDefinitions="Auto,*,Auto"
      ColumnDefinitions="*"
      Padding="20"
      RowSpacing="12">

    <Label Grid.Row="0"
           Text="My Notes"
           FontSize="28" />

    <Label Grid.Row="1"
           Text="(your notes will appear here)"
           VerticalOptions="Start" />

    <Button Grid.Row="2"
            Text="Add note" />

</Grid>
```

*What just happened:* `RowDefinitions="Auto,*,Auto"` makes three rows. **`Auto`** sizes a row to exactly fit its content - the title and the button take only the height they need. **`*`** means "take all the leftover space" - the middle row stretches to fill whatever's left, exactly what you want for a scrolling list of notes. Header on top, body fills the gap, action button parked at the bottom.

> 💡 The sizing tokens are the whole point of `Grid`. **`Auto`** = size to content. **`*`** = take the remaining space (`2*` takes twice the share of a plain `*`, so `*,2*` splits leftover space one-third / two-thirds). Because `*` rows and columns flex with the screen, the *same* layout looks right on a narrow phone and a wide desktop.

### ScrollView - when content overflows

A `ContentView` or stack won't scroll on its own - if its content is taller than the screen, the overflow is clipped. Wrap it in a `ScrollView` and it scrolls.

```xml
<ScrollView>
    <VerticalStackLayout Spacing="16" Padding="20">
        <Label Text="A very long note..." />
        <!-- ...many more controls... -->
    </VerticalStackLayout>
</ScrollView>
```

*What just happened:* `ScrollView` takes one child (here the stack) and gives it a scrollable viewport - anything taller than the screen now scrolls instead of getting cut off. (`FlexLayout` and `AbsoluteLayout` also exist for wrap-and-flow and pixel-precise positioning, but you'll reach for them far less often - start with stacks, `Grid`, and `ScrollView`.)

## Common controls and their key properties

Layouts arrange things; **controls** are the things. The handful you'll use constantly:

- **`Label`** - displays text.
- **`Button`** - a tappable button with a `Clicked` event.
- **`Entry`** - single-line text input.
- **`Editor`** - multi-line text input (think note body).
- **`Image`** - shows an image from a `Source`.

Properties are written as XAML attributes. The ones you'll set most often:

```xml
<VerticalStackLayout Spacing="10" Padding="20">

    <Label Text="Note title"
           FontSize="22"
           TextColor="DarkSlateBlue"
           HorizontalOptions="Center" />

    <Entry Placeholder="Title" />

    <Editor Placeholder="Write your note..."
            HeightRequest="120" />

    <Button Text="Save"
            Margin="0,16,0,0"
            HorizontalOptions="End" />

</VerticalStackLayout>
```

*What just happened:* `FontSize` and `TextColor` style the label. `HorizontalOptions` (and its sibling `VerticalOptions`) control how a child sits in the space its parent gives it - values are `Start`, `Center`, `End`, and `Fill`; here the title is centered and the button pushed right (`End`). `Margin="0,16,0,0"` adds space *outside* the button (left, top, right, bottom order), nudging it down from the editor. `Placeholder` is the grey hint text shown in an empty `Entry`/`Editor`.

> ⚠️ Margin vs. Padding catches everyone once: **Padding** is space *inside* a container, before its children start. **Margin** is space *outside* a control, pushing its neighbors away. Same four-number order - `left,top,right,bottom` - but opposite sides of the control's edge.

## One real warning: don't nest stacks endlessly

When stacks are the only tool you know, it's tempting to build a whole screen by burying stacks inside stacks inside stacks to push things around. Resist it.

⚠️ **Deeply nested stack layouts hurt performance and are miserable to maintain.** Each stack measures and arranges its children, and nesting multiplies that work on every layout pass - on a scrolling list, it shows up as jank. Worse, six levels deep, *nobody* can tell which container owns which spacing. A `Grid` with a few `RowDefinitions`/`ColumnDefinitions` does the same job flat. **For anything beyond a short line of controls, reach for `Grid` first.**

## Building the notes app's main page

Let's put it together into the real screen for our running **notes** app: a title at the top, a list area in the middle (a placeholder - the real list comes in [Phase 3](03-controls-and-data-binding.md)), and an Add button at the bottom.

```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NotesApp.MainPage"
             Title="Notes">

    <Grid RowDefinitions="Auto,*,Auto"
          Padding="20"
          RowSpacing="16">

        <!-- Header -->
        <Label Grid.Row="0"
               Text="My Notes"
               FontSize="32"
               FontAttributes="Bold"
               TextColor="DarkSlateBlue" />

        <!-- List area (placeholder until Phase 3) -->
        <Label Grid.Row="1"
               Text="You have no notes yet."
               FontSize="16"
               TextColor="Gray"
               HorizontalOptions="Center"
               VerticalOptions="Start" />

        <!-- Action -->
        <Button Grid.Row="2"
                Text="+ Add note"
                HorizontalOptions="Fill" />

    </Grid>

</ContentPage>
```

*What just happened:* One `ContentPage`, one root `Grid`, three children - exactly the mental model. The `Auto,*,Auto` rows give a fixed-height header, a flexible body that fills the screen (room to grow and scroll once real notes arrive), and a fixed-height button at the bottom. The body label is a placeholder; Phase 3 swaps it for a `CollectionView` bound to actual data. `HorizontalOptions="Fill"` on the button stretches it the full width. Run this and you've got the skeleton of the whole app - built with one flat `Grid` instead of a tangle of stacks.

## Recap

- A page is **one root layout holding controls** - `ContentPage` has exactly one `Content`, and to show several controls that one child must be a layout.
- **XAML is C# objects.** Each element constructs an object and each attribute sets a property; `x:Class` ties the file to a C# partial class.
- **Stack layouts** line children up (`Spacing` between, `Padding` around); **`Grid`** places children in rows/columns using `Grid.Row`/`Grid.Column`, with `Auto` = size-to-content and `*` = take remaining space; **`ScrollView`** lets overflow scroll.
- Common controls - `Label`, `Button`, `Entry`, `Editor`, `Image` - are styled with attributes like `FontSize`, `TextColor`, `HorizontalOptions`, `Margin`, and `Padding`.
- Prefer **`Grid` over deeply nested stacks** for any non-trivial screen - it's faster and far more maintainable - and lean on `*` sizing so one layout works across phone, tablet, and desktop.

## Quick check

```quiz
[
  {
    "q": "In a Grid with RowDefinitions=\"Auto,*,Auto\", what does the middle \"*\" row do?",
    "choices": ["Sizes itself to exactly fit its content", "Takes up all the leftover space after the Auto rows", "Is hidden until content is added", "Forces a fixed height of 1 unit"],
    "answer": 1,
    "explain": "\"*\" means take the remaining space. Combined with Auto rows above and below, it gives a flexible body between a fixed header and footer."
  },
  {
    "q": "How many direct children can a ContentPage's Content hold?",
    "choices": ["As many as you like", "Exactly one (usually a layout)", "Up to ten", "Only controls, never layouts"],
    "answer": 1,
    "explain": "A ContentPage holds one Content. To show multiple controls, that single child must be a layout that holds the rest."
  },
  {
    "q": "Why prefer a Grid over deeply nested stack layouts for a non-trivial screen?",
    "choices": ["Grids can't be styled, so they're simpler", "Nested stacks hurt performance and maintainability; a flat Grid does the same job", "Stacks don't work on iOS", "Grids are the only layout that supports Padding"],
    "answer": 1,
    "explain": "Each nested stack adds measure/arrange work and obscures which container owns what. A Grid places everything flat by row and column - faster and clearer."
  }
]
```


---

# Controls & Data Binding

In Phase 2 you arranged controls by hand, setting a `Label`'s text yourself in code. That works for one label, but falls apart once your UI has a dozen fields reflecting a changing object - you'd write `titleLabel.Text = note.Title;` over and over, re-running it every time the data changes. Data binding is the escape hatch.

## The mental model: tie the control to the data, then walk away

Here's the one idea to hold for this whole phase: **data binding connects a control's property to a property on a data object.** You declare the connection once - "this `Label`'s `Text` should mirror this note's `Title`" - and MAUI keeps them in sync. You stop poking the UI by hand.

Two pieces make it work:

- **`BindingContext`** - the data object a control (or a whole page) is pointed at, "the thing this UI is about." A notes detail page is *about* one `Note`, so that `Note` is its `BindingContext`.
- **`{Binding PropertyName}`** - in XAML, "fill me from the property called `PropertyName`, looked up on the `BindingContext`."

The page gets pointed at a `Note`, and each control reaches into that note for the property it cares about. You set the source once; the UI follows.

> 📝 If you've used the web, this is the same instinct as templating - but two-way and live, not a one-time render. The control doesn't *copy* the value; it *subscribes* to it.

## Your first binding

Bind a `Label` to a note's title. First the data object - a plain C# class for now:

```csharp
public class Note
{
    public string Title { get; set; }
    public string Body { get; set; }
}
```

*What just happened:* a class with two properties - the kind of object a binding reads from. (It has one limitation, coming up.)

Now the page. Set the `BindingContext` in the code-behind, then bind in XAML:

```csharp
public partial class NoteDetailPage : ContentPage
{
    public NoteDetailPage()
    {
        InitializeComponent();
        BindingContext = new Note { Title = "Buy milk", Body = "2%, not skim" };
    }
}
```

```xml
<ContentPage ...>
    <VerticalStackLayout Padding="20" Spacing="10">
        <Label Text="{Binding Title}" FontSize="24" />
        <Label Text="{Binding Body}" />
    </VerticalStackLayout>
</ContentPage>
```

*What just happened:* the page's `BindingContext` is one `Note`. The first `Label` looks up `Title` and shows "Buy milk"; the second shows the body - no `titleLabel.Text = ...` anywhere. Controls inherit the page's `BindingContext`, so set it once and every child can bind against it.

## Binding modes: which way does data flow?

A binding has a *direction*. The mode decides who tells whom about changes.

- **OneWay** - source → UI. The data drives the control; default for display controls like `Label`, and usually what you want for read-only text.
- **TwoWay** - source ↔ UI. Changes flow both directions - for *inputs*, where you want the user's typing written back into the data object. `Entry.Text` and `Switch.IsToggled` default to TwoWay.
- **OneTime** - set once at bind time, then never again. Rare; useful for values you know won't change.

Here's a TwoWay binding on an editable title:

```xml
<Entry Text="{Binding Title, Mode=TwoWay}" Placeholder="Note title" />
```

*What just happened:* the `Entry` shows the note's current `Title`, and edits write straight back to `note.Title` - no `TextChanged` handler, no manual assignment. That's the payoff of TwoWay.

> 💡 You often don't need to write `Mode=TwoWay` on an `Entry` - its `Text` is TwoWay by default. Spell it out when you want to be explicit, or when you're binding a property whose default mode isn't what you want.

## The catch: live updates need change notifications

Now the part that trips up everyone the first time. Bind a `Label` to `note.Title`, then later run `note.Title = "Buy oat milk";` in code - and the label **doesn't change.** The binding read the value once and has no idea the property moved. A plain class like our `Note` has no way to announce "hey, `Title` changed" - the binding wired itself up, but nobody rang the bell.

The fix is an interface called **`INotifyPropertyChanged`**: your data object raises an event every time a property changes, and the binding listens for it. With that in place, set `note.Title` in code and the label updates instantly.

> ⚠️ A plain object (a "POCO") binds *once* - it'll show the initial value fine - but it won't *live-update* when properties change afterward. If your UI mysteriously goes stale after you change data in code, this is almost always why.

We're not implementing `INotifyPropertyChanged` by hand here - it's the whole reason Phase 4 exists, and CommunityToolkit.Mvvm makes it nearly free. For now, hold the rule: **bindings show the current value at bind time, but only live-update if the source raises change notifications.** See [Phase 4: The MVVM Pattern](04-mvvm.md).

## Lists: CollectionView, ItemsSource, and ItemTemplate

A notes app needs to show *many* notes, not one. That's `CollectionView` - MAUI's workhorse for scrolling lists. It has two key bindings:

- **`ItemsSource`** - bound to a *collection* of objects (your list of notes).
- **`ItemTemplate`** - a `DataTemplate` describing how to render *one* row.

```xml
<CollectionView ItemsSource="{Binding Notes}">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Label Text="{Binding Title}" Padding="10" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>
```

*What just happened:* `ItemsSource` points at the page's `Notes` collection, so the `CollectionView` knows it has, say, five notes, and stamps out a copy of the `DataTemplate` for each. Crucially, **inside the `DataTemplate`, `{Binding Title}` is relative to each item - a single `Note` - not the page.** The page's `BindingContext` is the screen as a whole; each row's `BindingContext` is automatically the note for that row.

So you have two layers of binding context at once: the page is about the *list*, each row is about *one note*. MAUI sets the row context for you.

> 💡 Back the list with an **`ObservableCollection<T>`**, not a plain `List<T>`. It tells the `CollectionView` whenever you add or remove an item, so the list redraws automatically - a plain `List` has no such signal. Same problem as `INotifyPropertyChanged`, one level up: the *collection* needs to announce changes too.

## Wiring the notes app

The list page is *about* a collection of notes; the detail page is *about* one note with an editable title.

The list page's context holds an `ObservableCollection<Note>`:

```csharp
public partial class NotesListPage : ContentPage
{
    public ObservableCollection<Note> Notes { get; } = new()
    {
        new Note { Title = "Buy milk", Body = "2%, not skim" },
        new Note { Title = "Call dentist", Body = "Reschedule cleaning" },
    };

    public NotesListPage()
    {
        InitializeComponent();
        BindingContext = this;
    }
}
```

*What just happened:* the page exposes a `Notes` collection and sets itself as the `BindingContext` (`BindingContext = this`), so `{Binding Notes}` resolves to that property. `ObservableCollection` means a later `Notes.Add(...)` shows up without a manual refresh.

The list XAML binds the `CollectionView` to it:

```xml
<ContentPage ...>
    <CollectionView ItemsSource="{Binding Notes}">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <VerticalStackLayout Padding="15" Spacing="2">
                    <Label Text="{Binding Title}" FontSize="18" />
                    <Label Text="{Binding Body}" FontSize="13" TextColor="Gray" />
                </VerticalStackLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentPage>
```

*What just happened:* each note becomes a two-line row - title on top, body in gray underneath. The bindings inside the template read from each individual `Note`, because that's the row's context. One template, every note rendered consistently.

And the detail page edits one note's title with a TwoWay `Entry`:

```xml
<VerticalStackLayout Padding="20" Spacing="10">
    <Entry Text="{Binding Title, Mode=TwoWay}" Placeholder="Title" FontSize="22" />
    <Editor Text="{Binding Body, Mode=TwoWay}" Placeholder="Write your note..." HeightRequest="200" />
</VerticalStackLayout>
```

*What just happened:* point this page's `BindingContext` at one `Note` and the `Entry`/`Editor` both show and *write back* its title and body as the user types - the "set it once, walk away" promise from the top of the phase.

You now have a list that renders itself and an edit screen that reads and writes a note - all declared in XAML, no manual UI-poking. One gap left: making the UI live-update when data changes in code, the heart of the next phase.

## Recap

- **Data binding ties a control's property to a data object's property.** Set the source once via `BindingContext`; the UI follows - no manual `label.Text = ...`.
- **`{Binding PropertyName}`** looks the property up on the current `BindingContext`. Children inherit the page's context unless overridden.
- **Modes:** OneWay (source → UI, default for display) and TwoWay (source ↔ UI, default for inputs like `Entry`). OneTime sets once.
- **Live updates need change notifications** - a plain object binds once but won't refresh when its properties change later. `INotifyPropertyChanged` (Phase 4) fixes that.
- **Lists** use `CollectionView` + `ItemsSource` + an `ItemTemplate`/`DataTemplate`; bindings inside the template are relative to *each item*. Back the source with an `ObservableCollection` so adds/removes show up automatically.

## Quick check

```quiz
[
  {
    "q": "A Label is bound to note.Title. In code you run note.Title = \"new\", but the Label doesn't change. Why?",
    "choices": ["The binding only supports TwoWay mode", "A plain object doesn't raise change notifications, so the binding never hears about the update", "Labels can't be bound to strings", "BindingContext must be re-assigned on every change"],
    "answer": 1,
    "explain": "Bindings show the current value at bind time, but live updates require the source to raise change notifications via INotifyPropertyChanged (Phase 4)."
  },
  {
    "q": "Inside a CollectionView's DataTemplate, what is {Binding Title} relative to?",
    "choices": ["The page's BindingContext", "The CollectionView itself", "Each individual item (e.g. one Note) being rendered", "The app's global resources"],
    "answer": 2,
    "explain": "Each row's BindingContext is the item for that row, so bindings in the template read from a single item, not the page."
  },
  {
    "q": "You want an Entry whose text both displays and updates a note's Title as the user types. Which mode fits?",
    "choices": ["OneTime", "OneWay", "TwoWay", "No binding needed"],
    "answer": 2,
    "explain": "TwoWay flows both directions: the Entry shows the current value and writes edits back to the source. It's the default for Entry.Text."
  }
]
```


---

# The MVVM Pattern

In [Phase 3](03-controls-and-data-binding.md) you wired a control to a property with `{Binding}` and hit the wall everyone hits: you changed the property in code, and the screen didn't budge. The binding fired *once*, at startup, then went quiet. This phase is the fix - and the moment your notes app stops being a toy and starts being structured like a real app.

## The mental model: three roles, one rule

MVVM stands for **Model–View–ViewModel**. It sounds like architecture-astronaut jargon, but it's three plain ideas:

- **View** - your XAML page. It describes what's on screen. Nothing more.
- **ViewModel** - a plain C# class that holds the page's **state** (the data it shows) and its **commands** (what the buttons do). It knows nothing about XAML, buttons, or labels.
- **Model** - your actual data. For the notes app, that's a `Note` class with a `Title`.

The View binds to the ViewModel. You set the ViewModel as the page's `BindingContext`, and every `{Binding Title}` in the XAML reads from a `Title` property on the ViewModel. The arrows only point one way: **the View asks the ViewModel for data; the ViewModel never reaches up and touches the View.**

> 💡 The whole payoff is in that last sentence. Because the ViewModel has **no UI dependency**, you can create one in a unit test, call its `AddNote` command, and assert that the notes collection grew - no screen, no emulator, no tapping. A ViewModel you can test without a UI is the entire point of MVVM.

```mermaid
flowchart LR
  V["View (XAML page)"] -- "BindingContext" --> VM["ViewModel (state + commands)"]
  VM -- "holds" --> M["Model (Note data)"]
  VM -. "raises PropertyChanged" .-> V
```

Hold "View shows, ViewModel decides, Model is the data" and everything below is detail.

## Why the screen didn't update: INotifyPropertyChanged

A binding isn't magic. When you write `{Binding Title}`, MAUI reads `Title` once and shows it. For MAUI to *re-read* it when the value changes, the object holding `Title` has to **announce the change** - the contract for that is an interface called `INotifyPropertyChanged`: one event, `PropertyChanged`, raised whenever a bound property's value changes.

Here's that contract done by hand, for a single `Title` property:

```csharp
using System.ComponentModel;

public class NoteViewModel : INotifyPropertyChanged
{
    private string _title = "";
    public string Title
    {
        get => _title;
        set
        {
            _title = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}
```

*What just happened:* the setter doesn't only store the new value - it fires `PropertyChanged`, naming `Title` as the thing that changed. Any `{Binding Title}` in the XAML hears its name called and re-reads the property. That's the missing piece from Phase 3 - the binding wasn't broken, the object just never told anyone its value had moved.

That's a lot of ceremony for **one** property. A notes page with a title, a body, a "saving…" flag, and a selected note means writing that pattern four times - and one typo in a `nameof` gives a silent bug where the screen won't refresh for that field. Nobody writes it by hand anymore.

## The modern way: CommunityToolkit.Mvvm

`CommunityToolkit.Mvvm` is a small, official NuGet package that generates all that boilerplate at compile time using **source generators**. You add the package, inherit from `ObservableObject`, and annotate fields - the generator writes the properties and the `PropertyChanged` plumbing behind the scenes.

📝 You add it once per project: `dotnet add package CommunityToolkit.Mvvm`. The class must be `partial` so the generator can add the generated half.

Here's the real `NotesViewModel` for our app:

```csharp
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class NotesViewModel : ObservableObject
{
    [ObservableProperty]
    private string newTitle = "";

    public ObservableCollection<Note> Notes { get; } = new();

    [RelayCommand]
    private void AddNote()
    {
        Notes.Add(new Note { Title = NewTitle });
        NewTitle = "";
    }
}
```

*What just happened:* three pieces of magic, none of which you had to write:

- `[ObservableProperty]` on the field `newTitle` generates a public property `NewTitle` (capital N) - *with* the getter, setter, and `PropertyChanged` notification from the hand-written version above. One line in; the generator writes fifteen.
- `ObservableCollection<Note>` is a list that *itself* raises change notifications. `Add` to it and any bound list on screen updates automatically. (A plain `List<Note>` would not.)
- `[RelayCommand]` on the method `AddNote` generates a public property `AddNoteCommand` of type `ICommand` - the thing your button binds to.

`ObservableObject` *is* the `INotifyPropertyChanged` implementation, and `[ObservableProperty]` *is* the verbose setter - both handed to you for free.

## Commands: buttons that talk to the ViewModel

In the old code-behind world, a button did its work through a `Clicked` event handler living next to the XAML:

```xml
<!-- The old way: logic lives in code-behind -->
<Button Text="Add" Clicked="OnAddClicked" />
```

That handler sits in the page's `.xaml.cs` file, tangling logic up with the View - exactly what MVVM separates. The MVVM way replaces the event with a **command binding**:

```xml
<VerticalStackLayout Padding="20" Spacing="10">
    <Entry Placeholder="New note title"
           Text="{Binding NewTitle}" />

    <Button Text="Add"
            Command="{Binding AddNoteCommand}" />

    <CollectionView ItemsSource="{Binding Notes}">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Label Text="{Binding Title}" />
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</VerticalStackLayout>
```

*What just happened:* the `Entry`'s text is two-way bound to `NewTitle`, so as the user types, the ViewModel's `NewTitle` updates live. The `Button`'s `Command` is bound to `AddNoteCommand` - generated from your `AddNote` method. Tap the button, the command runs `AddNote`, which adds a `Note` and clears `NewTitle`. The `CollectionView` shows a new row instantly and the `Entry` clears itself - no code-behind handler touched. The View describes; the ViewModel decides.

To connect the two, you set the page's `BindingContext` to the ViewModel - typically in the page's constructor:

```csharp
public partial class NotesPage : ContentPage
{
    public NotesPage()
    {
        InitializeComponent();
        BindingContext = new NotesViewModel();
    }
}
```

*What just happened:* every unqualified `{Binding ...}` on the page now resolves against this `NotesViewModel` instance - that one line is the wire between View and ViewModel. (In a larger app you'd inject the ViewModel through MAUI's dependency injection rather than `new` it here, but the idea is identical.)

## The one discipline that keeps it testable

⚠️ The most common MVVM mistake: the moment something is mildly inconvenient, you reach for a UI call inside the ViewModel. "I'll just pop a `DisplayAlert` to confirm the delete." Don't.

`DisplayAlert`, navigation, and other UI calls live on the *page*, not the ViewModel. The second your ViewModel calls `DisplayAlert`, it can no longer be constructed in a unit test - it depends on a live page - and you've thrown away the entire benefit. Same goes for stuffing logic back into code-behind: decisions living in `.xaml.cs` can't be tested either.

The rule of thumb:

- **State and decisions** → ViewModel. ("What notes exist? Is the title empty? Add this note.")
- **Showing a dialog, navigating, animating** → these are UI concerns. Keep them on the page, or hand the ViewModel a small injected *service* (like `IAlertService`) that you can swap for a fake in tests.

When you find yourself wanting UI in the ViewModel, that's the signal to extract a service - not to break the boundary. Guard it and your ViewModels stay the cheap-to-test core of your app.

## Recap

- **MVVM** splits a screen into three roles: the **View** (XAML, shows things), the **ViewModel** (plain C# holding state and commands), and the **Model** (your data). The View binds to the ViewModel via `BindingContext`.
- A binding only refreshes when the object **announces** changes through `INotifyPropertyChanged` - raising `PropertyChanged` in setters. That's why the Phase 3 screen wouldn't update.
- Doing that by hand is verbose and typo-prone. **CommunityToolkit.Mvvm** generates it: inherit `ObservableObject`, mark fields `[ObservableProperty]`, mark methods `[RelayCommand]`.
- Bind a button's `Command` to a generated command (`AddNoteCommand`) instead of writing a code-behind `Clicked` handler. Use `ObservableCollection<T>` so list changes show up automatically.
- Keep UI concerns (`DisplayAlert`, navigation) out of the ViewModel - that no-UI-dependency is what makes the ViewModel testable, and testability is the whole point.

## Quick check

```quiz
[
  {
    "q": "Your bound Label still shows the old value after you change the property in code. What's the most likely cause?",
    "choices": ["The XAML is malformed", "The property's setter doesn't raise PropertyChanged (the class doesn't implement INotifyPropertyChanged)", "You forgot to call InitializeComponent", "Bindings only work with strings"],
    "answer": 1,
    "explain": "A binding re-reads a property only when the object announces the change via PropertyChanged. Without INotifyPropertyChanged, the UI never hears about updates."
  },
  {
    "q": "What does [RelayCommand] on a method named AddNote generate?",
    "choices": ["A backing field named addNote", "A public ICommand property named AddNoteCommand that a Button can bind to", "An event handler in the code-behind", "Nothing - it's just documentation"],
    "answer": 1,
    "explain": "[RelayCommand] generates an ICommand property (method name + \"Command\"), so you bind Command=\"{Binding AddNoteCommand}\" instead of writing a Clicked handler."
  },
  {
    "q": "Why should a ViewModel avoid calling DisplayAlert directly?",
    "choices": ["DisplayAlert is deprecated in MAUI", "It makes the app slower", "It ties the ViewModel to a live UI, so it can no longer be unit-tested without a page", "Alerts can only be shown from XAML"],
    "answer": 2,
    "explain": "The point of MVVM is a ViewModel with no UI dependency. A direct DisplayAlert call requires a live page, which breaks testability. Inject a service instead."
  }
]
```


---

# Navigation with Shell

In [Phase 4](04-mvvm.md) your notes app got real structure: a `NotesViewModel` holding a list, a command to add a note, and a `CollectionView` showing the rows. But it's stuck on one page - tap a note and nothing happens. A real notes app needs a detail page, and a way to *get there* and *get back*. That's navigation, and in modern MAUI the answer is **Shell**.

## The mental model: a map and an address bar

Here's the one idea to hold. Think of your app like a small website.

- **Shell is the site map.** In one file - `AppShell.xaml` - you declare the app's whole skeleton: which screens are top-level (the bottom tabs, the flyout menu) and what page each one shows.
- **Routes are addresses.** Every screen has a short name, like `notes` or `notedetail`. To move, you don't construct a page and shove it onto a stack - you navigate to an *address* with `Shell.Current.GoToAsync("notedetail")`, the way a browser goes to a URL. Going back is the address `".."`, exactly like `cd ..` in a terminal.

So: **Shell declares the structure once; you move around it with URI-style routes.** Hold that and the rest is filling in names.

```mermaid
flowchart LR
  Shell["AppShell (the map)"] --> Notes["notes (tab)"]
  Shell --> About["about (tab)"]
  Notes -- "GoToAsync(notedetail?id=3)" --> Detail["notedetail (registered route)"]
  Detail -- "GoToAsync('..')" --> Notes
```

> 📝 There's an older model you'll see in tutorials and legacy apps: `NavigationPage` with `Navigation.PushAsync(new DetailPage())` - a literal stack of pages you push and pop. It still works and MAUI still supports it, but Shell is the modern, recommended approach - flyout and tabs are built in, and routes scale better than juggling a stack by hand. We'll build with Shell.

## AppShell.xaml: the map

The MAUI template already gives you an `AppShell.xaml`. Here's a focused version for our notes app - two tabs at the bottom, one for the notes list and one for an about page:

```xml
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:local="clr-namespace:NotesApp"
       x:Class="NotesApp.AppShell">

    <TabBar>
        <ShellContent Title="Notes"
                      ContentTemplate="{DataTemplate local:NotesPage}"
                      Route="notes" />
        <ShellContent Title="About"
                      ContentTemplate="{DataTemplate local:AboutPage}"
                      Route="about" />
    </TabBar>

</Shell>
```

*What just happened:* `<Shell>` is the root of your app's navigation. `<TabBar>` says "give me a bottom tab bar," and each `<ShellContent>` is one tab. `Title` is the label the user sees; `ContentTemplate="{DataTemplate local:NotesPage}"` tells Shell which page to render (wrapped in `DataTemplate` so it's built lazily, only when first shown); `Route="notes"` is that screen's address. Swap `<TabBar>` for `<FlyoutItem>` entries and you'd get a slide-out hamburger menu instead - same `ShellContent` children, different chrome.

## Moving between screens: GoToAsync

Top-level tabs are reachable just by tapping them, but our detail page isn't a tab - it's a screen you reach by tapping a note. To go there in code, you ask Shell to navigate to its route:

```csharp
// Go forward to the detail screen
await Shell.Current.GoToAsync("notedetail");

// Go back to the previous screen (like the back button)
await Shell.Current.GoToAsync("..");
```

*What just happened:* `Shell.Current` is the running Shell instance - the app's one navigation map. `GoToAsync("notedetail")` looks up the route and pushes that page. `".."` means "up one" - it pops back, same as the back button. It's `async` because navigation can run page-creation and transition animations.

But there's a catch everybody trips over. The `notes` and `about` routes work because they're declared in `AppShell.xaml`. `notedetail` is **not** in the Shell tree - it's a page you only visit on demand, and Shell doesn't know that name yet.

> ⚠️ Navigate to a route Shell has never heard of and `GoToAsync` throws **"route not found"** at runtime. Tabs and flyout items register themselves; any *other* page you navigate to must be registered by hand first. This is the single most common Shell error.

You register detail routes once, typically in your `AppShell` constructor:

```csharp
public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        Routing.RegisterRoute("notedetail", typeof(NoteDetailPage));
    }
}
```

*What just happened:* `Routing.RegisterRoute` tells Shell, "when someone navigates to `notedetail`, build a `NoteDetailPage`." Now `GoToAsync("notedetail")` resolves instead of throwing. Rule of thumb: if a page is a tab or flyout item, it's already registered; anything else (detail, edit, settings reached from a button) needs registering once at startup.

## Passing parameters: which note?

Navigating to `notedetail` opens *a* detail page, but our app has many notes - we need to tell the detail page *which* note. Just like a web URL carries a query string (`?id=3`), Shell routes do too.

On the sending side, you append the data to the route. Imagine each `Note` now has an `Id`, and tapping a row runs this command in the ViewModel:

```csharp
[RelayCommand]
private async Task GoToDetail(Note note)
{
    if (note is null) return;
    await Shell.Current.GoToAsync($"notedetail?id={note.Id}");
}
```

*What just happened:* `$"notedetail?id={note.Id}"` builds an address like `notedetail?id=3` - the route name, then a `?id=` query parameter carrying that note's id. (A `CollectionView` can fire a command on tap via `SelectionChanged` or a tappable item template; the point here is the navigation call, not the gesture.) The detail page now needs to *receive* that `id`.

On the receiving side, you decorate the page (or its ViewModel) with `[QueryProperty]`, which maps a query key to a property:

```csharp
using CommunityToolkit.Mvvm.ComponentModel;

[QueryProperty(nameof(NoteId), "id")]
public partial class NoteDetailViewModel : ObservableObject
{
    [ObservableProperty]
    private string noteId = "";

    partial void OnNoteIdChanged(string value)
    {
        // value is "3" - now load that note from your store
        LoadNote(value);
    }
}
```

*What just happened:* `[QueryProperty(nameof(NoteId), "id")]` wires the query key `"id"` to the `NoteId` property. When Shell navigates, it sets `NoteId = "3"` for you. Because `NoteId` is an `[ObservableProperty]` (Phase 4's CommunityToolkit.Mvvm), the generator gives us an `OnNoteIdChanged` hook that fires the moment the value lands - a clean place to load that note. Query values arrive as **strings**, so you'll often `int.Parse` the id before looking it up.

There's also a richer way when you need to pass a whole object instead of a flat id - a dictionary plus the `IQueryAttributable` interface:

```csharp
// Sending: pass the actual Note object, not just an id
await Shell.Current.GoToAsync("notedetail", new Dictionary<string, object>
{
    ["Note"] = note
});
```

```csharp
// Receiving: implement IQueryAttributable to pull it out
public partial class NoteDetailViewModel : ObservableObject, IQueryAttributable
{
    [ObservableProperty]
    private Note? note;

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        Note = query["Note"] as Note;
    }
}
```

*What just happened:* the dictionary lets you hand over real objects (here, the whole `Note`) instead of cramming everything into a string. `ApplyQueryAttributes` is called by Shell with that dictionary right after navigation, and you fish out your value by key. Use the query-string `?id=` style for simple values like an id (link-friendly, survives app restarts); reach for the dictionary + `IQueryAttributable` when you need to pass an object you already have in hand.

💡 Both styles solve the same problem - "tell the next screen what to show." Pass the id when you have an id, pass the object when you have the object.

## The whole flow, end to end

Navigation now reads as one clean story: the list page shows `Notes`; tapping a row runs `GoToDetail(note)`, which navigates to `notedetail?id={note.Id}`; Shell builds `NoteDetailPage`, sets `NoteId` via `[QueryProperty]`, and the detail ViewModel loads and shows that note. Edit it, then `GoToAsync("..")` takes the user back to the list. One map, a handful of addresses, and a query string carrying the id - the entire navigation layer of a real app.

## Recap

- **Shell** (`AppShell.xaml`) declares your app's structure in one place: `<TabBar>`/`<FlyoutItem>` with `<ShellContent>` entries, each pointing at a page via `ContentTemplate` and naming an address with `Route`.
- **Navigate with routes**: `Shell.Current.GoToAsync("notedetail")` goes forward; `GoToAsync("..")` goes back. It's `async` - `await` it.
- **Register non-tab pages**: detail/edit pages that aren't in the Shell tree need `Routing.RegisterRoute("notedetail", typeof(NoteDetailPage))`, or `GoToAsync` throws "route not found".
- **Pass parameters** two ways: a query string `?id={note.Id}` received with `[QueryProperty(nameof(NoteId), "id")]` (values arrive as strings), or a dictionary received with `IQueryAttributable` when you need to hand over a whole object.
- The **older** `NavigationPage` + `PushAsync`/`PopAsync` stack model still exists; Shell is the modern, recommended approach with tabs, flyout, and URI routes built in.

## Quick check

```quiz
[
  {
    "q": "You call Shell.Current.GoToAsync(\"notedetail\") and get a runtime \"route not found\" error. What's the fix?",
    "choices": ["Add ?id= to the route string", "Register the route with Routing.RegisterRoute(\"notedetail\", typeof(NoteDetailPage))", "Make GoToAsync synchronous", "Move NoteDetailPage into a TabBar"],
    "answer": 1,
    "explain": "Pages that aren't ShellContent tabs/flyout items must be registered with Routing.RegisterRoute (usually in the AppShell constructor) before GoToAsync can resolve their route."
  },
  {
    "q": "How do you navigate back to the previous screen with Shell?",
    "choices": ["GoToAsync(\"back\")", "GoToAsync(\"..\")", "PopAsync()", "GoToAsync(\"/\")"],
    "answer": 1,
    "explain": "The route \"..\" means \"up one\" - it pops back to the previous screen, like a browser back button or cd .. in a terminal."
  },
  {
    "q": "You navigate with GoToAsync($\"notedetail?id={note.Id}\"). How does the detail ViewModel receive that id?",
    "choices": ["It reads Shell.Current.QueryString manually", "With [QueryProperty(nameof(NoteId), \"id\")] mapping the \"id\" key to a NoteId property", "The id is passed to the page constructor automatically", "It can't - query strings only work with web apps"],
    "answer": 1,
    "explain": "[QueryProperty(nameof(NoteId), \"id\")] maps the query key \"id\" to the NoteId property; Shell sets it during navigation. Values arrive as strings, so parse the id before lookup."
  }
]
```


---

# Data & Calling APIs

Up to now the notes app has lived entirely in memory - add a note and it's gone the moment you close the app. Fine for learning bindings and commands, but no real app behaves that way. This phase gives it a memory that survives a restart, and a way to reach the wider world.

## The mental model: two directions data flows

There are only two places your app's data can live, and an app worth shipping uses both:

- **Remote** - on a server somewhere, reached over the network with `HttpClient`. This is how your app talks to a backend (the kind you'd build in [ASP.NET Core](/guides/aspnet-core-from-zero)), syncs across a user's devices, or pulls down shared data.
- **Local** - on the device itself. This keeps the app feeling instant and working when the user walks into an elevator and loses signal.

```mermaid
flowchart LR
  VM["ViewModel"] -- "HttpClient (JSON)" --> API["Backend API"]
  VM -- "save / load" --> LOCAL["Local storage<br/>(Preferences · SecureStorage · files · SQLite)"]
```

The whole phase is learning *which tool* for each direction - and for local, there are four, sized from a single setting up to a full database. Hold "reach out with `HttpClient`, persist with the smallest local store that fits the data," and the rest is detail.

## Calling an API with HttpClient

C# has talked to HTTP APIs the same way for years: `HttpClient`. MAUI adds nothing new here - the skill transfers straight from ASP.NET Core, Blazor, or any console app you've written. The package worth knowing is `System.Net.Http.Json`, which turns a JSON response into your C# types in a single call.

Say the backend exposes notes at `https://api.example.com/notes`:

```csharp
using System.Net.Http.Json;

var http = new HttpClient();
List<Note>? notes = await http.GetFromJsonAsync<List<Note>>(
    "https://api.example.com/notes");

// Sending one back:
await http.PostAsJsonAsync("https://api.example.com/notes", newNote);
```

*What just happened:* `GetFromJsonAsync<List<Note>>` did three things in one line - made the GET request, read the response body, and deserialized the JSON into a `List<Note>` matching your model. `PostAsJsonAsync` runs the same play in reverse. No manual `JsonSerializer`, no reading streams by hand.

📝 Property names on your `Note` class need to line up with the JSON field names (case-insensitive by default). If the API sends `"title"` and your class has `Title`, they match.

### Don't `new` it in the ViewModel - wrap it in a service

That raw snippet works, but dropping `new HttpClient()` and a hard-coded URL inside a ViewModel is the same boundary-breaking Phase 4 warned against. The ViewModel should *ask* for notes, not know they come from `api.example.com` over HTTP. Wrap the call in a small service behind an interface:

```csharp
public interface INotesApi
{
    Task<List<Note>> GetNotesAsync();
    Task AddNoteAsync(Note note);
}

public class NotesApi : INotesApi
{
    private readonly HttpClient _http;
    public NotesApi(HttpClient http) => _http = http;

    public async Task<List<Note>> GetNotesAsync() =>
        await _http.GetFromJsonAsync<List<Note>>("notes") ?? new();

    public async Task AddNoteAsync(Note note) =>
        await _http.PostAsJsonAsync("notes", note);
}
```

*What just happened:* the URLs lost their host (`"notes"` instead of the full address) because the `HttpClient` carries a base address, configured once in DI. The service depends on an injected `HttpClient` rather than newing its own - the same constructor-injection you've seen in ASP.NET Core. The ViewModel depends on `INotesApi`, never on `HttpClient`, so you can hand it a fake in a unit test.

Register both in `MauiProgram.cs`, the same DI container that wires up everything else:

```csharp
builder.Services.AddHttpClient<INotesApi, NotesApi>(client =>
    client.BaseAddress = new Uri("https://api.example.com/"));
```

*What just happened:* `AddHttpClient<INotesApi, NotesApi>` registered the service *and* gave it a properly managed `HttpClient` with the base address baked in. (It's the recommended way to hand out `HttpClient`s - it pools connections so you don't leak sockets, a real bug you'd hit `new`ing one per request.) Now any ViewModel can take `INotesApi` in its constructor.

### Calling it from a command - with the mobile realities handled

The ViewModel calling that service from a `[RelayCommand]`, with a loading flag (the Phase 4 pattern) and - crucially - error handling:

```csharp
[ObservableProperty]
private bool isBusy;

[RelayCommand]
private async Task LoadNotesAsync()
{
    if (IsBusy) return;
    IsBusy = true;
    try
    {
        var fromServer = await _notesApi.GetNotesAsync();
        Notes.Clear();
        foreach (var note in fromServer)
            Notes.Add(note);
    }
    catch (HttpRequestException)
    {
        await _alerts.ShowAsync("Couldn't reach the server. Check your connection.");
    }
    finally
    {
        IsBusy = false;
    }
}
```

*What just happened:* `IsBusy` flips on (bind it to an `ActivityIndicator` so the user sees a spinner), the `await` keeps the UI thread free while the network call is in flight, and the `try/catch` catches the call *failing* - which, on a phone, it routinely will.

⚠️ Desktop and web developers underestimate this: a mobile device is on a flaky network by default - signal drops in a tunnel, Wi-Fi hands off to cellular mid-request, a request hangs and times out. Three rules keep you out of trouble:

- **Never block the UI thread.** Always `async`/`await` network calls - a synchronous `.Result` will freeze the app and trigger an OS "not responding" kill.
- **Always expect failure.** Wrap calls in `try/catch` and show the user something human, not a stack trace.
- **Use HTTPS.** Plain HTTP is blocked by default on both iOS and Android for production traffic.

## The local storage tour: four tools, smallest to biggest

Now the offline half. MAUI gives you four built-in ways to persist data on the device. They aren't competitors - they're sized for different jobs, so reach for the *smallest* one that fits.

**1. `Preferences` - a single key/value, for settings.** Last-opened note id, a theme choice, "has the user seen the welcome screen." Stored in the platform's native settings store.

```csharp
Preferences.Set("theme", "dark");
var theme = Preferences.Get("theme", "light"); // "light" is the fallback
```

*What just happened:* one line in, one line out. The second argument to `Get` is the default returned when the key was never set, so first launch reads `"light"` instead of crashing. Use this for small, non-secret scalars only - not lists or objects.

**2. `SecureStorage` - encrypted key/value, for secrets.** An auth token, an API key, anything you'd be embarrassed to leave in plaintext. Same shape as `Preferences`, but the value is encrypted by the OS keychain - and the calls are `async`.

```csharp
await SecureStorage.SetAsync("token", jwt);
var token = await SecureStorage.GetAsync("token"); // null if not set
```

*What just happened:* the JWT went into the platform's secure enclave (iOS Keychain / Android KeyStore) rather than a plain settings file. ⚠️ Never put tokens or passwords in `Preferences` - that's the difference between the two stores.

**3. The file system - for app-private files.** For something bigger than a setting - a cached JSON blob, a downloaded image, an exported document - write a file under `FileSystem.AppDataDirectory`, a private folder only your app can read.

```csharp
var path = Path.Combine(FileSystem.AppDataDirectory, "notes.json");
await File.WriteAllTextAsync(path, json);
```

*What just happened:* `AppDataDirectory` resolves to the right private location on each platform, so you write ordinary `System.IO` file code and MAUI handles the per-platform path. Good for a handful of files; clumsy once you want to *query* the data ("notes containing 'meeting'").

**4. SQLite - a real database, for structured, queryable data.** The right home for the notes list itself: many rows, each with fields, that you want to add to, delete from, and search. SQLite is a full relational database in a single file on the device. Add the `sqlite-net-pcl` package, decorate your model, and get `async` table operations.

Mark up the `Note` model so SQLite knows how to store it:

```csharp
using SQLite;

public class Note
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public string Body { get; set; } = "";
}
```

*What just happened:* `[PrimaryKey, AutoIncrement]` tells SQLite this is the row's unique id and to fill it in automatically on insert - a new note doesn't need an id, it gets one. The other properties become columns. Same `Note` your ViewModel already binds to; the attributes are the only addition.

Now the store that saves and loads them:

```csharp
public class NoteDatabase
{
    private readonly SQLiteAsyncConnection _db;

    public NoteDatabase()
    {
        var path = Path.Combine(FileSystem.AppDataDirectory, "notes.db");
        _db = new SQLiteAsyncConnection(path);
        _db.CreateTableAsync<Note>().Wait();
    }

    public Task<int> SaveAsync(Note note) => _db.InsertAsync(note);
    public Task<List<Note>> GetAllAsync() => _db.Table<Note>().ToListAsync();
}
```

*What just happened:* the connection points at a `notes.db` file in the same private `AppDataDirectory`. `CreateTableAsync<Note>()` builds the table from the model's attributes (and does nothing if it already exists, so it's safe on every launch). `InsertAsync` writes a row; `Table<Note>().ToListAsync()` reads them all back. Swap `NoteDatabase` in behind a service interface and inject it into the ViewModel exactly like `INotesApi` - it never knows whether a note came from SQLite or the network.

### How to choose

| Data | Store |
|------|-------|
| One small setting (theme, last id) | `Preferences` |
| A secret (token, password) | `SecureStorage` |
| A blob or document | A file in `AppDataDirectory` |
| A list of structured records you'll query | **SQLite** |

## Offline-first: local plus sync

💡 The pattern that ties both directions together and makes an app feel genuinely good on a phone: **persist locally first, sync to the API when you can.**

The user adds a note - you write it to SQLite *immediately* and update the screen; the note is saved no matter what the network is doing. Then, in the background or on the next launch, you push unsynced notes to the API and pull down any new ones. SQLite is the source of truth on the device; the API is how that truth gets shared across devices. You won't build the full sync engine here, but *local write is instant, network sync is eventual* is what separates an app that survives real-world use from one that only works on office Wi-Fi.

## Recap

- An app's data flows two ways: **out** to a backend over `HttpClient`, and **down** into local storage on the device. A real app uses both.
- Call APIs with `HttpClient` + `System.Net.Http.Json` (`GetFromJsonAsync`, `PostAsJsonAsync`). Wrap calls in a service behind an interface, register it with `AddHttpClient` in DI, and inject it into ViewModels - never `new` an `HttpClient` in a ViewModel.
- Mobile networks fail constantly: `await` every call so the UI thread stays free, wrap calls in `try/catch` and surface human errors, and use HTTPS.
- Pick the smallest local store that fits: `Preferences` for settings, `SecureStorage` for secrets, files in `AppDataDirectory` for blobs, and **SQLite** (`sqlite-net-pcl`) for structured, queryable lists like the notes themselves.
- Aim for **offline-first**: write to SQLite instantly so the app always works, and sync to the API when a connection is available.

## Quick check

```quiz
[
  {
    "q": "You need to store a list of notes the user can add to, delete from, and search - and it must survive offline. Which local store fits?",
    "choices": ["Preferences", "SecureStorage", "SQLite via sqlite-net-pcl", "A single JSON file in AppDataDirectory"],
    "answer": 2,
    "explain": "SQLite is a real relational database for structured, queryable records. Preferences and SecureStorage are key/value only, and a flat JSON file can't be queried efficiently."
  },
  {
    "q": "Where should an authentication token (JWT) be stored?",
    "choices": ["Preferences, since it's just a string", "SecureStorage, which encrypts it via the OS keychain", "A file in AppDataDirectory", "In the ViewModel as a property"],
    "answer": 1,
    "explain": "SecureStorage encrypts values using the platform keychain/keystore. Preferences and plain files store data unencrypted, which is wrong for secrets."
  },
  {
    "q": "Why wrap an HttpClient call to the notes API in a try/catch and use async/await?",
    "choices": ["JSON parsing always throws", "It makes the request faster", "Mobile networks fail often, so calls must handle errors and never block the UI thread", "It's required syntax for HttpClient"],
    "answer": 2,
    "explain": "Phones lose signal routinely. await keeps the UI thread responsive during the request, and try/catch lets you show a human error instead of crashing when the network drops."
  }
]
```


---

# Platform Features & Deployment

MAUI gives you **one C# API that reaches each platform's native features** - the GPS chip, the
network state, the battery, the clipboard. Call one method, and MAUI talks to Android's location
services on Android and Apple's on iOS. For the rare case the shared API doesn't cover, you drop
into the `Platforms/` folders with native code guarded by `#if`. Once the app does what you want,
you **package it per store** - a different bundle and signing process for each target.

Three moves, in order: reach native features with shared code, escape to platform code only
when forced, then ship. Our notes app has lived on a single codebase since Phase 1, and that's
about to pay off - the same app, with the same logic, becomes an Android `.aab`, an iOS build,
and a Windows `.msix`.

> 📝 These device APIs used to be a separate package called **Xamarin.Essentials**. In modern
> MAUI they're built in, under namespaces like `Microsoft.Maui.Devices` and
> `Microsoft.Maui.ApplicationModel`. If you find old tutorials importing `Xamarin.Essentials`,
> that's the same feature set - the names just moved.

## Device APIs - one call, all platforms

A phone is a pile of sensors and services: location, network, battery, contacts, the camera.
Each platform exposes these through its own native SDK with its own types and ceremony. MAUI
wraps the common ones so you write the call **once**.

Take connectivity. Before our notes app syncs to a server, it should know whether there's a
network at all - firing an `HttpClient` request into airplane mode just gives a slow,
confusing failure. One property tells you:

```csharp
using Microsoft.Maui.Networking;

async Task SyncNotesAsync()
{
    if (Connectivity.Current.NetworkAccess != NetworkAccess.Internet)
    {
        await Shell.Current.DisplayAlert(
            "Offline",
            "You're not connected. Your notes are saved locally and will sync later.",
            "OK");
        return;
    }

    // We have a connection - safe to call the API (Phase 6).
    await _notesApi.PushAsync(_notes);
}
```

*What just happened:* `Connectivity.Current.NetworkAccess` returns an enum describing the
device's network state. We check for `Internet` *before* touching the network, so an offline
user gets a clear message instead of a timeout. The exact same code runs on Android, iOS, and
Windows - MAUI asks each platform's connectivity API under the hood.

Want to react when the connection changes mid-session? Subscribe to an event:

```csharp
Connectivity.Current.ConnectivityChanged += (s, e) =>
{
    bool online = e.NetworkAccess == NetworkAccess.Internet;
    SyncBanner.IsVisible = !online; // show an "offline" banner when we drop
};
```

*What just happened:* `ConnectivityChanged` fires whenever the device gains or loses a
connection. We flip a banner's visibility so the user always knows the app's sync state.

That single-call shape repeats across the whole family. A quick map of the ones you'll reach
for most:

| API | What it gives you | Example call |
|-----|-------------------|--------------|
| `Geolocation` | Current GPS coordinates | `await Geolocation.GetLocationAsync()` |
| `Connectivity` | Network state + changes | `Connectivity.Current.NetworkAccess` |
| `Battery` | Charge level, charging state | `Battery.Default.ChargeLevel` |
| `DeviceInfo` | Model, OS version, platform | `DeviceInfo.Current.Platform` |
| `Clipboard` | Copy/paste text | `await Clipboard.SetTextAsync(note)` |
| `Email` / `Browser` | Open a composer / a URL | `await Browser.OpenAsync(url)` |
| `Preferences` | Small key-value storage (Phase 6) | `Preferences.Set("key", value)` |

`DeviceInfo` is handy for branching on platform without leaving C#:

```csharp
using Microsoft.Maui.Devices;

if (DeviceInfo.Current.Platform == DevicePlatform.iOS)
{
    // e.g. nudge layout for the iOS status bar
}
```

*What just happened:* `DeviceInfo.Current.Platform` tells you which OS you're running on at
runtime, so you can make small adjustments in shared code without dropping into a `Platforms/`
folder. Use it for tweaks; use platform code below for genuinely native behavior.

## Permissions - ask, and declare

Some features touch private user data - location, camera, contacts. Both Android and iOS guard
these behind **runtime permission prompts**: the OS asks the user, at the moment of use, whether
your app may have access. MAUI gives you a shared API to check and request:

```csharp
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Devices.Sensors;

async Task<Location?> GetCurrentLocationAsync()
{
    var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();

    if (status != PermissionStatus.Granted)
        status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();

    if (status != PermissionStatus.Granted)
        return null; // user said no - degrade gracefully, don't crash

    return await Geolocation.Default.GetLocationAsync();
}
```

*What just happened:* we **check** the current permission status first (no need to nag a user
who already said yes), and only **request** if we don't have it. If the user declines, we
return `null` and the caller handles the no-location case. For our notes app, this might tag a
note with "where it was written," skipping the tag gracefully if location is off.

> ⚠️ The C# call is only half the job. Each platform also requires you to **declare** the
> permission in its manifest - and the request will **silently fail** (or the store will
> **reject your app**) if you forget. The declaration is per-platform:
>
> - **Android** → an entry in `Platforms/Android/AndroidManifest.xml`:
>   ```
>   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
>   ```
> - **iOS** → a *usage-description string* in `Platforms/iOS/Info.plist`. Apple shows your text
>   to the user in the prompt and rejects apps that omit it:
>   ```
>   <key>NSLocationWhenInUseUsageDescription</key>
>   <string>We tag notes with where you wrote them.</string>
>   ```

Two halves, every time: **request in C#, declare in the manifest.** When a permission
"doesn't work," a missing manifest entry is the first thing to check.

## Platform-specific code - only when you must

The shared APIs cover a lot, but not everything. Sometimes you need behavior that exists only on
one platform - a native widget, a vendor SDK, an OS-specific tweak. MAUI gives you three escape
hatches.

**1. Conditional compilation with `#if`.** The compiler defines a symbol per target
(`ANDROID`, `IOS`, `MACCATALYST`, `WINDOWS`), so you can fence off platform code inline:

```csharp
public void Vibrate()
{
#if ANDROID
    var vibrator = Android.Views.View.GetSystemService(/* ... Android API ... */);
    // call into the native Android vibrator
#elif IOS
    UIKit.UIImpactFeedbackGenerator
        .Init(UIKit.UIImpactFeedbackStyle.Medium)
        .ImpactOccurred();
#endif
}
```

*What just happened:* the code inside `#if ANDROID` only compiles into the Android build, and
`#elif IOS` only into the iOS build. Each branch calls that platform's native API directly.
Great for a one-off; messy if it sprawls.

**2. The `Platforms/` folders + partial classes.** For anything bigger, MAUI's project layout
already separates native code into `Platforms/Android/`, `Platforms/iOS/`, and so on. Declare a
`partial` method in shared code and implement it once per platform folder - same shape as `#if`,
but each platform's code lives in its own clean file.

**3. An interface with per-platform implementations.** The cleanest pattern for real native
features: define an interface in shared code, write one implementation per platform, and inject
the right one via dependency injection.

```csharp
// Shared code - the contract
public interface IDeviceTorch
{
    Task ToggleAsync(bool on);
}

// In a ViewModel - depend on the abstraction, never the platform
public class NoteEditorViewModel(IDeviceTorch torch)
{
    public Task FlashAsync() => torch.ToggleAsync(true);
}
```

*What just happened:* the ViewModel knows only `IDeviceTorch` - pure shared C#, fully testable.
The Android and iOS implementations live in their `Platforms/` folders and get registered with
the DI container at startup. Your app logic never sees a platform type.

> 💡 Reach for these only when the cross-platform API doesn't cover you. Most of what an app
> needs already has a shared API - check the device-API table first. Platform code is a tool
> for the edges, not the default.

## Deployment - package per store

Your notes app runs. Now you turn one codebase into store-ready bundles. Each target produces a
different artifact with its own signing, icons, and review process - the build command picks the
target with `-f`:

**Android** → a signed `.aab` (Android App Bundle, what Google Play wants) or `.apk`:

```bash
dotnet publish -f net8.0-android -c Release
```

*What just happened:* `dotnet publish` compiles a Release build for the Android target and
produces the app bundle. Sign it with a keystore (Google Play also offers managed signing) and
upload it to the Play Console. The `.aab` lets Google generate device-optimized APKs per user.

**iOS / Mac Catalyst** → an App Store build:

```bash
dotnet publish -f net8.0-ios -c Release
```

> ⚠️ Building and shipping iOS **requires a Mac** - Apple's toolchain (the signing and packaging
> step) only runs on macOS. You'll also need an **Apple Developer account** (paid) and
> **provisioning profiles** that tie your app ID and signing certificate together. There's no way
> around the Mac; even from a Windows dev box, the final iOS build runs on a connected or remote
> Mac.

**Windows** → an `.msix` package for the Microsoft Store or sideloading:

```bash
dotnet publish -f net8.0-windows10.0.19041.0 -c Release
```

*What just happened:* this produces an `.msix`, Windows' modern app-package format. Submit it
to the Microsoft Store or distribute it directly (sideloading) with a trusted certificate.

Each store then has its **own** gauntlet: signing keys to guard, icon and splash assets at the
right sizes, metadata and screenshots, and a review queue. Apple's review is the strictest - 
budget days, not minutes. Packaging is the easy part; store paperwork is where first-time
shippers lose time.

> 📝 The mechanics of *actually getting through a store review* - assets, metadata, privacy
> labels, beta tracks, and the waiting - are their own discipline. The
> [Ship Your Side Project](/guides/ship-your-side-project) guide walks the whole release path,
> and it applies directly here.

## Recap

- **One C# API reaches each platform's native features.** `Geolocation`, `Connectivity`,
  `Battery`, `DeviceInfo`, `Clipboard`, and friends are single calls that work everywhere - 
  check connectivity before syncing, read the GPS, copy text, all from shared code.
- **Permissions are two halves: request in C#, declare in the manifest.** Use
  `Permissions.CheckStatusAsync<T>()` / `RequestAsync<T>()`, AND add the entry to
  `AndroidManifest.xml` / `Info.plist`. ⚠️ A missing manifest entry fails silently or gets the
  app rejected.
- **Drop into platform code only when forced** - `#if ANDROID`, `Platforms/` partial classes,
  or an interface with per-platform implementations behind DI. Check the shared APIs first.
- **Package per target:** Android `.aab` via `dotnet publish -f net8.0-android`, iOS (⚠️ needs a
  Mac + Apple Developer account + provisioning profiles), Windows `.msix`. Each store brings its
  own signing, assets, and review.
- The whole point of MAUI pays off here: the same notes app, one codebase, becomes a native
  bundle on every platform.

## Quick check

```quiz
[
  {
    "q": "Your app calls Permissions.RequestAsync<Permissions.LocationWhenInUse>() and the GPS read still fails on a real Android device. What's the most likely cause?",
    "choices": ["MAUI doesn't support location on Android", "You forgot to declare the permission in AndroidManifest.xml", "You must use #if ANDROID for all location code", "Connectivity is off"],
    "answer": 1,
    "explain": "The C# request is only half the job - Android also needs the matching <uses-permission> entry in AndroidManifest.xml, and iOS needs a usage-description string in Info.plist. Without the manifest declaration, the request silently fails."
  },
  {
    "q": "Before syncing notes to a server, which API tells you whether the device has a network connection?",
    "choices": ["DeviceInfo.Current.Platform", "Battery.Default.ChargeLevel", "Connectivity.Current.NetworkAccess", "Preferences.Get"],
    "answer": 2,
    "explain": "Connectivity.Current.NetworkAccess returns the device's network state. Checking for NetworkAccess.Internet before an HttpClient call lets you fail fast and show an offline message instead of waiting on a timeout."
  },
  {
    "q": "Which deployment fact is true?",
    "choices": ["iOS apps can be built and shipped entirely from Windows with no Mac", "Android publishes to a .msix package", "Building/shipping iOS requires a Mac, an Apple Developer account, and provisioning profiles", "Windows apps ship as a signed .aab"],
    "answer": 2,
    "explain": "Apple's toolchain only runs on macOS, so iOS needs a Mac plus a paid Apple Developer account and provisioning profiles. Android ships an .aab/.apk; Windows ships an .msix."
  }
]
```


---

# Where to Go Next

Look at the distance you covered. You can describe a UI in **XAML** and arrange it with **layouts**. You can place **controls** and wire them to data with **binding**. You can structure an app the real way with **MVVM** - a View that's all markup, a ViewModel that holds state and commands, `INotifyPropertyChanged` keeping them in sync. You can move between screens with **Shell**, call **APIs** with `HttpClient`, and keep data around with **Preferences and SQLite**. You can reach into **platform features** - sensors, permissions, per-platform code - and you know the shape of getting a build into the stores.

That's a native, cross-platform app, written in C#, running on Android, iOS, macOS, and Windows from one codebase - a real skill.

So this last phase isn't more APIs to memorize - it's the map: where MAUI sits next to the other cross-platform frameworks, ways to reuse what you already know, the backend it pairs with, and one concrete thing to go build.

## MAUI vs Flutter and React Native

You'll get asked this, maybe in an interview: "Why MAUI instead of Flutter?" The real answer isn't "MAUI is better" - it's "they're aimed at different teams and different jobs." Pick the tool that fits the work and the people, not the loudest one.

```mermaid
flowchart TD
  Q{What's the situation?}
  Q -->|".NET team, shares C# with<br/>a backend, line-of-business app"| M[.NET MAUI]
  Q -->|"Want one rendering engine,<br/>pixel-perfect UI, big momentum"| F[Flutter]
  Q -->|"JS/React team, vast npm<br/>ecosystem, large hiring pool"| R[React Native]
```

Here's the straight breakdown:

- **.NET MAUI** - you build the UI in **C#**, render with **native controls**, and ship one codebase to four platforms. Its sweet spot is **.NET teams** and **line-of-business apps**, especially ones that share code with a .NET backend - no second language, no second toolchain, one debugger across the stack.
- **Flutter** - written in **Dart**, with **its own rendering engine** rather than native controls. That buys remarkable UI consistency across platforms and a lot of momentum, with a large, energetic mobile community.
- **React Native** - written in **JavaScript/React**, with a **huge ecosystem** and a big hiring pool. If your team already lives in JS and React, it meets you where you are.

💡 MAUI shines for C# shops and apps that share code with a .NET backend - that shared-language advantage is genuinely large. But face facts: **Flutter and React Native have larger mobile communities** - more packages, more tutorials, more people who've already hit your bug. None of these is "the bad one"; they all build native apps, just for different teams. If you internalized View + ViewModel + binding here, you've learned the hard part of any of them.

## Reuse what you already know

You don't have to choose between "web skills" and "native app." MAUI gives you two ways to bring more to the table.

**Blazor Hybrid.** A MAUI app can host a `BlazorWebView`, which runs real **Blazor** components - the same ones you'd build for the web - *inside* the native shell. A web-skilled team can reuse their UI and Razor know-how, and still mix in native MAUI pages where it matters. If you've gone through [Blazor From Zero](/guides/blazor-from-zero), that knowledge ports directly in.

**CommunityToolkit.Maui.** Before you hand-build a control or a converter, check the toolkit. **CommunityToolkit.Maui** adds extra controls, behaviors, converters, and helpers that fill the gaps in the box. Add it early - it saves you from reinventing pieces the community already polished.

## It pairs with ASP.NET Core

Most real apps talk to a backend, and MAUI has a natural partner. [ASP.NET Core From Zero](/guides/aspnet-core-from-zero) is the other half of this stack: the API your app calls for data, auth, and sync. The payoff for staying in one language is real here - you can **share C# model classes and DTOs** between app and server, so the shape your API returns is the exact type your ViewModel binds to. No duplicating models in a second language, no drift between client and server.

## What to build next

Reading more won't make this stick - finishing one real thing will. Pick either path.

**Path A - finish the notes app.** You built it phase by phase. Now take it the last mile:

- Persist notes locally with **SQLite** so they survive a restart.
- **Sync** with a backend API so notes follow the user across devices.
- Add a couple of **platform features** - a share action, a notification, a sensor - to make it feel native.
- Drop in **CommunityToolkit.Maui** for the controls and helpers you've been missing.
- Then **publish it to a store**. Going through that gauntlet once teaches you more than any tutorial.

**Path B - a small CRUD app on your own API.** Build create / read / update / delete end to end: a MAUI front end against an **ASP.NET Core** backend you write, sharing the same C# DTOs. Add auth so users see their own data - it exercises nearly everything you learned, plus the backend it leans on.

Remember the throughline that ran under every phase: **XAML describes the UI, a ViewModel holds the state and behavior, and binding wires them together** - one codebase, native everywhere. Go ship one of these, put it on a real device, and show someone. You're ready.

## Recap

1. **You can ship a native cross-platform app in C#** - XAML and layouts, controls and binding, MVVM, Shell navigation, APIs and local storage, platform features and deployment. That's a real skill, not a toy.
2. **MAUI vs Flutter vs React Native is about fit, not winners** - MAUI wins for .NET teams and line-of-business apps that share C# with a backend; Flutter (Dart, its own renderer) and React Native (JS, huge ecosystem) have larger mobile communities. Weigh the tradeoff clearly.
3. **Reuse what you know** - Blazor Hybrid runs real Blazor components inside a MAUI shell via `BlazorWebView`; CommunityToolkit.Maui adds controls, behaviors, converters, and helpers worth pulling in early.
4. **It pairs with ASP.NET Core** - the backend your app talks to, with shared C# models and DTOs so client and server stay in sync without duplication.
5. **Build one app and finish it** - finish the notes app (SQLite + API sync + platform features) and publish it, or a small CRUD app on your own ASP.NET Core API. Shipping cements the whole guide.

## Quick check

Three decisions to take with you as you leave this guide:

```quiz
[
  {
    "q": "A .NET team is building an internal line-of-business app and wants to share their C# model classes with the backend. Which choice fits the reasoning best?",
    "choices": [
      "React Native, because it always has the smallest app size",
      ".NET MAUI, because the UI is C# and shares language, types, and DTOs with the backend",
      "Flutter, because line-of-business apps require Dart",
      "It makes no difference; all three are interchangeable"
    ],
    "answer": 1,
    "explain": "MAUI's big win for a .NET team is building the app in C# and sharing models and DTOs with an ASP.NET Core backend. Flutter and React Native are strong choices too, especially for teams already in Dart or JS, or wanting larger mobile communities."
  },
  {
    "q": "Your team has strong Blazor and web skills and wants to reuse that UI inside a native MAUI app. What lets you do that?",
    "choices": [
      "CommunityToolkit.Maui",
      "Shell routing",
      "A BlazorWebView hosting Blazor components (Blazor Hybrid)",
      "Preferences"
    ],
    "answer": 2,
    "explain": "Blazor Hybrid uses a BlazorWebView to run real Blazor web components inside a MAUI app, so a web-skilled team reuses its UI and Razor skills while still mixing in native MAUI pages."
  },
  {
    "q": "You want extra ready-made controls, behaviors, and converters instead of hand-building them in MAUI. What do you reach for?",
    "choices": [
      "Flutter",
      "CommunityToolkit.Maui",
      "HttpClient",
      "InteractiveAuto"
    ],
    "answer": 1,
    "explain": "CommunityToolkit.Maui adds extra controls, behaviors, converters, and helpers that fill gaps in the box. Add it early so you stop reinventing pieces the community already built."
  }
]
```
