Flutter BLoC Pattern Deep Dive: Advanced State Management with Appwrite | Flutter Bloc Appwrite 2025

flutter-bloc-pattern

In today’s fast-paced world of mobile app development, managing state effectively is crucial for building scalable and maintainable applications. This comprehensive guide will walk you through implementing advanced state management in Flutter using the BLoC (Business Logic Component) pattern, integrated with Appwrite backend, and following Clean Architecture principles.

Master the Flutter BLoC Pattern with our comprehensive guide. Learn advanced state management, clean architecture, and real-time features using Appwrite backend integration.

Content Structure:

  1. Understanding Flutter BLoC Pattern
    • The Flutter BLoC Pattern separates business logic from UI components, creating a maintainable and testable codebase. By implementing the Flutter BLoC Pattern, you ensure a clear separation of concerns in your application.
  2. Core Components of Flutter BLoC Pattern
    • Events: Trigger state changes
    • States: Represent UI states
    • BLoC: Manages business logic
    • – Streams: Handle data flow
  3. Implementing Flutter BLoC Pattern
    • Learn how to structure your application using the Flutter BLoC Pattern with clean architecture principles. This section covers practical implementation details and best practices.
  4. Advanced Flutter BLoC Pattern Features
    • Real-time state management
    • Dependency injection
    • Error handling
    • Testing strategies

Introduction

State management in Flutter applications can become complex, especially when dealing with real-time collaborative features and backend integration. The BLoC pattern, combined with Appwrite’s powerful backend services and Clean Architecture, provides a robust solution for handling these complexities.

Prerequisites

  • Basic knowledge of Flutter and Dart
  • Understanding of state management concepts
  • Familiarity with dependency injection
  • An Appwrite account

Project Setup

Let’s start by creating a new Flutter project and setting up the necessary dependencies. Our project will demonstrate a collaborative document editing system where multiple users can edit documents in real-time.

Dependencies Setup

First, create a new Flutter project and add the following dependencies to your pubspec.yaml:

dependencies:
flutter:
sdk: flutter
appwrite: ^8.0.0 # Appwrite SDK for Flutter
flutter_bloc: ^8.1.3 # BLoC pattern implementation
get_it: ^7.6.4 # Dependency injection
equatable: ^2.0.5 # Value equality
dartz: ^0.10.1 # Functional programming features

dev_dependencies:
flutter_lints: ^2.0.0

Project Structure

Flutter BLoC Pattern Following Clean Architecture principles, we’ll organize our code into distinct layers:

lib/
├── core/
│ ├── error/
│ │ └── failures.dart
│ └── usecases/
│ └── usecase.dart
├── features/
│ └── document_editing/
│ ├── data/
│ │ ├── datasources/
│ │ │ └── document_remote_datasource.dart
│ │ ├── models/
│ │ │ └── document_model.dart
│ │ └── repositories/
│ │ └── document_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ └── document.dart
│ │ ├── repositories/
│ │ │ └── document_repository.dart
│ │ └── usecases/
│ │ ├── create_document.dart
│ │ ├── get_document.dart
│ │ └── update_document.dart
│ └── presentation/
│ ├── bloc/
│ │ ├── document_bloc.dart
│ │ ├── document_event.dart
│ │ └── document_state.dart
│ ├── pages/
│ │ └── document_edit_page.dart
│ └── widgets/
│ └── document_editor.dart
├── injection_container.dart
└── main.dart
flutter-bloc-pattern
flutter-bloc-pattern

Core Implementation

1. Error Handling

Let’s start with the core error handling implementation:

// lib/core/error/failures.dart
import 'package:equatable/equatable.dart';

abstract class Failure extends Equatable {
@override
List<Object> get props => [];
}

class ServerFailure extends Failure {}
class NetworkFailure extends Failure {}
class CacheFailure extends Failure {}

2. Base UseCase

Create a base usecase class for consistent implementation:

// lib/core/usecases/usecase.dart
import 'package:dartz/dartz.dart';
import '../error/failures.dart';

abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}

class NoParams extends Equatable {
@override
List<Object> get props => [];
}

Domain Layer

