Dataniedziela, 9 sierpnia 2026 Czas08:06:31
← Aplikacje mobilne (.NET MAUI)

Lekcja 17. Bazy danych SQLite w aplikacji mobilnej — wprowadzenie

Trudny / egzaminacyjny

Po co się tego uczymy?

Pliki TXT i JSON z poprzedniej lekcji świetnie nadają się do prostych danych, ale gdy aplikacja ma przechowywać setki czy tysiące rekordów, sortować je, wyszukiwać i filtrować — zapisywanie całej listy jako jeden plik JSON za każdym razem robi się nieefektywne i niewygodne. Do tego służy prawdziwa baza danych — a w aplikacjach mobilnych standardem jest SQLite, dokładnie ta sama technologia bazodanowa, którą poznałeś już w dziale "Bazy danych i SQL", tylko teraz osadzona bezpośrednio wewnątrz aplikacji.

Teoria

Dlaczego akurat SQLite w aplikacjach mobilnych? Cała baza danych to JEDEN plik (rozszerzenie .db3 albo .db) — nie trzeba instalować ani konfigurować żadnego oddzielnego serwera bazodanowego. SQLite działa identycznie na Androidzie, iOS, Windows i macOS, działa w pełni OFFLINE (bez połączenia z internetem) i obsługuje normalne zapytania SQL (SELECT, INSERT, UPDATE, DELETE) — dokładnie te same polecenia, których nauczyłeś się w dziale baz danych. Na egzaminie INF.04, jeśli w zadaniu pojawia się baza danych w aplikacji mobilnej, to niemal zawsze jest to właśnie SQLite.

Przygotowanie projektu. SQLite w MAUI obsługuje się przez bibliotekę (pakiet NuGet) sqlite-net-pcl — instalujesz ją przez PPM na projekt → Manage NuGet Packages… → wyszukaj i zainstaluj. Zalecana struktura folderów (analogiczna do poprzednich lekcji): folder Models na klasy reprezentujące tabele, folder Data na klasę obsługującą połączenie z bazą i zapytania. Sam plik bazy danych zapisujemy w tym samym bezpiecznym miejscu co pliki z poprzedniej lekcji: FileSystem.AppDataDirectory.

Model danych = tabela. Każda klasa modelu, którą chcesz zapisywać w SQLite, oznaczasz atrybutami z biblioteki SQLite:

Atrybut Zastosowanie
[PrimaryKey, AutoIncrement] pole Id jest kluczem głównym, a baza sama nadaje kolejne numery (identycznie jak AUTO_INCREMENT w SQL)
[MaxLength(100)] ogranicza maksymalną długość tekstu w danej kolumnie
[NotNull] kolumna nie może być pusta (odpowiednik NOT NULL w SQL)
[Ignore] właściwość klasy C#, która ma NIE być zapisywana jako kolumna w tabeli

Klasa obsługi bazy (serwis). Zamiast pisać zapytania SQL ręcznie w każdej stronie aplikacji, tworzy się jedną klasę (np. NotatkaDatabase) skupiającą WSZYSTKIE operacje na danej tabeli — dokładnie ta sama zasada oddzielenia logiki od UI z wcześniejszych lekcji. Klasa ta trzyma połączenie SQLiteAsyncConnection i udostępnia metody odpowiadające podstawowym operacjom SQL:

Metoda biblioteki Odpowiednik SQL
CreateTableAsync<T>() CREATE TABLE (tworzy tabelę, jeśli jeszcze nie istnieje)
Table<T>().ToListAsync() SELECT * FROM tabela
InsertAsync(obiekt) INSERT INTO tabela ...
UpdateAsync(obiekt) UPDATE tabela SET ... WHERE Id = ...
DeleteAsync(obiekt) DELETE FROM tabela WHERE Id = ...

Wszystkie te metody są ASYNCHRONICZNE (zwracają Task/Task<T> i wymagają await) — operacje na bazie danych, podobnie jak na plikach, mogą chwilę trwać i nie powinny blokować interfejsu użytkownika.

Globalny dostęp do bazy. Bazę danych zwykle inicjalizuje się RAZ, przy starcie aplikacji, w konstruktorze klasy App (plik App.xaml.cs), i udostępnia jako statyczną właściwość (App.Database) — dzięki temu KAŻDA strona aplikacji ma dostęp do tego samego połączenia z bazą, bez potrzeby tworzenia nowego przy każdym wejściu na stronę.

Schemat

App.xaml.cs (start aplikacji)
        │
        │  dbPath = Path.Combine(AppDataDirectory, "notatki.db3")
        │  Database = new NotatkaDatabase(dbPath)
        ▼
