ВступУ 2025 році версії Flutter 3.38+ зробили Impeller rendering engine стандартним, з'явився web hot-reload і полегшилася інтеграція з AI. Як інженер-програміст, якщо ви хочете швидко та чисто розробляти крос-платформні додатки, комбінація Riverpod (актуальний король управління станом), Firebase (backend-as-a-service) та Clean Architecture стала золотим стандартом.
У цьому посібнику ми створимо з нуля
Додаток Todo + Not (з автентифікацією, реальним часом синхронізації, офлайн-підтримкою). Проект:
- Вхід користувача (Firebase Auth)
- Додавання/видалення/оновлення нотаток (Firestore)
- Управління станом з Riverpod
- Шари Clean Architecture (data, domain, presentation)
- Темна/світла тема + адаптивний дизайн
ВимогиFlutter 3.38+, Dart 3.10+
flutter pub add flutter_riverpod riverpod_annotation freezed_annotation json_annotation
flutter pub add --dev build_runner riverpod_generator freezed json_serializable- Структура проекту (Clean Architecture)
lib/
- core/ # спільні утиліти, константи, винятки
- features/
- todo/
- data/ # репозиторії, моделі, джерела даних
- domain/ # сутності, use cases, репозиторії абстрактні
- presentation/ # екрани, віджети, провайдери
- main.dart
- Налаштування Firebase (Актуальне на 2025 рік)
- Firebase Console → Створити новий проект
- Додати Android/iOS додаток (додати файли google-services.json / GoogleService-Info.plist)
- Ініціалізувати в main.dart:
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart'; // створено за допомогою flutterfire configure
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const ProviderScope(child: MyApp()));
}- Сутність та Модель (Domain → Data)
lib/features/todo/domain/entities/todo.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'todo.freezed.dart';
part 'todo.g.dart';
@freezed
class Todo with _$Todo {
const factory Todo({
required String id,
required String title,
@Default(false) bool completed,
required DateTime createdAt,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}Для перетворення map з Firestore → entity напишіть converter:
- Провайдери Riverpod (Управління станом)
lib/features/todo/presentation/providers/todo_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../domain/entities/todo.dart';
part 'todo_provider.g.dart';
@riverpod
class TodoNotifier extends _$TodoNotifier {
@override
Stream<List<Todo>> build() {
return FirebaseFirestore.instance
.collection('todos')
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => Todo.fromJson({...doc.data(), 'id': doc.id})).toList());
}
Future<void> addTodo(String title) async {
await FirebaseFirestore.instance.collection('todos').add({
'title': title,
'completed': false,
'createdAt': FieldValue.serverTimestamp(),
});
}
Future<void> toggleTodo(String id, bool completed) async {
await FirebaseFirestore.instance.collection('todos').doc(id).update({
'completed': completed,
});
}
Future<void> deleteTodo(String id) async {
await FirebaseFirestore.instance.collection('todos').doc(id).delete();
}
}Проста автентифікація email/password:
final authProvider = Provider<FirebaseAuth>((ref) => FirebaseAuth.instance);
final userProvider = StreamProvider<User?>((ref) {
return ref.watch(authProvider).authStateChanges();
});Екран логіну → Перенаправлення з AuthWrapper:
class AuthWrapper extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider).value;
if (user == null) return LoginScreen();
return TodoScreen();
}
}- Головний Екран (Список Todo)
TodoScreen.dart
class TodoScreen extends ConsumerWidget {
final _titleController = TextEditingController();
@override
Widget build(BuildContext context, WidgetRef ref) {
final todosAsync = ref.watch(todoNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('My Todos 2025')),
body: todosAsync.when(
data: (todos) => ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return Dismissible(
key: ValueKey(todo.id),
onDismissed: (_) => ref.read(todoNotifierProvider.notifier).deleteTodo(todo.id),
child: CheckboxListTile(
title: Text(
todo.title,
style: TextStyle(decoration: todo.completed ? TextDecoration.lineThrough : null),
),
value: todo.completed,
onChanged: (_) => ref.read(todoNotifierProvider.notifier).toggleTodo(todo.id, !todo.completed),
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(child: Text('Помилка: $err')),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Новий Todo'),
content: TextField(controller: _titleController),
actions: [
TextButton(
onPressed: () {
if (_titleController.text.isNotEmpty) {
ref.read(todoNotifierProvider.notifier).addTodo(_titleController.text);
_titleController.clear();
}
Navigator.pop(context);
},
child: const Text('Додати'),
),
],
),
);
},
child: const Icon(Icons.add),
),
);
}
}- Поради щодо Теми та Адаптивності
У MaterialApp використовуйте ThemeMode.system + адаптивний відступ з MediaQuery:
ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
)Результат та Наступні КрокиЗ цією структурою ви заклали основу для production-ready додатка. На вищому рівні:
• Для офлайн-підтримки додайте firebase_offline або hive
• Напишіть unit/widget тести (flutter_test + mockito)
• Для CI/CD використовуйте GitHub Actions
• Для інтеграції AI використовуйте Gemini API або Firebase ML