1. Document Entity

// lib/features/document_editing/domain/entities/document.dart
import 'package:equatable/equatable.dart';

class Document extends Equatable {
final String id;
final String content;
final String lastEditedBy;
final DateTime lastEditedAt;
final List<String> collaborators;

const Document({
required this.id,
required this.content,
required this.lastEditedBy,
required this.lastEditedAt,
required this.collaborators,
});

@override
List<Object> get props => [id, content, lastEditedBy, lastEditedAt, collaborators];
}

2. Repository Interface

// lib/features/document_editing/domain/repositories/document_repository.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../entities/document.dart';

abstract class DocumentRepository {
Future<Either<Failure, Document>> getDocument(String id);
Future<Either<Failure, Document>> createDocument(String content, String userId);
Future<Either<Failure, Document>> updateDocument(String id, String content, String userId);
Stream<Document> watchDocument(String id);
}

Data Layer

1. Document Model

// lib/features/document_editing/data/models/document_model.dart
import '../../domain/entities/document.dart';

class DocumentModel extends Document {
const DocumentModel({
required String id,
required String content,
required String lastEditedBy,
required DateTime lastEditedAt,
required List<String> collaborators,
}) : super(
id: id,
content: content,
lastEditedBy: lastEditedBy,
lastEditedAt: lastEditedAt,
collaborators: collaborators,
);

factory DocumentModel.fromJson(Map<String, dynamic> json) {
return DocumentModel(
id: json['$id'] as String,
content: json['content'] as String,
lastEditedBy: json['lastEditedBy'] as String,
lastEditedAt: DateTime.parse(json['lastEditedAt'] as String),
collaborators: List<String>.from(json['collaborators'] as List),
);
}

Map<String, dynamic> toJson() {
return {
'content': content,
'lastEditedBy': lastEditedBy,
'lastEditedAt': lastEditedAt.toIso8601String(),
'collaborators': collaborators,
};
}
}

2. Remote Data Source

// lib/features/document_editing/data/datasources/document_remote_datasource.dart
import 'package:appwrite/appwrite.dart';
import '../models/document_model.dart';

abstract class DocumentRemoteDataSource {
Future<DocumentModel> getDocument(String id);
Future<DocumentModel> createDocument(String content, String userId);
Future<DocumentModel> updateDocument(String id, String content, String userId);
Stream<DocumentModel> watchDocument(String id);
}

class DocumentRemoteDataSourceImpl implements DocumentRemoteDataSource {
final Databases databases;
final Client client;
final String databaseId = 'your_database_id';
final String collectionId = 'your_collection_id';

DocumentRemoteDataSourceImpl({
required this.client,
required this.databases,
});

@override
Future<DocumentModel> getDocument(String id) async {
try {
final response = await databases.getDocument(
databaseId: databaseId,
collectionId: collectionId,
documentId: id,
);
return DocumentModel.fromJson(response.data);
} catch (e) {
throw ServerException();
}
}

@override
Future<DocumentModel> createDocument(String content, String userId) async {
try {
final document = await databases.createDocument(
databaseId: databaseId,
collectionId: collectionId,
documentId: ID.unique(),
data: {
'content': content,
'lastEditedBy': userId,
'lastEditedAt': DateTime.now().toIso8601String(),
'collaborators': [userId],
},
);
return DocumentModel.fromJson(document.data);
} catch (e) {
throw ServerException();
}
}

@override
Future<DocumentModel> updateDocument(String id, String content, String userId) async {
try {
final document = await databases.updateDocument(
databaseId: databaseId,
collectionId: collectionId,
documentId: id,
data: {
'content': content,
'lastEditedBy': userId,
'lastEditedAt': DateTime.now().toIso8601String(),
},
);
return DocumentModel.fromJson(document.data);
} catch (e) {
throw ServerException();
}
}

@override
Stream<DocumentModel> watchDocument(String id) {
final realtime = Realtime(client);
return realtime.subscribe([
'databases.$databaseId.collections.$collectionId.documents.$id'
]).map((event) => DocumentModel.fromJson(event.payload));
}
}

Presentation Layer

