100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Building a To-Do App

A hands-on walkthrough of building a MAUI to-do list app with MVVM data binding and local SQLite persistence.

PracticeBeginner11 min readJul 10, 2026
Analogies

Project Setup and the TodoItem Model

Start a new MAUI app with dotnet new maui -n TodoApp, which scaffolds the platform heads and a default MainPage. Before touching XAML, define the domain model: a simple TodoItem class with an Id, Title, IsDone boolean, and DueDate. Keeping the model free of UI concerns (no INotifyPropertyChanged on the raw entity) lets you reuse it cleanly for persistence, while a separate observable wrapper handles UI binding.

🏏

Cricket analogy: Before a tournament begins, the groundskeeper prepares the pitch (project scaffold) long before any players (UI code) arrive, and the scorecard template (TodoItem model) is fixed before a single run is scored.

csharp
public class TodoItem
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public bool IsDone { get; set; }
    public DateTime? DueDate { get; set; }
}

Wiring Up MVVM Data Binding

The view model wraps TodoItem in an ObservableObject (from CommunityToolkit.Mvvm) so property changes automatically notify the UI, and exposes an ObservableCollection<TodoItemViewModel> for the list plus [RelayCommand] methods for AddTodo, ToggleDone, and DeleteTodo. The XAML CollectionView binds ItemsSource to that collection, using a DataTemplate with a CheckBox bound two-way to IsDone and a Label bound to Title, with compiled bindings (x:DataType) enabled for both the page and the DataTemplate to catch typos at build time.

🏏

Cricket analogy: The view model is like the scoreboard operator who watches every ball bowled (property change) and instantly updates the digital display (UI), rather than the crowd having to manually recalculate the score themselves.

xaml
<CollectionView x:DataType="vm:TodoListViewModel"
                ItemsSource="{Binding Todos}">
  <CollectionView.ItemTemplate>
    <DataTemplate x:DataType="vm:TodoItemViewModel">
      <Grid ColumnDefinitions="Auto,*" Padding="10">
        <CheckBox IsChecked="{Binding IsDone}" />
        <Label Grid.Column="1" Text="{Binding Title}"
               TextDecorations="{Binding IsDone, Converter={StaticResource BoolToStrikethrough}}" />
      </Grid>
    </DataTemplate>
  </CollectionView.ItemTemplate>
</CollectionView>

Persisting To-Dos with SQLite

Add the sqlite-net-pcl NuGet package and create a TodoDatabase service that opens a connection to a file path from FileSystem.AppDataDirectory, calling CreateTableAsync<TodoItem>() once at startup. Wrap all inserts, updates, and deletes in async methods on this service, inject it via DI as a singleton in MauiProgram.cs, and call LoadTodosAsync from the view model's constructor or an OnAppearing-triggered command so the list survives app restarts.

🏏

Cricket analogy: SQLite persistence is like the official scorebook that survives after the day's play ends, so tomorrow's match can pick up historical stats, unlike a chalk scoreboard that's wiped clean overnight.

Call await Database.Init() once, guarded by a boolean flag or lazy Task, before any query — opening the SQLite connection is async and calling it redundantly on every page navigation wastes I/O.

Never store the SQLite database file inside the app bundle's read-only resources folder — always use FileSystem.AppDataDirectory, which is writable and correctly sandboxed per platform (Application Support on iOS, files/ on Android).

  • Scaffold with dotnet new maui, then define a plain TodoItem model decoupled from UI concerns.
  • Wrap the model in an ObservableObject-based view model for automatic UI updates via data binding.
  • Use CollectionView with a DataTemplate and compiled bindings (x:DataType) for the to-do list.
  • Use [RelayCommand] from CommunityToolkit.Mvvm to implement Add, Toggle, and Delete actions.
  • Persist data with sqlite-net-pcl, storing the .db3 file under FileSystem.AppDataDirectory.
  • Register the database service as a DI singleton so all pages share one connection.
  • Initialize the database connection lazily/once to avoid redundant async overhead on every navigation.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#NETMAUIStudyNotes#BuildingAToDoApp#Building#App#Project#Setup#StudyNotes#SkillVeris#ExamPrep