What you will learn

  • Install sqlite-net-pcl and define a table model
  • Read and write records with SQLiteAsyncConnection
  • Refresh a CollectionView after changes
How to use this example: Start from a new .NET MAUI App project called MauiTodo with .NET 10. Use the complete file listings below, and leave other template-generated files unchanged unless the lesson explicitly says otherwise. Run the Android target on Windows.
iOS testing: The shared code is designed for MAUI targets, but no iOS build is claimed. iOS compilation, device testing and App Store release require a Mac/Xcode toolchain.

1. Create the project and install SQLite

Create MauiTodo. Right-click the project and choose Manage NuGet Packages. Install sqlite-net-pcl (the package maintained by SQLite-net). Add TodoTask.cs and TodoDatabase.cs as C# files in the project root.

Important: The namespace in all four files is MauiTodo. If you choose a different project name, change the namespaces and the XAML x:Class value consistently.

2. Implement the database service

The model uses [PrimaryKey, AutoIncrement] to define the SQLite key. The database path uses FileSystem.AppDataDirectory, which is appropriate for private app data on mobile devices. SQLiteAsyncConnection prevents long disk operations from blocking the UI thread.

3. Build the list interface

The Entry captures a title. The Add button creates a row. A CollectionView shows current tasks, and the Remove Selected button deletes the highlighted record. The page loads saved data every time it appears.

4. Test persistence

  1. Run the Android app.
  2. Add three sample tasks.
  3. Close and restart the app; the tasks should still exist.
  4. Select an item, tap Remove selected, and verify the list refreshes.

Data persists for the installed app. Uninstalling the app can remove its local private database.

Complete sample code

Use the exact file names and folder locations shown below. Copy each listing into your MAUI project. The separate source files can also be downloaded individually.

TodoTask.cs
using SQLite;

namespace MauiTodo;

public class TodoTask
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }

    [NotNull]
    public string Title { get; set; } = "";
}
TodoDatabase.cs
using Microsoft.Maui.Storage;
using SQLite;

namespace MauiTodo;

public class TodoDatabase
{
    private SQLiteAsyncConnection? database;

    private async Task InitializeAsync()
    {
        if (database is not null) return;
        string path = Path.Combine(FileSystem.AppDataDirectory, "tasks.db3");
        database = new SQLiteAsyncConnection(path);
        await database.CreateTableAsync<TodoTask>();
    }

    public async Task<List<TodoTask>> GetAllAsync()
    {
        await InitializeAsync();
        return await database!.Table<TodoTask>().ToListAsync();
    }

    public async Task AddAsync(string title)
    {
        await InitializeAsync();
        await database!.InsertAsync(new TodoTask { Title = title });
    }

    public async Task DeleteAsync(TodoTask task)
    {
        await InitializeAsync();
        await database!.DeleteAsync(task);
    }
}
MainPage.xaml
<?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="MauiTodo.MainPage"
             Title="Tasks">
    <Grid Padding="20" RowDefinitions="Auto,Auto,*,Auto" RowSpacing="12">
        <Label Text="My To-Do List" FontSize="26"
               FontAttributes="Bold" />
        <Grid Grid.Row="1" ColumnDefinitions="*,Auto" ColumnSpacing="8">
            <Entry x:Name="TaskEntry" Placeholder="New task" />
            <Button Grid.Column="1" Text="Add" Clicked="OnAddClicked" />
        </Grid>
        <CollectionView Grid.Row="2" x:Name="TasksView"
                        SelectionMode="Single"
                        SelectionChanged="OnTaskSelectionChanged"
                        EmptyView="No tasks yet.">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Border Stroke="#D6E4F3" StrokeThickness="1"
                            Padding="12" Margin="0,4">
                        <Label Text="{Binding Title}" FontSize="17" />
                    </Border>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
        <Button Grid.Row="3" Text="Remove selected"
                Clicked="OnRemoveClicked" />
    </Grid>
</ContentPage>
MainPage.xaml.cs
namespace MauiTodo;

public partial class MainPage : ContentPage
{
    private readonly TodoDatabase database = new();
    private TodoTask? selectedTask;

    public MainPage() => InitializeComponent();

    protected override async void OnAppearing()
    {
        base.OnAppearing();
        await RefreshAsync();
    }

    private async Task RefreshAsync()
    {
        TasksView.ItemsSource = await database.GetAllAsync();
    }

    private async void OnAddClicked(object sender, EventArgs e)
    {
        string title = TaskEntry.Text?.Trim() ?? "";
        if (title.Length == 0)
        {
            await DisplayAlertAsync("Validation", "Enter a task title.", "OK");
            return;
        }
        await database.AddAsync(title);
        TaskEntry.Text = "";
        await RefreshAsync();
    }

    private void OnTaskSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        selectedTask = e.CurrentSelection.FirstOrDefault() as TodoTask;
    }

    private async void OnRemoveClicked(object sender, EventArgs e)
    {
        if (selectedTask is null)
        {
            await DisplayAlertAsync("Select a task", "Tap a task first.", "OK");
            return;
        }
        await database.DeleteAsync(selectedTask);
        selectedTask = null;
        TasksView.SelectedItem = null;
        await RefreshAsync();
    }
}

Common problems and solutions

  • SQLite types are unknown: Install sqlite-net-pcl into the MAUI project, then restore packages.
  • The list seems empty after restart: Confirm that a task was added successfully and the same app installation is being launched.
  • Database errors: Check that the table model has a primary key and that FileSystem.AppDataDirectory is used for the path.

Practice exercise

Add a Completed Boolean property to the model and a checkbox in the list. For an existing database, plan and test a schema migration rather than silently dropping saved tasks.

Expected result: Tasks persist after the app is closed and can be removed from the SQLite database.

Official references