1. BLoC Implementation

// lib/features/document_editing/presentation/bloc/document_state.dart
abstract class DocumentState extends Equatable {
const DocumentState();

@override
List<Object> get props => [];
}

class DocumentInitial extends DocumentState {}
class DocumentLoading extends DocumentState {}
class DocumentLoaded extends DocumentState {
final Document document;
const DocumentLoaded({required this.document});

@override
List<Object> get props => [document];
}
class DocumentError extends DocumentState {
final String message;
const DocumentError({required this.message});

@override
List<Object> get props => [message];
}

// lib/features/document_editing/presentation/bloc/document_event.dart
abstract class DocumentEvent extends Equatable {
const DocumentEvent();

@override
List<Object> get props => [];
}

class GetDocumentEvent extends DocumentEvent {
final String documentId;
const GetDocumentEvent({required this.documentId});

@override
List<Object> get props => [documentId];
}

class CreateDocumentEvent extends DocumentEvent {
final String content;
final String userId;
const CreateDocumentEvent({required this.content, required this.userId});

@override
List<Object> get props => [content, userId];
}

class UpdateDocumentEvent extends DocumentEvent {
final String documentId;
final String content;
final String userId;
const UpdateDocumentEvent({
required this.documentId,
required this.content,
required this.userId,
});

@override
List<Object> get props => [documentId, content, userId];
}

// lib/features/document_editing/presentation/bloc/document_bloc.dart
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
final GetDocument getDocument;
final CreateDocument createDocument;
final UpdateDocument updateDocument;
StreamSubscription? _documentSubscription;

DocumentBloc({
required this.getDocument,
required this.createDocument,
required this.updateDocument,
}) : super(DocumentInitial()) {
on<GetDocumentEvent>(_onGetDocument);
on<CreateDocumentEvent>(_onCreateDocument);
on<UpdateDocumentEvent>(_onUpdateDocument);
}

Future<void> _onGetDocument(
GetDocumentEvent event,
Emitter<DocumentState> emit,
) async {
emit(DocumentLoading());
final result = await getDocument(GetDocumentParams(id: event.documentId));

result.fold(
(failure) => emit(DocumentError(message: _mapFailureToMessage(failure))),
(document) {
_subscribeToDocumentUpdates(event.documentId);
emit(DocumentLoaded(document: document));
},
);
}

void _subscribeToDocumentUpdates(String documentId) {
_documentSubscription?.cancel();
_documentSubscription = documentRemoteDataSource
.watchDocument(documentId)
.listen(
(document) => add(
UpdateDocumentEvent(
documentId: documentId,
content: document.content,
userId: document.lastEditedBy,
),
),
);
}

@override
Future<void> close() {
_documentSubscription?.cancel();
return super.close();
}
}

2. UI Implementation

// lib/features/document_editing/presentation/pages/document_edit_page.dart
class DocumentEditPage extends StatelessWidget {
final String documentId;

const DocumentEditPage({Key? key, required this.documentId}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Collaborative Document Editor'),
actions: [
BlocBuilder<DocumentBloc, DocumentState>(
builder: (context, state) {
if (state is DocumentLoaded) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Last edited by: ${state.document.lastEditedBy}',
style: Theme.of(context).textTheme.bodySmall,
),
);
}
return const SizedBox.shrink();
},
),
],
),
body: BlocConsumer<DocumentBloc, DocumentState>(
listener: (context, state) {
if (state is DocumentError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.red,
),
);
}
},
builder: (context, state) {
if (state is DocumentLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is DocumentLoaded) {
return DocumentEditor(
document: state.document,
onContentChanged: (content) {
context.read<DocumentBloc>().add(
UpdateDocumentEvent(
documentId: documentId,
content: content,
userId: 'current_user_id', // Replace with actual user ID
),
);
},
);
}
return const Center(child: Text('Something went wrong'));
},
),
);
}
}

// lib/features/document_editing/presentation/widgets/document_editor.dart
class DocumentEditor extends StatefulWidget {
final Document document;
final ValueChanged<String> onContentChanged;

const DocumentEditor({
Key? key,
required this.document,
required this.onContentChanged,
}) : super(key: key);

@override
State<DocumentEditor> createState() => _DocumentEditorState();
}