NotatkaDatabase (Data/)                    Notatka (Models/)
┌────────────────────────────┐             ┌──────────────────────┐
│ SQLiteAsyncConnection       │  operuje    │ [PrimaryKey,          │
│ GetNotatkiAsync()   (SELECT)│──na────────►│  AutoIncrement] Id    │
│ ZapiszNotatkeAsync() (INSERT│  obiektach  │ [MaxLength(100)] Tytul│
│ UsunNotatkeAsync()  (DELETE)│  tej klasy  │ Tresc                 │
└──────────────┬─────────────┘             └──────────────────────┘
               │  App.Database.XXX()
               ▼
NotatkiPage.xaml.cs — Dodaj_Click / WczytajNotatki / Usun_Click
               │
               ▼
CollectionView — wyświetla listę notatek z bazy

Przykład z życia

Aplikacja notatnika przechowująca setki notatek nie mogłaby efektywnie działać na jednym pliku JSON odczytywanym i zapisywanym w całości przy każdej zmianie — SQLite pozwala dodawać, usuwać i wyszukiwać pojedyncze notatki bez przepisywania całego zbioru danych za każdym razem, dokładnie tak jak działają prawdziwe aplikacje typu Notatnik Google czy Evernote (choć oczywiście w znacznie bardziej rozbudowanej wersji).

NotatkiPage.xaml

<!-- NotatkiPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="BazyDemo.NotatkiPage"
             Title="Notatki">

    <VerticalStackLayout Padding="16" Spacing="12">

        <Entry x:Name="TytulEntry" Placeholder="Tytuł notatki" />
        <Editor x:Name="TrescEntry" Placeholder="Treść notatki"
                AutoSize="TextChanges" HeightRequest="120" Margin="0,4,0,0" />
        <Button Text="Dodaj notatkę" Clicked="Dodaj_Click" Margin="0,8,0,0" />

        <CollectionView x:Name="ListaNotatek" HeightRequest="300"
                        SelectionMode="Single" Margin="0,12,0,0">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Frame BorderColor="Gray" Padding="8" Margin="0,6" HasShadow="False">
                        <VerticalStackLayout Spacing="4">
                            <Label Text="{Binding Tytul}" FontAttributes="Bold" FontSize="16" />
                            <Label Text="{Binding Tresc}" FontSize="14" LineBreakMode="WordWrap" />
                        </VerticalStackLayout>
                    </Frame>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

        <Button Text="Usuń zaznaczoną" Clicked="Usun_Click" Margin="0,8,0,0" />

    </VerticalStackLayout>
</ContentPage>

Models/Notatka.cs + Data/NotatkaDatabase.cs + App.xaml.cs + NotatkiPage.xaml.cs

// Models/Notatka.cs - model = tabela w SQLite
using SQLite;

namespace BazyDemo.Models;

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

    [MaxLength(100)]
    public string Tytul { get; set; } = "";

    public string Tresc { get; set; } = "";
}

// ---------------------------------------------------------------------

// Data/NotatkaDatabase.cs - obsługa bazy (CRUD)
using SQLite;
using BazyDemo.Models;

namespace BazyDemo.Data;

public class NotatkaDatabase
{
    private readonly SQLiteAsyncConnection _database;

    public NotatkaDatabase(string dbPath)
    {
        _database = new SQLiteAsyncConnection(dbPath);
        _database.CreateTableAsync<Notatka>().Wait();
    }

    public Task<List<Notatka>> GetNotatkiAsync() => _database.Table<Notatka>().ToListAsync();
    public Task<int> ZapiszNotatkeAsync(Notatka notatka) => _database.InsertAsync(notatka);
    public Task<int> UsunNotatkeAsync(Notatka notatka) => _database.DeleteAsync(notatka);
}

// ---------------------------------------------------------------------

// App.xaml.cs - inicjalizacja bazy przy starcie aplikacji
using BazyDemo.Data;

namespace BazyDemo;

public partial class App : Application
{
    public static NotatkaDatabase Database { get; private set; }

    public App()
    {
        InitializeComponent();

        string dbPath = Path.Combine(FileSystem.AppDataDirectory, "notatki.db3");
        Database = new NotatkaDatabase(dbPath);

        MainPage = new AppShell();
    }
}

// ---------------------------------------------------------------------

// NotatkiPage.xaml.cs
using BazyDemo.Models;

namespace BazyDemo;

public partial class NotatkiPage : ContentPage
{
    public NotatkiPage()
    {
        InitializeComponent();
        WczytajNotatki();
    }

    private async void WczytajNotatki()
    {
        ListaNotatek.ItemsSource = await App.Database.GetNotatkiAsync();
    }

    private async void Dodaj_Click(object sender, EventArgs e)
    {
        if (!string.IsNullOrWhiteSpace(TytulEntry.Text))
        {
            var nowa = new Notatka { Tytul = TytulEntry.Text, Tresc = TrescEntry.Text };
            await App.Database.ZapiszNotatkeAsync(nowa);
            TytulEntry.Text = "";
            TrescEntry.Text = "";
            WczytajNotatki();
        }
        else
        {
            await DisplayAlert("Błąd", "Podaj tytuł notatki", "OK");
        }
    }

