Dataniedziela, 9 sierpnia 2026 Czas06:14:43
← Aplikacje mobilne (.NET MAUI)

Lekcja 16. Obsługa plików TXT i JSON w .NET MAUI

Trudny / egzaminacyjny

Po co się tego uczymy?

Bez trwałego zapisu aplikacja traci wszystkie dane po zamknięciu — każde uruchomienie zaczyna się od zera. Pliki pozwalają zachować dane na urządzeniu i wczytać je przy kolejnym starcie aplikacji. To dokładnie ten scenariusz, którego wymaga egzamin INF.04: zapisz dane → zamknij aplikację → uruchom ponownie → wczytaj i pokaż to, co było zapisane wcześniej.

Teoria

W .NET MAUI, w odróżnieniu od aplikacji desktopowej, NIE MOŻNA zapisywać plików w dowolnym miejscu na dysku — aplikacje mobilne (Android/iOS) działają w piaskownicy (sandboxie) z ograniczonym dostępem do systemu plików, ze względów bezpieczeństwa. Dlatego MAUI udostępnia gotową, bezpieczną lokalizację, identyczną w kodzie niezależnie od platformy: FileSystem.AppDataDirectory — folder danych aplikacji, dostępny do odczytu i zapisu na Androidzie, iOS, Windows i macOS. Ścieżkę do konkretnego pliku buduje się zawsze przez Path.Combine(FileSystem.AppDataDirectory, "nazwa.ext"), NIGDY przez ręczne sklejanie stringów ukośnikami (różne platformy używają różnych separatorów ścieżek).

Zapis i odczyt zwykłego tekstu (TXT) — najprostszy przypadek, np. notatnik. Klasa File (z System.IO) udostępnia metody asynchroniczne: File.WriteAllTextAsync(sciezka, tekst) zapisuje (nadpisując istniejący plik) i File.ReadAllTextAsync(sciezka) odczytuje całą zawartość jako jeden string. Metody ASYNCHRONICZNE (z await) są tu ważne — operacje na pliku mogą chwilę trwać, a synchroniczna wersja zablokowałaby na ten czas cały interfejs aplikacji (aplikacja "zamarzłaby"). Przed odczytem zawsze warto sprawdzić File.Exists(sciezka), żeby uniknąć wyjątku przy próbie odczytania pliku, który jeszcze nie istnieje (np. przy pierwszym uruchomieniu aplikacji).

Zapis i odczyt danych strukturalnych (JSON). Gdy dane to nie jeden tekst, tylko lista albo obiekt (np. lista imion, lista produktów), zwykły plik TXT nie wystarczy — potrzeba formatu, który zachowa STRUKTURĘ danych. JSON (JavaScript Object Notation) to uniwersalny, czytelny dla człowieka format tekstowy do zapisu takich danych, obsługiwany w C# przez wbudowaną klasę System.Text.Json.JsonSerializer:

Metoda Co robi
JsonSerializer.Serialize(obiekt) zamienia obiekt C# (np. List<string>) na tekst w formacie JSON
JsonSerializer.Deserialize<T>(tekstJson) zamienia tekst JSON z powrotem na obiekt C# podanego typu T

Typowy przepływ: dane trzymane są w RAM jako zwykła List<T> (albo lista obiektów własnej klasy modelu) → przy zapisie Serialize zamienia listę na tekst i zapisujemy go do pliku przez File.WriteAllTextAsync → przy odczycie File.ReadAllTextAsync wczytuje tekst, a Deserialize<List<string>> odtwarza z niego listę. Operator ?? z new List<string>() po Deserialize zabezpiecza przed sytuacją, gdyby plik był uszkodzony albo pusty (metoda zwróciłaby wtedy null).

Odświeżanie CollectionView po zmianie kolekcji. Jeśli lista danych to zwykła List<string> (a nie ObservableCollection<T> z poprzedniej lekcji), CollectionView NIE odświeży się automatycznie po dodaniu elementu do listy. Prosty, skuteczny trik: Lista.ItemsSource = null; Lista.ItemsSource = imiona; — wymuszenie ponownego przypisania źródła danych każe kontrolce przerysować całą listę od nowa.

Schemat

ZAPIS (Editor/Entry → plik)                    ODCZYT (plik → Editor/lista)

Editor.Text                                     File.Exists(path)?
    │                                               │
    ▼                                          tak  │  nie
File.WriteAllTextAsync(path, tekst)                │   │
    │                                               ▼   ▼
    ▼                                    File.ReadAllTextAsync   Status: "Plik nie istnieje"
notatka.txt (na dysku aplikacji)              │
                                               ▼
JSON:                                    Editor.Text = wczytanyTekst
List<string> imiona (RAM)
    │
    ▼                                    JSON (odczyt):
