Zaawansowane projektowanie aplikacji mobilnych

Rok akademicki: 2026/27

Projekt zespołowy: Wieloplatformowe aplikacje mobilne Offline-First w ekosystemie Expo

Prowadzący: Tomasz Goluch Wersja: 1.0


Wprowadzenie i organizacja zajęć

Cel: Zaprojektowanie, implementacja, przetestowanie oraz wdrożenie produkcyjnego instalatora wieloplatformowej aplikacji mobilnej w paradygmacie Offline-First, opartej o ekosystem Expo (React Native) oraz Czystą Architekturę (Clean Architecture).

Projekt realizowany jest w zespołach 3–4 osobowych w wymiarze 8 godzin dydaktycznych podzielonych na dwa bloki stacjonarne oraz asynchroniczną pracę projektową w repozytorium Git:

  1. Spotkanie 1 (4h lekcyjne – Wprowadzenie i warsztat):
    • Omówienie założeń architektonicznych i standardów inżynierskich na bazie Instrukcji laboratoryjnej "Stop Scout".
    • Formowanie zespołów, wybór wariantu domenowego oraz formalny podział ról.
    • Inicjalizacja repozytorium Git, konfiguracja reguł gałęzi, szablonów Pull Requestów oraz szkieletu katalogów Clean Architecture.
  2. Praca projektowa (Asynchroniczna realizacja w Git):
    • Implementacja warstw domeny, infrastruktury, bazy SQLite i interfejsu użytkownika.
    • Prowadzenie rewizji kodu (Code Review) w ramach zadań przypisanych do poszczególnych ról.
    • Implementacja automatycznych testów jednostkowych w Jest oraz wygenerowanie pliku instalacyjnego .apk.
  3. Spotkanie 2 (4h lekcyjne – Finałowy odbiór i ocena):
    • Prezentacja działania samodzielnego instalatora .apk na fizycznym telefonie (klonowanym na ekran przez scrcpy).
    • Weryfikacja działania w trybie offline (odcięcie łączności sieciowej).
    • Audyt inżynierski historii repozytorium Git i recenzji PR.
    • Weryfikacja indywidualnej znajomości kodu przez każdego członka zespołu.

💻 Prezentacja ekranu telefonu (scrcpy):
Podczas odbioru projektu wymagane jest udostępnianie ekranu fizycznego smartfona na monitorze komputera za pomocą narzędzia scrcpy (klonowanie obrazu przez kabel USB z zerowym opóźnieniem). Aplikacja musi działać w 100% autonomicznie jako zainstalowany pakiet .apk – serwer deweloperski Metro na komputerze musi pozostać wyłączony.


Role w zespole projektowym i kultura inżynierska

Cel: Równomierny podział odpowiedzialności technicznej, eliminacja konfliktów scalania (merge conflicts) oraz egzekwowanie dobrych praktyk inżynierii oprogramowania.

Czysta Architektura w sposób naturalny separuje warstwy systemu na niezależne moduły. Każdy student obejmuje jedną z poniższych ról technicznych i odpowiada za powierzony mu obszar:

┌────────────────────────────────────────────────────────────────────────┐
│                      STRUKTURA ODPOWIEDZIALNOŚCI                       │
├────────────────────────────────────────────────────────────────────────┤
│  [UI & State Lead]         --> app/, components/, src/store/           │
│  [Domain & QA Lead]        --> src/domain/, src/use-cases/, __tests__/ │
│  [Data & Security Lead]    --> src/infrastructure/storage/, api/, auth │
│  [Hardware & DevOps Lead]  --> src/infrastructure/location/, webview,  │
│                                app.json, eas.json, CI/CD               │
└────────────────────────────────────────────────────────────────────────┘

1. Inżynier Domeny & Jakości (Domain & QA Lead)

2. Inżynier Danych & Bezpieczeństwa (Data & Security Lead)

3. Inżynier UI/UX & Reaktywności (UI & State Lead)

4. Inżynier Integracji Natywnej & DevOps (Hardware & DevOps Lead)

(W zespołach 3-osobowych obowiązki ról Data/Security oraz Hardware/DevOps zostają połączone).


Praca w Git i Rewizja Kodu (Code Review)