class _DocumentEditorState extends State<DocumentEditor> {
late TextEditingController _controller;
Timer? _debounce;

@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.document.content);
}

@override
void didUpdateWidget(DocumentEditor oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.document.content != widget.document.content) {
_controller.text = widget.document.content;
}
}

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Expanded(
child: TextField(
controller: _controller,
maxLines: null,
expands: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Start typing...',
),
onChanged: (value) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
widget.onContentChanged(value);
});
},
),
),
const SizedBox(height: 16),
Text(
'Last edited: ${widget.document.lastEditedAt.toLocal()}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}

@override
void dispose() {
_controller.dispose();
_debounce?.cancel();
super.dispose();
}
}

Dependency Injection Setup

// lib/injection_container.dart
import 'package:get_it/get_it.dart';
import 'package:appwrite/appwrite.dart';

final sl = GetIt.instance;

Future<void> init() async {
// BLoC
sl.registerFactory(
() => DocumentBloc(
getDocument: sl(),
createDocument: sl(),
updateDocument: sl(),
),
);

// Use cases
sl.registerLazySingleton(() => GetDocument(sl()));
sl.registerLazySingleton(() => CreateDocument(sl()));
sl.registerLazySingleton(() => UpdateDocument(sl()));

// Repository
sl.registerLazySingleton<DocumentRepository>(
() => DocumentRepositoryImpl(remoteDataSource: sl()),
);

// Data sources
sl.registerLazySingleton<DocumentRemoteDataSource>(
() => DocumentRemoteDataSourceImpl(client: sl(), databases: sl()),
);

// External
final client = Client()
..setEndpoint('YOUR_APPWRITE_ENDPOINT')
..setProject('YOUR_PROJECT_ID');

sl.registerLazySingleton(() => client);
sl.registerLazySingleton(() => Databases(client));
}

Main Application Entry

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'injection_container.dart' as di;

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await di.init();
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter BLoC with Appwrite',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: BlocProvider(
create: (context) => di.sl<DocumentBloc>()
..add(const GetDocumentEvent(documentId: 'your_document_id')),
child: const DocumentEditPage(documentId: 'your_document_id'),
),
);
}
}

Key Features Implemented

  1. Clean Architecture: The codebase is organized into distinct layers (presentation, domain, and data) for better separation of concerns.
  2. Dependency Injection: Using get_it for efficient dependency management and better testability.
  3. Real-time Updates: Implemented using Appwrite’s Realtime feature to sync document changes across multiple users.
  4. Optimistic Updates: The UI updates immediately while changes are being saved to the backend.
  5. Error Handling: Comprehensive error handling with proper error states and user feedback.
  6. Debouncing: Implemented to prevent excessive API calls during rapid text changes.

Best Practices and Considerations

  1. State Management:
    • Use BLoC for complex state management
    • Keep states immutable
    • Handle loading and error states appropriately
  2. Performance:
    • Implement debouncing for text input
    • Use streams efficiently
    • Cancel subscriptions when disposing
  3. Error Handling:
    • Implement proper error handling at all layers
    • Show user-friendly error messages
    • Handle network errors gracefully
  4. Code Organization:
    • Follow Clean Architecture principles
    • Use dependency injection
    • Keep components small and focused

Conclusion

This implementation demonstrates a robust approach to building a collaborative document editing feature using Flutter, BLoC, and Appwrite. The clean architecture ensures the code is maintainable and scalable, while the BLoC pattern provides efficient state management. The integration with Appwrite enables real-time collaboration features with minimal effort.

To use this code:

  1. Set up an Appwrite project and create the necessary collection
  2. Replace the placeholder values for endpoint and project ID
  3. Implement proper user authentication
  4. Add additional features like offline support or conflict resolution as needed

This implementation provides a solid foundation for building complex, collaborative applications while maintaining clean and maintainable code.

For more Flutter tutorials and best practices, visit FlutterCodingHub.com.

Previous Article

10 Essential Flutter Widgets Every New Developer Should Master

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