Какие ошибки делают команды, когда строят приложение на 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