FlutterMiddleExperience

Какие ошибки делают команды, когда строят приложение на Flutter как web/backend-проект без учёта mobile constraints?

Команды с web/backend фоном чаще всего игнорируют мобильный lifecycle (AppLifecycleState), блокируют UI тяжёлыми операциями на main isolate вместо compute(), не кешируют изображения и не обрабатывают offline-сценарии.

Ошибка 1: Игнорирование mobile lifecycle

Web/backend разработчики привыкли, что приложение всегда активно. На мобильном ОС убивает процессы, ставит на паузу, ограничивает background-работу.

// Ошибка: запуск long-running операции без учёта lifecycle
class DataSyncService {
  Timer? _timer;

  void startSync() {
    // ПРОБЛЕМА: timer продолжает работать в background,
    // но UI обновится только когда приложение вернётся на экран
    _timer = Timer.periodic(
      const Duration(minutes: 5),
      (_) => syncData(),
    );
  }
  // dispose() не вызывается — утечка ресурсов
}

// Правильно: учитывать AppLifecycleState
class DataSyncService with WidgetsBindingObserver {
  Timer? _timer;

  void init() {
    WidgetsBinding.instance.addObserver(this);
    _startTimer();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      _timer?.cancel(); // Остановить в background
    } else if (state == AppLifecycleState.resumed) {
      _startTimer(); // Возобновить
    }
  }

  void _startTimer() {
    _timer = Timer.periodic(const Duration(minutes: 5), (_) => syncData());
  }

  void dispose() {
    _timer?.cancel();
    WidgetsBinding.instance.removeObserver(this);
  }
}

Ошибка 2: Тяжёлые операции на main isolate

Web-разработчики используют async/await и считают, что это «параллельно». В Flutter async не создаёт отдельный поток — всё выполняется на main isolate, блокируя UI.

// Ошибка: парсинг большого JSON на main isolate
Future<List<Product>> loadProducts() async {
  final response = await http.get(Uri.parse('/api/products'));
  // ПРОБЛЕМА: jsonDecode на 10MB — заморозит UI на 200-500ms
  return (jsonDecode(response.body) as List)
      .map((e) => Product.fromJson(e))
      .toList();
}

// Правильно: использовать compute() для CPU-heavy работы
Future<List<Product>> loadProducts() async {
  final response = await http.get(Uri.parse('/api/products'));
  return compute(_parseProducts, response.body);
}

List<Product> _parseProducts(String body) {
  return (jsonDecode(body) as List)
      .map((e) => Product.fromJson(e))
      .toList();
}

Ошибка 3: Игнорирование памяти и размера ресурсов

В web браузер управляет памятью и сеть быстрее. На мобильном RAM ограничен (особенно low-end Android), а мобильная сеть — нестабильна.

  • Загрузка оригинальных изображений (5–10 MB) вместо thumbnails — OOM-крэш на устройствах с 2 GB RAM.
  • Кеширование всех данных в памяти без LRU-лимита.
  • Использование Image.network() без cached_network_image — повторная загрузка при каждой перестройке виджета.
// Правильно: кешировать изображения
CachedNetworkImage(
  imageUrl: product.imageUrl,
  // Автоматически кеширует на диск и в память
  placeholder: (ctx, url) => const CircularProgressIndicator(),
  errorWidget: (ctx, url, err) => const Icon(Icons.error),
  memCacheWidth: 400, // Ограничить размер в памяти
)

Ошибка 4: setState() повсюду вместо правильного State management

Перенос backend-паттернов (глобальные переменные, singleton service) без учёта реактивности виджетов:

// Ошибка: глобальный setState в корневом виджете перестраивает всё дерево
class AppState extends StatefulWidget {
  static late _AppStateState instance;

  @override
  State<AppState> createState() {
    instance = _AppStateState();
    return instance;
  }
}

// Правильно: Riverpod с точечными обновлениями
final cartProvider = StateNotifierProvider<CartNotifier, CartState>(
  (ref) => CartNotifier(),
);

// Только виджеты, подписанные на cartProvider, перестраиваются
class CartBadge extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(cartProvider.select((s) => s.itemCount));
    return Badge(count: count);
  }
}

Ошибка 5: Отсутствие offline-режима

Mobile-сеть нестабильна. Web-разработчики редко думают об offline. Приложение должно корректно обрабатывать SocketException, TimeoutException, кешировать данные локально (Hive, Drift, SharedPreferences) и показывать stale data с индикацией.

Подводные камни

  • Не отписываться от StreamSubscription в dispose() — утечка памяти, которая проявляется только при длительном использовании приложения.
  • Использование BuildContext после await без проверки if (mounted) — крэш при быстрой навигации назад.
  • Анимации на 60 Hz устройстве без TickerProvider — AnimationController без vsync потребляет CPU даже когда невидим.
  • Глубокая вложенность виджетов (Nested hell) замедляет diff-алгоритм — разбивать на именованные виджеты.
  • Игнорирование back button на Android — Navigator.maybePop() вместо жёсткого exit.
  • Хранение чувствительных данных в SharedPreferences (не зашифровано) — использовать flutter_secure_storage.
  • Не учитывать safe area (notch, dynamic island) — Scaffold.body без SafeArea или MediaQuery.padding.
  • Тестирование только на эмуляторе с быстрым Wi-Fi — обязательно тестировать на реальном low-end устройстве с ограниченной сетью (Android Go, старый iPhone).

What hurts your answer

  • Перечислять ошибки без объяснения причин
  • Не отличать beginner mistakes от production failure modes
  • Не предлагать процесс, который предотвращает повторение ошибок

What they're listening for

  • Знает типичные ошибки при работе с Flutter
  • Понимает причины ошибок
  • Предлагает практики prevention и early detection

Related topics