    private async void Usun_Click(object sender, EventArgs e)
    {
        if (ListaNotatek.SelectedItem is Notatka wybrana)
        {
            await App.Database.UsunNotatkeAsync(wybrana);
            WczytajNotatki();
        }
        else
        {
            await DisplayAlert("Info", "Najpierw zaznacz notatkę", "OK");
        }
    }
}

Komentarz i wyjaśnienie kodu

Zwróć uwagę na podział odpowiedzialności między CZTERY pliki: Notatka.cs (co to za dane), NotatkaDatabase.cs (jak się z nimi komunikować — CRUD), App.xaml.cs (kiedy i gdzie baza jest tworzona, jedna instancja dla całej aplikacji) i NotatkiPage.xaml.cs (jak dane są pokazywane i modyfikowane przez użytkownika).

WczytajNotatki() jest wywoływana DWUKROTNIE w tym przykładzie: raz w konstruktorze strony (żeby lista była wypełniona od razu po otwarciu), i ponownie po każdej zmianie danych (dodaniu, usunięciu) — to gwarantuje, że CollectionView zawsze pokazuje AKTUALNY stan bazy danych, a nie "starą" wersję listy z pamięci.

ListaNotatek.SelectedItem is Notatka wybrana to wzorzec dopasowania (pattern matching) — sprawdza jednocześnie, czy coś jest zaznaczone (nie jest null) ORAZ rzutuje to na typ Notatka, przypisując do zmiennej wybrana w jednym kroku. Dzięki SelectionMode="Single" w XAML, CollectionView pozwala zaznaczyć dokładnie jedną pozycję na liście (kliknięciem), a SelectedItem zwraca właśnie ten wybrany obiekt.

Ćwiczenie samodzielne

Utwórz nowy projekt MAUI, zainstaluj pakiet sqlite-net-pcl, i odtwórz powyższy przykład notatnika. Sprawdź na emulatorze: dodaj kilka notatek, zamknij aplikację CAŁKOWICIE, uruchom ponownie i upewnij się, że notatki nadal tam są (w przeciwieństwie do zwykłej listy w pamięci, dane w SQLite przetrwają restart).

Zadania do pracy własnej

  1. Dodaj do modelu Notatka pole DataUtworzenia typu DateTime, ustawiane automatycznie na DateTime.Now przy tworzeniu nowej notatki, i wyświetl tę datę w szablonie listy.

  2. Dodaj do NotatkaDatabase metodę AktualizujNotatkeAsync(Notatka notatka) korzystającą z _database.UpdateAsync(notatka), i wykorzystaj ją do zaimplementowania edycji istniejącej notatki (kliknięcie notatki na liście wypełnia pola Entry/Editor jej danymi, a przycisk zmienia się na "Zapisz zmiany").

  3. Zbuduj aplikację "Lista kontaktów" z modelem Kontakt (Imię, Nazwisko, Telefon, Email) i pełnym CRUD-em przez SQLite: dodawanie, wyświetlanie posortowanej alfabetycznie listy (podpowiedź: LINQ OrderBy na wyniku z GetNotatkiAsync-podobnej metody), edycję i usuwanie, oraz pole wyszukiwania filtrujące listę po imieniu lub nazwisku (analogicznie do filtrowania z lekcji o CollectionView).

Typowe błędy

Tworzenie NOWEGO połączenia SQLiteAsyncConnection w każdej stronie zamiast korzystania z jednej, współdzielonej instancji przez App.Database — prowadzi do niepotrzebnego zużycia zasobów i potencjalnych konfliktów przy jednoczesnym dostępie do tego samego pliku bazy.

Zapominanie o CreateTableAsync<T>() przy starcie — bez tego wywołania tabela nigdy nie zostanie utworzona, a każda próba operacji na niej zakończy się błędem "no such table"; metoda jest bezpieczna do wywoływania za każdym razem (nie nadpisuje istniejącej tabeli, jeśli już istnieje).

Brak odświeżenia listy (WczytajNotatki()) po operacji zmieniającej daneInsertAsync/DeleteAsync zmieniają bazę danych, ale NIE odświeżają automatycznie ItemsSource na ekranie; trzeba to zrobić ręcznie, ponownie pobierając listę.

Mylenie [PrimaryKey, AutoIncrement] z ręcznym ustawianiem Id — jeśli pole jest oznaczone jako auto-inkrementowane, nie należy samodzielnie przypisywać mu wartości przed InsertAsync (baza sama nada kolejny numer); ręczne ustawienie może prowadzić do konfliktów kluczy.

Nawiązanie do egzaminu zawodowego

To bezpośrednia realizacja wymagania INF.04.6.2 "baza danych w aplikacji mobilnej" oraz naturalne połączenie z całym działem INF.03.4 (bazy danych i SQL) — te same pojęcia (tabela, klucz główny, SELECT/INSERT/DELETE) występują tu w nowym kontekście: nie osobnego programu bazodanowego, tylko wbudowanej w aplikację mobilną biblioteki. Kolejna lekcja rozwinie ten temat o praktyczny projekt z dwiema powiązanymi tabelami (produkty i koszyk).