Zaawansowane projektowanie aplikacji mobilnych
Rok akademicki: 2026/27
Projekt semestralny: Framework .NET MAUI, ekosystem C# i projekt "Stop Scout"
Prowadzący: Tomasz Goluch
Wersja: 1.0
Wprowadzenie
Cel: Przekazanie podstawowych informacji o zasobach laboratorium oraz specyfice środowiska wieloplatformowego .NET.
Laboratorium odbywa się na maszynach fizycznych (komputery laboratoryjne lub własne laptopy) oraz fizycznych smartfonach (z systemem Android lub iOS) bądź emulatorach systemowych. Wykorzystujemy ekosystem .NET MAUI (Multi-platform App UI), który kompiluje wspólny kod w języku C# oraz widoki XAML bezpośrednio do natywnych kontrolek systemów Android, iOS, macOS i Windows.
Instalacja środowiska i uruchomienie projektu
Cel: Konfiguracja narzędzi .NET SDK, obciążenia MAUI oraz wygenerowanie szablonu startowego.
⚠️ WAŻNE WYMAGANIE TECHNICZNE: Do wykonania tej instrukcji i testowania aplikacji na bieżąco, Twój komputer oraz fizyczny telefon (smartfon) muszą znajdować się w tej samej lokalnej sieci Wi-Fi. Upewnij się, że oba urządzenia są podłączone do tej samej sieci przed przejściem do kolejnych kroków.
Wymagane jest środowisko .NET 8.0 SDK (lub nowsze LTS) oraz Visual Studio 2022 (z zaznaczonym pakietem Programowanie aplikacji wieloplatformowych dla interfejsu .NET MAUI) lub Visual Studio Code z oficjalnym rozszerzeniem C# Dev Kit i .NET MAUI extension.
Weryfikacja instalacji środowiska:
Otwórz terminal (PowerShell) i sprawdź dostępność obciążenia MAUI:
dotnet --version
dotnet workload list
Jeśli obciążenie MAUI nie jest zainstalowane, wykonaj polecenie w terminalu administratora:
dotnet workload install maui
Generowanie projektu:
Tworzymy nową aplikację z architekturą opartą o
AppShell:
dotnet new maui -n StopScoutMobile
cd StopScoutMobile
Podłączenie fizycznego smartfona (Android):
- W telefonie wejdź w Ustawienia → Informacje o telefonie i kliknij 7 razy w Numer kompilacji, aby odblokować Opcje programisty.
- Włącz Debugowanie USB (USB Debugging).
- Podłącz telefon kablem do komputera i zatwierdź monit autoryzacji klucza RSA na ekranie smartfona.
- Sprawdź widoczność urządzenia w konsoli:
adb devices - Uruchom aplikację bezpośrednio na telefonie:
dotnet build -t:Run -f net8.0-android
💡 Rozwiązywanie problemów: Błędy wdrażania ADB i zawieszenie kompilatora (Kliknij, aby rozwinąć/zwinąć)
- Błąd
Device unauthorized: Odłącz kabel USB, w opcjach programisty kliknij Cofnij autoryzacje debugowania USB, podłącz ponownie i zaznacz Zawsze zezwalaj z tego komputera. - Blokada plików w bin/obj: Jeśli kompilator zgłasza błąd dostępu do plików procesów MSBuild, zamknij edytor i uruchom w terminalu:
dotnet clean dotnet build -f net8.0-android - Debugowanie przez lokalne Wi-Fi (bez kabla): Jeśli telefon i komputer są w tej samej sieci Wi-Fi, po jednorazowym podpięciu kablem wpisz:
Od tego momentu kabel USB można odłączyć.adb tcpip 5555 adb connect ADRES_IP_TELEFONU:5555
Rekomendowane narzędzia i rozszerzenia IDE
- Visual Studio 2022: Zapewnia natywny podgląd XAML Live Preview oraz XAML Hot Reload i C# Hot Reload (zmiany w kodzie logiki i interfejsu widoczne na telefonie bez restartu aplikacji).
- Visual Studio Code:
- C# Dev Kit (
ms-dotnettools.csdevkit) – oficjalne środowisko uruchomieniowe i kompilator języka C#. - .NET MAUI (
ms-dotnettools.dotnet-maui) – integracja z urządzeniami fizycznymi i debugerem Android/iOS.
- C# Dev Kit (
Struktura projektu i Clean Architecture
Cel: Refaktoryzacja domyślnego układu pod kątem czystej separacji odpowiedzialności biznesowej od platformy mobilnej.
Wygenerowane rozwiązanie posiada katalog Platforms/
zawierający specyficzne implementacje natywne. Aby zachować niezależność
logiki biznesowej, w projekcie wydzielamy cztery kluczowe warstwy:
StopScoutMobile/
├── Domain/ # WARSTWA DOMENY (Brak zależności od MAUI i UI)
│ ├── Entities/ # Czyste modele C# (np. Stop, Departure, User)
│ └── Repositories/ # Kontrakty interfejsów (np. IStopRepository)
├── UseCases/ # PRZYPADKI UŻYCIA (Czysta logika aplikacyjna)
│ ├── GetLiveDeparturesUseCase.cs
│ └── RegisterUserUseCase.cs
├── Infrastructure/ # INFRASTRUKTURA (Konkretna technologia)
│ ├── Api/ # Klient HTTP ZTM Gdańsk
│ └── Storage/ # Baza danych SQLite (sqlite-net-pcl)
└── Presentation/ # PREZENTACJA (MVVM / UI)
├── Views/ # Ekrany XAML i komponenty (ContentView)
└── ViewModels/ # Modele widoku z CommunityToolkit.Mvvm
Tworzenie pierwszego komponentu (Deklaratywne UI w XAML)
Cel: Zapoznanie z deklaratywnym komponentem
wielokrotnego użytku (ContentView).
Tworzymy komponent reprezentujący kartę pojedynczego odjazdu
autobusu. Utwórz plik
Presentation/Views/DepartureCardView.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentView x:Class="StopScoutMobile.Presentation.Views.DepartureCardView" x:Name="ThisCard" xmlns="[http://schemas.microsoft.com/dotnet/2021/maui](http://schemas.microsoft.com/dotnet/2021/maui)" xmlns:x="[http://schemas.microsoft.com/winfx/2009/xaml](http://schemas.microsoft.com/winfx/2009/xaml)">
<Frame BackgroundColor="White" BorderColor="#E5E7EB" CornerRadius="8" HasShadow="True" Margin="0,0,0,12" Padding="16">
<Grid ColumnDefinitions="50, *" ColumnSpacing="12" VerticalOptions="Center">
<!-- Badge numeru linii -->
<Border BackgroundColor="#DBEAFE" Grid.Column="0" HeightRequest="40" HorizontalOptions="Center" Stroke="#BFDBFE" StrokeShape="RoundRectangle 20" VerticalOptions="Center" WidthRequest="40">
<Label FontAttributes="Bold" FontSize="16" HorizontalOptions="Center" Text="{Binding Source={x:Reference ThisCard}, Path=RouteId}" TextColor="#1D4ED8" VerticalOptions="Center"/>
</Border>
<!-- Szczegóły kierunku i czasu -->
<VerticalStackLayout Grid.Column="1" VerticalOptions="Center">
<Label FontAttributes="Bold" FontSize="16" Text="{Binding Source={x:Reference ThisCard}, Path=Headsign}" TextColor="#374151"/>
<HorizontalStackLayout Margin="0,4,0,0" Spacing="6">
<Label FontSize="12" Text="{Binding Source={x:Reference ThisCard}, Path=TheoreticalTime, StringFormat='Planowo: {0}'}" TextColor="#6B7280"/>
<Label FontAttributes="Bold" FontSize="12" IsVisible="{Binding Source={x:Reference ThisCard}, Path=IsDelayed}" Text="{Binding Source={x:Reference ThisCard}, Path=DelayText}" TextColor="#DC2626"/>
</HorizontalStackLayout>
</VerticalStackLayout>
</Grid>
</Frame>
</ContentView>
W pliku code-behind
Presentation/Views/DepartureCardView.xaml.cs definiujemy
właściwości wiązane (BindableProperty):
namespace StopScoutMobile.Presentation.Views;
public partial class DepartureCardView : ContentView
{
public static readonly BindableProperty RouteIdProperty =
BindableProperty.Create(nameof(RouteId), typeof(string), typeof(DepartureCardView), string.Empty);
public static readonly BindableProperty HeadsignProperty =
BindableProperty.Create(nameof(Headsign), typeof(string), typeof(DepartureCardView), string.Empty);
public static readonly BindableProperty TheoreticalTimeProperty =
BindableProperty.Create(nameof(TheoreticalTime), typeof(string), typeof(DepartureCardView), string.Empty);
public static readonly BindableProperty DelayInSecondsProperty =
BindableProperty.Create(nameof(DelayInSeconds), typeof(int?), typeof(DepartureCardView), null,
propertyChanged: OnDelayChanged);
public string RouteId
{
get => (string)GetValue(RouteIdProperty);
set => SetValue(RouteIdProperty, value);
}
public string Headsign
{
get => (string)GetValue(HeadsignProperty);
set => SetValue(HeadsignProperty, value);
}
public string TheoreticalTime
{
get => (string)GetValue(TheoreticalTimeProperty);
set => SetValue(TheoreticalTimeProperty, value);
}
public int? DelayInSeconds
{
get => (int?)GetValue(DelayInSecondsProperty);
set => SetValue(DelayInSecondsProperty, value);
}
public bool IsDelayed => DelayInSeconds.HasValue && DelayInSeconds.Value > 0;
public string DelayText => DelayInSeconds.HasValue ? $"(+{DelayInSeconds.Value}s)" : string.Empty;
public DepartureCardView()
{
InitializeComponent();
}
private static void OnDelayChanged(BindableObject bindable, object oldValue, object newValue)
{
var card = (DepartureCardView)bindable;
card.OnPropertyChanged(nameof(IsDelayed));
card.OnPropertyChanged(nameof(DelayText));
}
}
Stan Aplikacji i Renderowanie List (CollectionView & MVVM)
Cel: Zastosowanie wzorca Model-View-ViewModel z
biblioteką CommunityToolkit.Mvvm oraz wydajnej
wirtualizacji listy (CollectionView).
Zainstaluj pakiet ułatwiający implementację MVVM:
dotnet add package CommunityToolkit.Mvvm
1. Model Danych
(Domain/Entities/DepartureItem.cs):
namespace StopScoutMobile.Domain.Entities;
public record DepartureItem(string Id, string RouteId, string Headsign, string TheoreticalTime, int? DelayInSeconds);
2. Model Widoku
(Presentation/ViewModels/HomeViewModel.cs):
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using StopScoutMobile.Domain.Entities;
namespace StopScoutMobile.Presentation.ViewModels;
public partial class HomeViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<DepartureItem> departures = new();
public HomeViewModel()
{
LoadMockDepartures();
}
private void LoadMockDepartures()
{
Departures = new ObservableCollection<DepartureItem>
{
new("1", "9", "Strzyża PKM", "19:09", 12),
new("2", "2", "Jelitkowo", "19:15", 0),
new("3", "12", "Ujeścisko", "19:22", 150),
new("4", "6", "Łostowice Świętokrzyska", "19:27", null),
new("5", "8", "Stogi Plaża", "19:31", 45),
new("6", "10", "Brętowo PKM", "19:36", 210),
new("7", "11", "Lawendowe Wzgórze", "19:42", 0),
new("8", "5", "Oliwa", "19:48", 85),
new("9", "199", "Wały Piastowskie", "19:53", 130)
};
}
}
3. Widok Główny (MainPage.xaml): Zastąp
dotychczasową zawartość MainPage.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage BackgroundColor="#F9FAFB" x:Class="StopScoutMobile.MainPage" xmlns="[http://schemas.microsoft.com/dotnet/2021/maui](http://schemas.microsoft.com/dotnet/2021/maui)" xmlns:views="clr-namespace:StopScoutMobile.Presentation.Views" xmlns:vm="clr-namespace:StopScoutMobile.Presentation.ViewModels" xmlns:x="[http://schemas.microsoft.com/winfx/2009/xaml](http://schemas.microsoft.com/winfx/2009/xaml)">
<ContentPage.BindingContext>
<vm:HomeViewModel />
</ContentPage.BindingContext>
<Grid Padding="16" RowDefinitions="Auto, *">
<Label FontAttributes="Bold" FontSize="20" Grid.Row="0" Margin="0,0,0,16" Text="Najbliższe odjazdy (Mock)" TextColor="#1F2937"/>
<!-- CollectionView z recyklingiem widoków -->
<CollectionView Grid.Row="1" ItemSizingStrategy="MeasureFirstItem" ItemsSource="{Binding Departures}">
<CollectionView.ItemTemplate>
<DataTemplate>
<views:DepartureCardView RouteId="{Binding RouteId}"
Headsign="{Binding Headsign}"
TheoreticalTime="{Binding TheoreticalTime}"
DelayInSeconds="{Binding DelayInSeconds}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
Integracja Map (WebView + Leaflet OpenStreetMap)
Cel: Wizualizacja przestrzenna w środowisku wieloplatformowym bez konieczności rejestrowania komercyjnych kluczy API Google Cloud.
Kontrolka WebView w .NET MAUI pozwala na renderowanie
kodu HTML z biblioteką Leaflet. Tworzymy nowy widok
Presentation/Views/MapPage.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage Title="Mapa Przystanków" x:Class="StopScoutMobile.Presentation.Views.MapPage" xmlns="[http://schemas.microsoft.com/dotnet/2021/maui](http://schemas.microsoft.com/dotnet/2021/maui)" xmlns:x="[http://schemas.microsoft.com/winfx/2009/xaml](http://schemas.microsoft.com/winfx/2009/xaml)">
<WebView HorizontalOptions="Fill" VerticalOptions="Fill" x:Name="MapWebView"/>
</ContentPage>
W pliku code-behind Presentation/Views/MapPage.xaml.cs
generujemy podkład OpenStreetMap:
namespace StopScoutMobile.Presentation.Views;
public partial class MapPage : ContentPage
{
public MapPage()
{
InitializeComponent();
LoadLeafletMap();
}
private void LoadLeafletMap()
{
var htmlSource = new HtmlWebViewSource
{
Html = @"
<!DOCTYPE html>
<html>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' />
<link rel='stylesheet' href='[https://unpkg.com/leaflet@1.9.4/dist/leaflet.css](https://unpkg.com/leaflet@1.9.4/dist/leaflet.css)' />
<script src='[https://unpkg.com/leaflet@1.9.4/dist/leaflet.js](https://unpkg.com/leaflet@1.9.4/dist/leaflet.js)'></script>
<style>
body, html, #map { margin: 0; padding: 0; height: 100%; width: 100%; }
</style>
</head>
<body>
<div id='map'></div>
<script>
var map = L.map('map').setView([54.352, 18.646], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map);
var marker = L.marker([54.352, 18.646]).addTo(map);
marker.bindPopup('<b>Dworzec Główny</b><br>Węzeł przesiadkowy').openPopup();
</script>
</body>
</html>"
};
MapWebView.Source = htmlSource;
}
}
Konfiguracja zakładek
(AppShell.xaml):
Skonfiguruj pasek dolnych zakładek w pliku
AppShell.xaml:
<Shell x:Class="StopScoutMobile.AppShell" xmlns="[http://schemas.microsoft.com/dotnet/2021/maui](http://schemas.microsoft.com/dotnet/2021/maui)" xmlns:local="clr-namespace:StopScoutMobile" xmlns:views="clr-namespace:StopScoutMobile.Presentation.Views" xmlns:x="[http://schemas.microsoft.com/winfx/2009/xaml](http://schemas.microsoft.com/winfx/2009/xaml)">
<TabBar>
<Tab Icon="home_icon.png" Title="Odjazdy">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}"/>
</Tab>
<Tab Icon="map_icon.png" Title="Mapa">
<ShellContent ContentTemplate="{DataTemplate views:MapPage}"/>
</Tab>
</TabBar>
</Shell>
Zadanie Laboratoryjne do samodzielnego wykonania
Celem zadania jest stworzenie kompletnej aplikacji mobilnej "Stop Scout" w środowisku .NET MAUI służącej do monitorowania rzeczywistych czasów przyjazdów pojazdów komunikacji miejskiej w Gdańsku w oparciu o Otwarte Dane ZTM Gdańsk.
Aplikacja musi zapewniać pełną funkcjonalność w warunkach terenowych (brak sieci) dzięki wykorzystaniu lokalnej bazy danych SQLite.
1. Uwierzytelnianie i lokalna baza danych (Offline-First)
- Zaimplementuj ekran rejestracji i logowania użytkownika. Hasło
użytkownika musi być bezpiecznie hashowane przy użyciu biblioteki
BCrypt.Net-Next. - Do trwałego przechowywania danych na telefonie wykorzystaj lokalną
bazę relacyjną SQLite (
sqlite-net-pcl). - W bazie przechowuj konta użytkowników, zbuforowaną listę wszystkich przystanków oraz przypisane do danego profilu ulubione przystanki wraz z notatką tekstową.
💡 Wskazówki implementacyjne: Clean Architecture, SQLite i BCrypt (Kliknij, aby rozwinąć/zwinąć)
Instalacja pakietów NuGet:
dotnet add package sqlite-net-pcl dotnet add package SQLitePCLRaw.bundle_green dotnet add package BCrypt.Net-NextWarstwa Domeny (
Domain/):- Encja użytkownika (
Domain/Entities/User.cs):using SQLite; namespace StopScoutMobile.Domain.Entities; [Table("Users")] public class User { [PrimaryKey] public string Id { get; set; } = Guid.NewGuid().ToString(); [Unique, Indexed] public string Username { get; set; } = string.Empty; public string PasswordHash { get; set; } = string.Empty; } - Interfejs repozytorium
(
Domain/Repositories/IAuthRepository.cs):public interface IAuthRepository { Task<User?> FindByUsernameAsync(string username); Task CreateUserAsync(User user); }
- Encja użytkownika (
Warstwa Infrastruktury (
Infrastructure/Storage/SqliteAuthRepository.cs): Wykorzystaj asynchroniczne API bazy danych:using SQLite; using StopScoutMobile.Domain.Entities; using StopScoutMobile.Domain.Repositories; namespace StopScoutMobile.Infrastructure.Storage; public class SqliteAuthRepository : IAuthRepository { private readonly SQLiteAsyncConnection _database; public SqliteAuthRepository() { var dbPath = Path.Combine(FileSystem.AppDataDirectory, "stopscout.db3"); _database = new SQLiteAsyncConnection(dbPath); _database.CreateTableAsync<User>().Wait(); } public Task<User?> FindByUsernameAsync(string username) => _database.Table<User>().Where(u => u.Username == username).FirstOrDefaultAsync(); public Task CreateUserAsync(User user) => _database.InsertAsync(user); }Warstwa Przypadków Użycia (
UseCases/):RegisterUserUseCase.cs: Sprawdza unikalność nazwy użytkownika, hashuje hasło za pomocąBCrypt.Net.BCrypt.HashPassword(password)i zapisuje encję w repozytorium.LoginUserUseCase.cs: Pobiera użytkownika i weryfikuje skrót:BCrypt.Net.BCrypt.Verify(password, user.PasswordHash).
Nawigacja po zalogowaniu (
Shell): Po udanym logowaniu przejdź do widoku głównego:await Shell.Current.GoToAsync("//MainPage");
2. Pobieranie danych z API ZTM i zarządzanie stanem
- Lista wszystkich przystanków (do pobrania i zbuforowania
lokalnie):
https://ckan.multimediagdansk.pl/.../stops.json - Estymowane czasy przyjazdów dla danego słupka przystankowego
(
stopId):- Szablon zapytania:
https://ckan2.multimediagdansk.pl/departures?stopId={stopId} - Przykładowy odnośnik (Dworzec Główny,
stopId=2072): https://ckan2.multimediagdansk.pl/departures?stopId=2072
- Szablon zapytania:
- Filtrowanie odjazdów: Na liście odjazdów nie
wyświetlaj pojazdów, które odjechały dawniej niż 2 minuty temu względem
aktualnego czasu (weryfikacja pól
estimatedTimelubtheoreticalTime). - Zastosuj technikę Optimistic UI – dodanie/usunięcie
przystanku w Ulubionych natychmiast modyfikuje kolekcję
ObservableCollection<T>w ViewModelu, a operacja zapisu do SQLite odbywa się w zadaniu asynchronicznym w tle.
💡 Wskazówki implementacyjne: HttpClient, buforowanie i Optimistic UI (Kliknij, aby rozwinąć/zwinąć)
Rejestracja
HttpClientwMauiProgram.cs:builder.Services.AddHttpClient("ZtmClient", client => { client.Timeout = TimeSpan.FromSeconds(10); });Warstwa Przypadków Użycia (
UseCases/GetLiveDeparturesUseCase.cs): Odfiltrowanie odjazdów starszych niż 2 minuty w C#:var now = DateTime.Now.TimeOfDay; var filtered = departures.Where(dep => { var timeStr = dep.EstimatedTime ?? dep.TheoreticalTime; if (!TimeSpan.TryParse(timeStr, out var depTime)) return true; var diff = (depTime - now).TotalMinutes; return diff >= -2.0; }).ToList();Wzorzec Optimistic UI w ViewModelu:
[RelayCommand] public async Task AddFavoriteOptimisticAsync(Stop stop) { // 1. Natychmiastowa aktualizacja interfejsu FavoriteStops.Add(stop); try { // 2. Asynchroniczny zapis do SQLite w tle await _stopRepository.SaveFavoriteAsync(stop); } catch (Exception ex) { // 3. Rollback w przypadku błędu FavoriteStops.Remove(stop); await Shell.Current.DisplayAlert("Błąd", "Nie udało się zapisać przystanku", "OK"); } }
3.
Komponent wyróżniania opóźnień (DelayHighlightView)
- Stwórz dedykowany komponent XAML wizualizujący status opóźnienia na
podstawie wartości pola
delayInSeconds:- Wartość
0lub ujemna: zielony napis "Na czas". - Opóźnienie poniżej 2 minut (
< 120s): żółty napis z liczbą sekund, np. "+90s". - Opóźnienie 2 minuty lub większe: czerwony, pogrubiony napis z minutami (np. "+3 min").
- Wartość
- Animacja ostrzegawcza: Dla opóźnień wynoszących co
najmniej 2 minuty tekst powinien cyklicznie pulsować. Zaimplementuj
zapętloną animację przezroczystości (
FadeTo) dostępną w silniku MAUI.
💡 Wskazówki implementacyjne: Logika domenowa i animacje MAUI (Kliknij, aby rozwinąć/zwinąć)
Warstwa Domeny (
Domain/Utils/DelayFormatter.cs): Czysta, w pełni testowalna logika bez zależności od MAUI:namespace StopScoutMobile.Domain.Utils; public enum DelaySeverity { OnTime, Minor, Major } public record DelayInfo(DelaySeverity Severity, string FormattedText, bool ShouldAnimate); public static class DelayFormatter { public static DelayInfo Format(int? delayInSeconds) { var delay = delayInSeconds ?? 0; if (delay <= 0) return new DelayInfo(DelaySeverity.OnTime, "Na czas", false); if (delay < 120) return new DelayInfo(DelaySeverity.Minor, $"+{delay}s", false); var minutes = delay / 60; return new DelayInfo(DelaySeverity.Major, $"+{minutes} min", true); } }Animacja pulsowania w Code-Behind komponentu: Uruchomienie asynchronicznej pętli animacji MAUI z tokenem anulowania (
CancellationToken):private CancellationTokenSource? _cts; private void ApplyDelay(int? delay) { _cts?.Cancel(); var info = DelayFormatter.Format(delay); DelayLabel.Text = info.FormattedText; DelayLabel.TextColor = info.Severity switch { DelaySeverity.OnTime => Color.FromArgb("#16A34A"), DelaySeverity.Minor => Color.FromArgb("#D97706"), DelaySeverity.Major => Color.FromArgb("#DC2626"), _ => Colors.Gray }; if (info.ShouldAnimate) { _cts = new CancellationTokenSource(); _ = StartPulsingAsync(_cts.Token); } else { DelayLabel.Opacity = 1.0; } } private async Task StartPulsingAsync(CancellationToken token) { while (!token.IsCancellationRequested) { await DelayLabel.FadeTo(0.2, 700, Easing.SinInOut); await DelayLabel.FadeTo(1.0, 700, Easing.SinInOut); } }
4. Interaktywna Mapa Przystanków
- W osobnej zakładce aplikacji umieść pełnoekranową mapę wyśrodkowaną na Gdańsk.
- Nanieś na mapę przystanki w postaci pinezek. Renderuj pinezki dopiero po odpowiednim przybliżeniu widoku (bounding box).
- Kliknięcie pinezki powinno otwierać dymek informacyjny z nazwą przystanku, numerem słupka oraz przyciskiem dodającym ten punkt do listy ulubionych.
💡 Wskazówki implementacyjne: Komunikacja JavaScript-to-C# przez WebView (Kliknij, aby rozwinąć/zwinąć)
Optymalizacja zapytania przestrzennego w SQLite:
public Task<List<Stop>> GetVisibleStopsAsync(double minLat, double maxLat, double minLon, double maxLon) => _database.Table<Stop>() .Where(s => s.Lat >= minLat && s.Lat <= maxLat && s.Lon >= minLon && s.Lon <= maxLon) .ToListAsync();Odbiór zdarzenia kliknięcia pinezki w .NET MAUI: W silniku Leaflet wewnątrz
WebViewużyj przekierowania URL (schemat Custom URL):function handleAddFavorite(stopId, name, lat, lon) { window.location.href = 'stopscout://favorite?stopId=' + stopId + '&name=' + encodeURIComponent(name); }W C# obsłuż zdarzenie
NavigatingkontrolkiWebView:private void OnWebViewNavigating(object sender, WebNavigatingEventArgs e) { if (e.Url.StartsWith("stopscout://favorite")) { e.Cancel = true; // Zablokuj faktyczną nawigację przeglądarki var uri = new Uri(e.Url); var query = System.Web.HttpUtility.ParseQueryString(uri.Query); var stopId = int.Parse(query["stopId"]!); var name = query["name"]!; // Wywołanie akcji w ViewModelu / UseCase MainThread.BeginInvokeOnMainThread(async () => { await _viewModel.AddFavoriteByIdAsync(stopId, name); }); } }
Kryteria oceny projektu (100 pkt)
| Moduł | Wymagania | Punkty |
|---|---|---|
| 1. Autoryzacja i SQLite | Rejestracja, hashowanie hasła
(BCrypt.Net-Next), logowanie, buforowanie danych offline w
relacyjnej bazie sqlite-net-pcl. |
25 pkt |
| 2. API ZTM & MVVM | Pobieranie przystanków i odjazdów z gdańskiego API, filtrowanie odjazdów starszych niż 2 min, stan w ViewModelu z obsługą Optimistic UI. | 25 pkt |
3. Komponent
DelayHighlightView |
Poprawna kategoryzacja opóźnień, cykliczna
animacja przezroczystości (FadeTo), separacja czystej
domeny. |
20 pkt |
| 4. Interaktywna Mapa | Wyświetlanie mapy, pinezki renderowane po przybliżeniu (bounding box), dymek z akcją dodawania do ulubionych przez mostek WebView. | 20 pkt |
| 5. Architektura i Kod | Podział na warstwy Domain,
UseCases, Infrastructure,
Presentation, Dependency Injection, brak błędów w
kompilacji. |
10 pkt |