JsonSerializer.Serialize(imiona)         File.ReadAllTextAsync(path)
    │                                          │
    ▼                                          ▼
File.WriteAllTextAsync(path, json)       JsonSerializer.Deserialize<List<string>>(json)
    │                                          │
    ▼                                          ▼
imiona.json (na dysku aplikacji)         imiona = wynik ?? new List<string>()
                                               │
                                               ▼
                                    Lista.ItemsSource = null; Lista.ItemsSource = imiona;

Przykład z życia

Prosty notatnik mobilny zapisuje wpisaną treść do pliku TXT, żeby przetrwała zamknięcie aplikacji — dokładnie jak wbudowana aplikacja "Notatki" w telefonie. Lista zakupów albo lista kontaktów w małej aplikacji może z kolei być zapisywana w formacie JSON, bo trzeba zachować WIELE pozycji naraz, a nie tylko jeden ciąg tekstu — to najprostsza, w pełni działająca forma trwałego przechowywania danych, zanim w kolejnych lekcjach poznasz prawdziwą bazę danych SQLite.

TxtPage.xaml + JsonPage.xaml

<!-- TxtPage.xaml (notatnik TXT) -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="PlikiDemo.TxtPage"
             Title="Notatnik TXT">

    <VerticalStackLayout Padding="16" Spacing="12">
        <Editor x:Name="NotatkaEditor" HeightRequest="150" Placeholder="Wpisz notatkę..." />

        <HorizontalStackLayout Spacing="8">
            <Button Text="Zapisz" Clicked="Zapisz_Click" />
            <Button Text="Wczytaj" Clicked="Wczytaj_Click" />
        </HorizontalStackLayout>

        <Label x:Name="Status" TextColor="Gray" />
    </VerticalStackLayout>

</ContentPage>

<!-- JsonPage.xaml (lista imion do JSON) -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="PlikiDemo.JsonPage"
             Title="Lista JSON">

    <VerticalStackLayout Padding="16" Spacing="10">
        <Entry x:Name="ImieEntry" Placeholder="Wpisz imię" />
        <Button Text="Dodaj" Clicked="Dodaj_Click" />

        <CollectionView x:Name="Lista" HeightRequest="200">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Label Text="{Binding}" FontSize="18" />
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

        <HorizontalStackLayout Spacing="8">
            <Button Text="Zapisz JSON" Clicked="ZapiszJson_Click" />
            <Button Text="Wczytaj JSON" Clicked="WczytajJson_Click" />
        </HorizontalStackLayout>
    </VerticalStackLayout>

</ContentPage>

TxtPage.xaml.cs + JsonPage.xaml.cs

// TxtPage.xaml.cs
namespace PlikiDemo;

public partial class TxtPage : ContentPage
{
    // Pełna ścieżka do pliku w bezpiecznym katalogu danych aplikacji
    string path = Path.Combine(FileSystem.AppDataDirectory, "notatka.txt");

    public TxtPage()
    {
        InitializeComponent();
    }

    private async void Zapisz_Click(object sender, EventArgs e)
    {
        await File.WriteAllTextAsync(path, NotatkaEditor.Text ?? "");
        Status.Text = "Zapisano plik.";
    }

    private async void Wczytaj_Click(object sender, EventArgs e)
    {
        if (File.Exists(path))
        {
            NotatkaEditor.Text = await File.ReadAllTextAsync(path);
            Status.Text = "Wczytano plik.";
        }
        else
        {
            Status.Text = "Plik nie istnieje (zapisz coś najpierw).";
        }
    }
}

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

// JsonPage.xaml.cs
using System.Text.Json;

namespace PlikiDemo;

public partial class JsonPage : ContentPage
{
    string path = Path.Combine(FileSystem.AppDataDirectory, "imiona.json");
    List<string> imiona = new();

    public JsonPage()
    {
        InitializeComponent();
    }

    private void Dodaj_Click(object sender, EventArgs e)
    {
        if (!string.IsNullOrWhiteSpace(ImieEntry.Text))
        {
            imiona.Add(ImieEntry.Text.Trim());

            // Wymuszenie odświeżenia CollectionView dla zwykłej List<T>
            Lista.ItemsSource = null;
            Lista.ItemsSource = imiona;
            ImieEntry.Text = "";
        }
    }

    private async void ZapiszJson_Click(object sender, EventArgs e)
    {
        var json = JsonSerializer.Serialize(imiona);
        await File.WriteAllTextAsync(path, json);
        await DisplayAlert("Informacja", "Zapisano dane do JSON.", "OK");
    }