Cel: Wdrożenie profesjonalnego standardu pracy zespołowej eliminującego ryzyko nadpisywania kodu.

  1. Zasada ochrony gałęzi głównej:
    • Obowiązuje bezwzględny zakaz bezpośredniego wypychania zmian (git push) do gałęzi main.
    • Prace prowadzone są na gałęziach funkcyjnych tworzonych wg wzorca: feature/nazwa-modulu lub fix/opis-bledu.
  2. Konwencja Conventional Commits:
    • feat: – nowa funkcjonalność (np. feat(auth): dodanie haszowania bcryptjs z expo-crypto).
    • fix: – naprawa błędu (np. fix(storage): dodanie indeksu dla zapytań zakresowych).
    • refactor: – refaktoryzacja kodu bez zmiany logiki biznesowej (np. refactor(domain): wydzielenie kalkulatora haversine).
    • test: – dodanie lub modyfikacja testów (np. test(use-cases): weryfikacja filtrowania odjazdow z czasem zamrozonym).
    • chore: – zadania konfiguracyjne i DevOps (np. chore(eas): konfiguracja profilu apk w eas.json).
  3. Procedura Pull Request i Code Review:
    • Każde scalenie do main odbywa się wyłącznie poprzez Pull Request (PR).
    • Autor PR przygotowuje zwięzły opis wprowadzonych zmian oraz załącza zrzut ekranu lub log potwierdzający przejście testów.
    • Obowiązkowe Code Review: co najmniej jeden inny członek zespołu musi przejrzeć diff zmian w kodzie, zamieścić uwagi techniczne w liniach kodu i formalnie zatwierdzić wdrożenie (Approve).

Warianty domenowe (Otwarte Dane Gdańsk / TRISTAR)

Cel: Realizacja praktycznego scenariusza inżynierskiego opartego o darmowe, publiczne i stabilne API.

Każdy zespół wybiera jeden z poniższych tematów:

Wariant A: UrbanPulse / Stop Scout Pro (Smart Mobility & Crowdsourcing)

Wariant B: FieldService / StopAudit (Inspekcja i Audyt Infrastruktury Miejskiej)

Wariant C: EventScout / CityQuest (Grywalizacja Terenowa i Eksploracja POI)


Wymagania techniczne i wzorce architektoniczne

Cel: Implementacja pełnego stosu technologii mobilnych zgodnie z paradygmatem Czystej Architektury.

1. Czysta Architektura (Clean Architecture)

Struktura projektu musi ściśle izolować reguły biznesowe od frameworka i bibliotek zewnętrznych:

// Przykład czystej reguły biznesowej w src/domain/utils/distanceCalculator.ts
export function calculateDistanceInMeters(
  lat1: number,
  lon1: number,
  lat2: number,
  lon2: number
): number {
  const R = 6371e3;
  const phi1 = (lat1 * Math.PI) / 180;
  const phi2 = (lat2 * Math.PI) / 180;
  const deltaPhi = ((lat2 - lat1) * Math.PI) / 180;
  const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;

  const a =
    Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) +
    Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  return Math.round(R * c);
}

2. Relacyjna baza SQLite (Offline-First)

// Przykład bezpiecznej inicjalizacji w src/infrastructure/storage/SqliteIncidentRepository.ts
import * as SQLite from 'expo-sqlite';
import { IIncidentRepository } from '../../domain/repositories/IIncidentRepository';
import { Incident } from '../../domain/entities/Incident';

export class SqliteIncidentRepository implements IIncidentRepository {
  private dbPromise: Promise<SQLite.SQLiteDatabase> | null = null;

  private async getDb(): Promise<SQLite.SQLiteDatabase> {
    if (!this.dbPromise) {
      this.dbPromise = (async () => {
        const db = await SQLite.openDatabaseAsync('urbanpulse.db');
        await db.execAsync(`
          PRAGMA journal_mode = WAL;
          CREATE TABLE IF NOT EXISTS incidents (
            id TEXT PRIMARY KEY NOT NULL,
            stopId INTEGER NOT NULL,
            type TEXT NOT NULL,
            description TEXT NOT NULL,
            createdAt TEXT NOT NULL
          );
          CREATE INDEX IF NOT EXISTS idx_incidents_stopId ON incidents(stopId);
        `);
        return db;
      })();
    }
    return this.dbPromise;
  }

  async save(incident: Incident): Promise<void> {
    const db = await this.getDb();
    await db.runAsync(
      'INSERT INTO incidents (id, stopId, type, description, createdAt) VALUES (?, ?, ?, ?, ?)',
      [incident.id, incident.stopId, incident.type, incident.description, incident.createdAt]
    );
  }

  async getAllForStop(stopId: number): Promise<Incident[]> {
    const db = await this.getDb();
    return await db.getAllAsync<Incident>(
      'SELECT * FROM incidents WHERE stopId = ? ORDER BY createdAt DESC',
      [stopId]
    );
  }
}

