Powrót do strony głównej

Optymalizacja Flutter: unikaj rebuildów

Artykuł analizuje 8 powszechnych błędów optymalizacji aplikacji Flutter: zbędne rebuildy, brak const, logika w build(), niepoprawne listy. Podane przykłady kodu poprawek i kluczowe praktyki dla stabilnych 60 FPS.

Flutter bez frizów: 8 kluczowych optymalizacji
Advertisement 728x90

Praktyczne techniki optymalizacji wydajności aplikacji Flutter

Aplikacje Flutter często cierpią na zacinanie się z powodu zbędnych przebudowań, operacji w wątku UI i nieoptymalnej pracy z listami. W artykule omówiono kluczowe błędy i sposoby ich naprawy: od lokalizacji aktualizacji po poprawne użycie builderów i animacji. Te techniki pozwalają utrzymać 60 FPS nawet na słabszych urządzeniach.

Minimalizacja zbędnych przebudowań UI

Zbędne przebudowy to główna przyczyna spadku wydajności. setState() przebudowuje całe poddrzewo StatefulWidget, w tym ciężkie listy i obrazy. Prowadzi to do obliczeń układu i spadku FPS.

Problemowy kod:

Google AdInline article slot
class MyScreen extends StatefulWidget {
  @override
  State<MyScreen> createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
  int counter = 0;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Counter: $counter'),
        ElevatedButton(
          onPressed: () {
            setState(() {
              counter++; // przebudowuje CAŁE drzewo
            });
          },
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Rozwiązanie: ValueListenableBuilder aktualizuje tylko zależną część UI.

class MyScreen extends StatelessWidget {
  final ValueNotifier<int> counter = ValueNotifier(0);
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ValueListenableBuilder<int>(
          valueListenable: counter,
          builder: (_, value, __) {
            return Text('Counter: $value');
          },
        ),
        ElevatedButton(
          onPressed: () => counter.value++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

Podobnie działają StreamBuilder, Selector z provider lub BlocBuilder. Obszar przerysowania jest minimalizowany, obciążenie CPU spada.

Użycie konstruktorów const

Bez const każda przebudowa tworzy nowe obiekty widżetów, obciążając GC i CPU. W listach z setkami ikon, tekstów i paddingów jest to krytyczne.

Google AdInline article slot

Optymalny wariant:

Widget build(BuildContext context) {
  return const Column(
    children: [
      Text('Hello'),
      Icon(Icons.home),
      Padding(
        padding: EdgeInsets.all(8),
        child: Text('World'),
      ),
    ],
  );
}

Flutter ponownie używa obiektów const, alokacje są redukowane. Wzrost wydajności jest zauważalny w złożonych UI z tysiącami małych widżetów.

Przenoszenie logiki poza build()

Sortowanie, filtrowanie lub parsowanie w build() wykonywane jest przy każdym wywołaniu — do 60 razy/sek. Dla 60 FPS na klatkę pozostaje 16 ms, ciężkie operacje powodują zacinanie.

Google AdInline article slot

Prawidłowe podejście:

late List<int> filtered;
@override
void initState() {
  super.initState();
  final sorted = [...items]..sort((a, b) => a.compareTo(b));
  filtered = sorted.where((e) => e > 10).toList();
}
@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: filtered.length,
    itemBuilder: (_, i) => Text('${filtered[i]}'),
  );
}

Logika wykonywana jest jednorazowo w initState lub warstwie biznesowej. build() pozostaje deklaratywny.

Leniwie listy z ListView.builder

ListView z children: [...] tworzy wszystkie elementy od razu, nawet niewidoczne. Przy 100+ elementach pamięć i pierwszy render cierpią.

Rekomendacja:

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      final item = items[index];
      return ListTile(
        title: Text(item.title),
        subtitle: Text(item.subtitle),
      );
    },
  );
}

Elementy generowane są w miarę przewijania, pamięć jest zoptymalizowana, recykling działa efektywnie.

Klucze dla stabilności list

Bez key Flutter nie dopasowuje elementów przy aktualizacjach listy, powodując zbędne przebudowy i wizualne artefakty.

Krytyczne dla:

  • Zmiany kolejności elementów
  • Usuwania/wstawiania
  • Animacji przejść
ListView.builder(
  itemCount: items.length,
  itemBuilder: (_, index) {
    final item = items[index];
    return ListTile(
      key: ValueKey(item.id),
      title: Text(item.title),
    );
  },
);

ValueKey(item.id) zapewnia poprawne aktualizowanie.

Animacje bez setState

Timer.periodic z setState co 16–100 ms przeciąża wątek UI.

Standardowe narzędzie:

late AnimationController controller;
@override
void initState() {
  super.initState();
  controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 2),
  )..repeat();
}
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: controller,
    builder: (_, __) {
      return LinearProgressIndicator(value: controller.value);
    },
  );
}

AnimationController synchronizuje się z FPS urządzenia, aktualizując minimum UI.

Podział na małe widżety

Jeden ogromny StatefulWidget przebudowuje się całkowicie przy każdej zmianie.

Zalety kompozycji:

  • Lokalizacja przebudów
  • Ponowne użycie kodu
  • Lepsza czytelność i testy
Widget build(BuildContext context) {
  return Column(
    children: const [
      Header(),
      Expanded(child: ItemsList()),
      Footer(),
    ],
  );
}
class Header extends StatelessWidget {
  const Header();
  @override
  Widget build(BuildContext context) {
    return Text('Header');
  }
}

Future poza build()

FutureBuilder z future: fetchData() w build() uruchamia zapytania przy każdej przebudowie.

late Future dataFuture;
@override
void initState() {
  super.initState();
  dataFuture = fetchData();
}
Widget build(BuildContext context) {
  return FutureBuilder(
    future: dataFuture,
    builder: (_, snapshot) {
      if (!snapshot.hasData) return CircularProgressIndicator();
      return Text(snapshot.data.toString());
    },
  );
}

Future tworzone jest raz, UI reaguje na zmiany.

Co jest ważne

  • Lokalizuj aktualizacje: ValueListenableBuilder, AnimatedBuilder zamiast globalnego setState.
  • const wszędzie: Redukuje alokacje o 50–70% w złożonych UI.
  • Logika w initState: build() — tylko deklaratywny opis.
  • ListView.builder + klucze: Dla stabilnych dynamicznych list.
  • Kompozycja widżetów: Małe StatelessWidget zamiast monolitów.

— Editorial Team

Advertisement 728x90

Czytaj dalej