# Flutter-klient, który działa w rzeczywistym świecie: trzy rozwiązania dla skomplikowanych scenariuszy
W teorii architektura aplikacji Flutter wygląda idealnie: Bloc do zarządzania stanem, Dio do API, go_router do nawigacji. W praktyce jednak realne projekty napotykają problemy wykraczające poza przykłady z tutoriali — od zmiany użytkownika po operacje w tle. Jak zbudować klienta, który nie rozpadnie się przy pierwszym odchyleniu od ścieżki bez problemów?
Rozważmy przypadek pełnoprawnej aplikacji do nauki języków z mnóstwem skomplikowanych funkcji: autoryzacja wielodostawcowa, wejście anonimowe, tłumaczenie, rozpoznawanie mowy, generowanie treści, tryb offline i połączenia WebSocket. Standardowe pakiety (flutter_bloc, dio, get_it) rozwiązują pojedyncze zadania, ale nie pokrywają scenariuszy, w których logika biznesowa wykracza poza proste przykłady. Główne problemy pojawiają się przy:
- Powiązaniu anonimowego użytkownika z trwałym kontem bez utraty danych
- Działaniu przy formalnie dostępnej, ale niestabilnej sieci
- Obsłudze długich operacji (dziesiątki sekund) bez blokowania UI
- Poprawnej zmianie identity przy logout/login
- Gwarantowanym czyszczeniu stanu po wyjściu użytkownika
Te przypadki wymagają nie wyboru nowych pakietów, lecz fundamentalnych rozwiązań w architekturze. Oto trzy podejścia, które pomogły stworzyć odpornego klienta.
Rozdzielenie identity i dostępu aplikacyjnego
Standardowa droga to używanie Firebase Auth jako jedynego źródła prawdy. Przy skomplikowanej logice biznesowej prowadzi to jednak do problemów. Na przykład przy przejściu od wejścia anonimowego do rejestracji przez Google tracone są dane, a reguły dostępu po stronie serwera stają się sztywno powiązane z Firebase.
Podzieliliśmy odpowiedzialność:
- Firebase odpowiada za identity (email/hasło, Google, Apple, wejście anonimowe)
- Backend generuje własny token JWT po potwierdzeniu identity
Schemat pracy:
- Użytkownik loguje się przez Firebase
- Klient otrzymuje Firebase ID token
- Wysyła go na backend
- Backend zwraca aplikacyjny JWT
- Wszystkie kolejne zapytania (REST i WebSocket) używają tego JWT
To podejście rozwiązuje trzy kluczowe problemy:
- Niezależność od dostawcy: wymiana Firebase na inny provider identity nie wpływa na logikę backendu
- Ujednolicenie transportu: jeden token działa dla HTTP, WebSocket i zarządzania prawami
- Płynne powiązanie kont: anonimowy użytkownik zachowuje dane przy przejściu na stałe konto
Krytycznym błędem jest próba używania tokenu Firebase bezpośrednio do dostępu do API. Backend musi zarządzać własnymi prawami i cyklem życia sesji. Dwustopniowa autentykacja zwiększa złożoność, ale rekompensuje to elastycznością i dostosowaniem do realnych scenariuszy.
Pełny reset kontekstu zamiast ręcznego czyszczenia
Przy próbie logicznego czyszczenia stanu po logout (zerowanie TokenCubit, UserCubit, pamięci podręcznej) nieuniknione są wycieki. Szczególnie w scenariuszach:
- Zmiana użytkownika (logout → login pod innym kontem)
- Powiązanie anonimowego profilu z zarejestrowanym
- Odzyskiwanie sesji po wygaśniętym tokenie
- Powrót z głęboko zagnieżdżonego ekranu
Zamiast punktowego czyszczenia odtwarzamy cały kontekst providerów. Implementacja techniczna:
- W głównym widżecie AppInitializer tworzony jest unikalny klucz
- Przy krytycznej zmianie stanu auth (logout, zmiana użytkownika) klucz jest zmieniany
- Poddrzewo z MultiBlocProvider jest przebudowywane od nowa
- Wszystkie Cubity są inicjalizowane przez GetIt od zera
final GlobalKey<_AppInitializerState> appContextKey =
GlobalKey<_AppInitializerState>();
class _AppInitializerState extends State<AppInitializer> {
Key _appKey = UniqueKey();
void resetApp() {
setState(() {
_appKey = UniqueKey();
});
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
key: _appKey,
providers: ProvidersManager.getAllProviders(),
child: widget.child,
);
}
}
Ta metoda nie jest elegancka, ale przewidywalna. Po resecie aplikacja zachowuje się jak nowa instancja sesji. Ważne:
- Wyraźnie rozdzielać długotrwałe serwisy (np. połączenie WebSocket) i stany ekranowe
- Skonfigurować DI tak, aby singlefony nie były naruszane przy odtwarzaniu
- Nie stosować tego podejścia do prostych aplikacji (mniej niż 5 ekranów)
Dla skomplikowanych projektów pełny reset kontekstu jest tańszy i pewniejszy niż nieskończone ręczne sanitizowanie.
Hybrydowy HTTP + WebSocket dla długich operacji
Standardowy wzorzec final response = await dio.post(...) zawodzi przy operacjach trwających dziesiątki sekund (analiza tekstu, generowanie treści, rozpoznawanie). Problemy:
- Timeouty zapytań HTTP
- Niestabilność przy zwinięciu aplikacji
- Zły UX przy długim oczekiwaniu
- Skomplikowana diagnostyka błędów
Rozwiązanie to przejście na model hybrydowy:
- Klient wysyła zapytanie z nagłówkiem
X-Async-Background: true - Serwer zwraca
task_idzamiast ostatecznego wyniku - Klient subskrybuje zdarzenie ukończenia przez WebSocket
- Po powiadomieniu pobiera wynik osobnym zapytaniem REST
Kluczowe zalety:
- Ewolucyjność: szybki endpoint może stać się operacją w tle bez przepisywania logiki klienta
- Niezawodność: WebSocket odpowiada za sygnał ukończenia, REST za pobranie wyniku
- Diagnostyka: oddzielne punkty do śledzenia, ponownych prób i obsługi błędów
Dla klienta mobilnego kluczowe jest zaimplementowanie:
- Automatycznego reconnect
- Mechanizmu heartbeat
- Kolejki wiadomości
- Obsługi zdarzeń cyklu życia aplikacji
- Oddzielnych strumieni dla statusów, danych i błędów
To zmienia WebSocket z „kanału powiadomień” w systemową warstwę infrastruktury. Nagłówek X-Async-Background to interfejs do gotowego rozwiązania backendu, a nie prowizorka po stronie klienta.
Co ważne: kluczowe wnioski
- Rozdzielaj identity i access: Firebase/Apple/Google powinny potwierdzać tożsamość, ale nie zarządzać prawami dostępu do zasobów
- Resetuj kontekst, a nie czyść stan: przy zmianie użytkownika odtwarzaj drzewo providerów zamiast ręcznego czyszczenia
- Projektuj z myślą o ewolucji: długie operacje od początku buduj wg schematu HTTP (start) → WebSocket (sygnał) → HTTP (wynik)
- Testuj scenariusze poza ścieżką bez problemów: wejście anonimowe → rejestracja, sieć z wysokim pingiem, zmiana użytkownika w głębokim stosie nawigacji
- Izoluj warstwy infrastrukturalne: WebSocket powinien być częścią systemu, a nie wyjątkiem
Te rozwiązania powstały nie z teoretycznych preferencji, lecz po realnych awariach. Flutter dostarcza narzędzia, ale odporność aplikacji zależy od tego, jak obsługujesz granice między komponentami. Skupienie na scenariuszach, w których wszystko idzie nie po planu, to jedyny sposób na stworzenie klienta gotowego do produkcji.
— Editorial Team
Brak komentarzy.