3. Stan globalny Zustand z Optimistic UI

Zarządzanie stanem musi implementować technikę Optimistic UI – natychmiastowa modyfikacja stanu w pamięci RAM zapewnia 0 ms opóźnienia interfejsu, podczas gdy zapis do SQLite odbywa się asynchronicznie w tle. W przypadku awarii operacji dyskowej stan jest automatycznie przywracany (rollback).

// Przykład akcji Optimistic UI w src/store/useIncidentStore.ts
import { create } from 'zustand';
import { Incident } from '../domain/entities/Incident';
import { IIncidentRepository } from '../domain/repositories/IIncidentRepository';

interface IncidentState {
  incidents: Incident[];
  setIncidents: (items: Incident[]) => void;
  addIncidentOptimistic: (incident: Incident, repo: IIncidentRepository) => Promise<boolean>;
}

export const useIncidentStore = create<IncidentState>((set, get) => ({
  incidents: [],
  setIncidents: (items) => set({ incidents: items }),

  addIncidentOptimistic: async (incident, repo) => {
    const previous = get().incidents;
    set({ incidents: [incident, ...previous] }); // Natychmiastowa reakcja UI

    try {
      await repo.save(incident); // Asynchroniczny zapis SQLite w tle
      return true;
    } catch (error) {
      console.error('Błąd zapisu do SQLite. Rollback stanu...', error);
      set({ incidents: previous }); // Przywrócenie stanu pierwotnego
      return false;
    }
  },
}));

4. Sensory i Mapa (expo-location + WebView)

5. Wydajne Animacje Natywne (Animated)

Każda animacja cykliczna lub dynamiczna (np. pulsowanie ostrzegawcze, wskaźnik zbliżeniowy) musi bezwzględnie posiadać flagę useNativeDriver: true. Dzięki temu kalkulacja klatek animacji jest w całości zrzucona na procesor graficzny i wątek renderujący systemu (UI Thread), bez blokowania logiki JavaScriptu.

6. Testy Jednostkowe w Jest

// Przykład testu use-case'a z fałszywym zegarem w src/use-cases/__tests__/RegisterIncidentUseCase.test.ts
import { RegisterIncidentUseCase } from '../RegisterIncidentUseCase';
import { IIncidentRepository } from '../../domain/repositories/IIncidentRepository';
import { ILocationService } from '../../domain/repositories/ILocationService';