    private async void WczytajJson_Click(object sender, EventArgs e)
    {
        if (File.Exists(path))
        {
            var json = await File.ReadAllTextAsync(path);
            imiona = JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
            Lista.ItemsSource = null;
            Lista.ItemsSource = imiona;
        }
        else
        {
            await DisplayAlert("Informacja", "Plik nie istnieje - zapisz dane najpierw.", "OK");
        }
    }
}

Komentarz i wyjaśnienie kodu

Obie strony korzystają z tej samej zasady wyznaczania ścieżki: Path.Combine(FileSystem.AppDataDirectory, "nazwa.ext")Path.Combine automatycznie dobiera poprawny separator ścieżek dla aktualnej platformy, więc ten sam kod C# działa bez zmian na Androidzie, iOS i Windows.

W TxtPage pole path jest inicjalizowane RAZ, jako pole klasy — obliczane przy starcie strony, a potem używane w obu metodach (Zapisz_Click, Wczytaj_Click) bez ponownego wyliczania. Metody obsługi zdarzeń są oznaczone async void, bo wywołują operacje z await (asynchroniczne odczyty/zapisy plików) — to standardowy wzorzec w MAUI dla handlerów zdarzeń wykonujących operacje We/Wy.

W JsonPage zwróć uwagę na dwuetapowy proces: (1) Dodaj_Click dodaje element TYLKO do listy w pamięci (RAM) i odświeża widok — dane NIE są jeszcze zapisane na dysku; (2) dopiero kliknięcie "Zapisz JSON" faktycznie utrwala całą listę w pliku. To rozróżnienie (zmiana w pamięci vs. zmiana na dysku) jest kluczowe do zrozumienia — użytkownik MUSI kliknąć zapis, inaczej dane znikną po zamknięciu aplikacji.

Ćwiczenie samodzielne

Utwórz nowy projekt MAUI z dwiema stronami jak w przykładzie (Notatnik TXT i Lista JSON). Przetestuj pełny cykl: wpisz notatkę → zapisz → zamknij aplikację całkowicie (nie tylko zminimalizuj) → uruchom ponownie → wczytaj. Sprawdź, czy treść się zgadza.

Zadania do pracy własnej

  1. Dodaj do notatnika TXT przycisk "Wyczyść", który czyści zarówno pole Editor, jak i usuwa plik z dysku (File.Delete(path), poprzedzone sprawdzeniem File.Exists).

  2. Rozbuduj listę JSON o możliwość usuwania pojedynczej pozycji (np. przez dodatkowy przycisk przy każdym elemencie w szablonie CollectionView, analogicznie do wzorca z poprzedniej lekcji z BindingContext) — po usunięciu z listy w pamięci pamiętaj o ponownym zapisaniu całości do pliku JSON.

  3. Zamiast listy zwykłych stringów, zapisz i odczytaj w JSON listę OBIEKTÓW własnej klasy (np. Zadanie z polami Tytul i Wykonane typu bool) — lista prostych zadań do zrobienia z możliwością odznaczenia jako wykonane, w pełni zapisywana i wczytywana z pliku JSON przy starcie i zamknięciu aplikacji.

Typowe błędy

Próba zapisu w dowolnym, "wymyślonym" folderze (np. C:dane albo ścieżka bezwzględna z komputera dewelopera) — na urządzeniu mobilnym taki folder zwykle nie istnieje albo aplikacja nie ma do niego uprawnień; zawsze FileSystem.AppDataDirectory.

Odczyt pliku bez wcześniejszego sprawdzenia File.Exists — przy pierwszym uruchomieniu aplikacji plik jeszcze nie istnieje, a próba jego odczytania bez tego sprawdzenia rzuci wyjątek i zamknie aplikację (crash).

Zapominanie o odświeżeniu ItemsSource przy zwykłej List<T> — w przeciwieństwie do ObservableCollection z poprzedniej lekcji, zwykła lista nie powiadamia automatycznie o zmianach; bez ręcznego ItemsSource = null; ItemsSource = lista; nowo dodane elementy nie pojawią się na ekranie.

Mylenie zmiany w pamięci ze trwałym zapisem — dodanie elementu do listy w RAM nie oznacza automatycznego zapisania go na dysku; bez wywołania metody zapisującej (np. po kliknięciu osobnego przycisku, albo automatycznie po każdej zmianie) dane znikną przy zamknięciu aplikacji.

Nawiązanie do egzaminu zawodowego

To bezpośrednia realizacja wymagania INF.04.6.2 "przechowywanie danych" — jeden z najczęściej sprawdzanych elementów na egzaminie zawodowym w części dotyczącej aplikacji mobilnych. Ten sam mechanizm plikowy (JSON) wykorzystasz też w bardziej złożonych projektach z tego działu, zanim w kolejnych lekcjach poznasz prawdziwą, relacyjną bazę danych SQLite — naturalny, stopniowy postęp od najprostszego trwałego zapisu do pełnej bazy danych.