Flutter 2025: Riverpod 3, Firebase та Clean Architecture

Автор Ayseg, Груд. 27, 2025, 03:00 PM

« попередня та - наступна тема »

Ayseg

Вступ
У 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