Powrót do strony głównej

Niezawodny klient Flutter: rozwiązania poza happy path | Przewodnik

Praktyczne rozwiązania do budowania niezawodnych aplikacji Flutter w warunkach rzeczywistych scenariuszy. Rozdzielenie identity i dostępu aplikacyjnego, pełny reset kontekstu zamiast ręcznego czyszczenia, hybrydowy model HTTP+WebSocket dla długich operacji.

Klient Flutter, który nie rozpadnie się przy zmianie użytkownika lub problemach sieciowych
Advertisement 728x90

# 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.

Google AdInline article slot

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:

Google AdInline article slot
  • 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:

Google AdInline article slot
  • 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_id zamiast 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

Advertisement 728x90

Czytaj dalej