describe('RegisterIncidentUseCase', () => {
  let mockRepo: jest.Mocked<IIncidentRepository>;
  let mockGps: jest.Mocked<ILocationService>;
  let useCase: RegisterIncidentUseCase;

  beforeEach(() => {
    jest.useFakeTimers();
    jest.setSystemTime(new Date('2026-10-15T12:00:00Z'));

    mockRepo = {
      save: jest.fn().mockResolvedValue(undefined),
      getAllForStop: jest.fn(),
    };

    mockGps = {
      requestPermissions: jest.fn().mockResolvedValue(true),
      getCurrentPosition: jest.fn(),
    };

    useCase = new RegisterIncidentUseCase(mockRepo, mockGps);
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('odrzuca zgłoszenie incydentu, gdy dystans fizyczny przekracza 150m', async () => {
    mockGps.getCurrentPosition.mockResolvedValue({
      latitude: 54.371,
      longitude: 18.618,
    });

    await expect(
      useCase.execute({
        stopId: 2072,
        stopCoords: { latitude: 54.352, longitude: 18.646 },
        type: 'DELAY',
        description: 'Blokada toru',
      })
    ).rejects.toThrow('Zbyt duża odległość od obiektu (wymagane <= 150m).');

    expect(mockRepo.save).not.toHaveBeenCalled();
  });
});

7. Samodzielny instalator APK (EAS Build)


Harmonogram odbiorów i punkty kontrolne (Milestones)

Przedmiot obejmuje 8 godzin lekcyjnych podzielonych na dwa bloki po 4 godziny oraz asynchroniczny kamień milowy weryfikowany w repozytorium GitHub / platformie Moodle:

Etap Termin rozliczenia Weryfikowany zakres prac Punkty
Spotkanie 1
(Laboratorium 1)
Podczas 1. bloku zajęć
(4 godziny lekcyjne)
Kick-off projektu i fundamenty architektoniczne:
• Podział zespołu na role inżynierskie i wybór wariantu domenowego (A, B lub C).
• Inicjalizacja repozytorium Git, konfiguracja reguł ochrony gałęzi, Conventional Commits i szablonów PR.
• Implementacja szkieletu Clean Architecture (domain/, use-cases/, infrastructure/).
15 pkt
Kamień Milowy
(Asynchronicznie)
Przed Spotkaniem 2
(Rozliczenie na Moodle/Git)
Implementacja modułów, testy i kompilacja APK:
• Baza expo-sqlite (indeksy, Bounding Box, brak top-level await) i haszowanie haseł bcryptjs.
• Integracja REST API (CKAN/TRISTAR) i globalny magazyn Zustand (Optimistic UI z rollbackiem).
• Moduł GPS expo-location, mapa kafelkowa Leaflet w WebView i animacje Animated (useNativeDriver).
• Minimum 8 testów jednostkowych w Jest (fałszywy zegar, mocki repozytoriów).
• Samodzielny plik instalacyjny .apk wygenerowany przez EAS Build.
45 pkt
Spotkanie 2
(Laboratorium 2)
Podczas 2. bloku zajęć
(4 godziny lekcyjne)
Finałowy odbiór, audyt inżynierski i weryfikacja kodu:
• Prezentacja działania samodzielnego pliku .apk na telefonie (narzędzie scrcpy), test trybu offline (samolotowego).
• Audyt repozytorium Git: wkład autorów, jakość i merytoryczność dyskusji w Code Review.
• Indywidualna weryfikacja znajomości kodu każdego członka zespołu (z procedurą ratunkową).
40 pkt
SUMA 100 pkt

Procedura odbioru końcowego i weryfikacja znajomości kodu (Spotkanie 2)

Spotkanie 2 jest w całości przeznaczone na weryfikację projektów, obronę oraz wystawienie ocen w protokole. Każdy zespół przystępuje do 20-minutowego odbioru na stanowisku laboratoryjnym:

1. Prezentacja aplikacji Standalone APK (5 minut)

2. Audyt repozytorium Git i architektury (5 minut)

3. Indywidualna weryfikacja znajomości kodu (10 minut)

Prowadzący przeprowadza szczegółowe odpytanie każdego członka zespołu z zakresu kodu, za który odpowiadał w ramach przypisanej roli. Weryfikowana jest znajomość przepływu danych, zrozumienie wzorców projektowych, obsługa stanów brzegowych oraz mechanika integracji komponentów.

⚠️ Procedura warunkowa (Zadanie ratunkowe w przypadku braku zrozumienia kodu)

W przypadku, gdy student wykaże rażący brak zrozumienia kodu, pod którym się podpisał (nie potrafi wyjaśnić działania funkcji, przepływu stanu lub zapytań SQL), nie otrzymuje natychmiastowej oceny niedostatecznej. Student ma możliwość obronienia oceny poprzez natychmiastowe doimplementowanie na żywo nowej mikrofunkcjonalności przed prowadzącym, odpowiadającej przypisanej mu roli w zespole:


Kryteria oceny projektu (100 pkt)

Obszar oceny Weryfikowany zakres techniczny Punkty
1. Architektura & TypeScript Ścisłe przestrzeganie Clean Architecture (brak importów UI/bazy w domenie), dependency inversion w use-cases, pełne typowanie TypeScript (brak any). 20 pkt
2. Baza SQLite & Offline-First Brak blokującego top-level await, optymalny schemat (indeksy, tryb WAL), zapytania przestrzenne (Bounding Box), bezbłędne działanie aplikacji w trybie samolotowym. 15 pkt
3. Stan Globalny & Optimistic UI Właściwe wykorzystanie Zustand, implementacja Optimistic UI (natychmiastowy update w RAM) oraz poprawny mechanizm rollbacku stanu w razie awarii zapisu do SQLite. 15 pkt
4. Sensory, Mapa & Animacje Obsługa cyklu życia uprawnień w expo-location, wydajna mapa w WebView z mostkiem postMessage, animacje modułu Animated z useNativeDriver: true. 15 pkt
5. Jakość, Testy & Wdrożenie APK Minimum 8 testów jednostkowych Jest (domena, zamockowany use-case z fałszywym zegarem), poprawny profil w eas.json, samodzielny instalator .apk działający bez Metro. 15 pkt
6. Kultura Inżynierska Git Brak pushów do maina, gałęzie funkcyjne, Conventional Commits, merytoryczne dyskusje i recenzje w Code Review (PR) z aktywnym udziałem każdego członka zespołu. 10 pkt
7. Znajomość Kodu & Obrona Płynne wyjaśnienie przepływu danych i użytych wzorców podczas odpytania (lub pomyślne wykonanie zadania ratunkowego na żywo na stanowisku). 10 pkt
SUMA 100 pkt