Packages

Firecraft: A Simplified Dart Wrapper for Cloud Firestore

Working with Cloud Firestore in Flutter applications often involves writing repetitive boilerplate code for common database operations. Enter Firecraft – a powerful Dart library that provides a clean, intuitive interface for interacting with Cloud Firestore, reducing complexity while maintaining full functionality.

What Problem Does Firecraft Solve?

When working directly with Cloud Firestore, developers often find themselves writing similar patterns over and over:

  • Converting document snapshots to model objects
  • Handling pagination logic
  • Managing error handling and null safety
  • Setting up real-time listeners
  • Performing batch operations

Firecraft eliminates this repetition by providing a unified API that handles these common patterns while maintaining type safety and flexibility.

Key Features

Simplified CRUD Operations

Perform create, read, update, and delete operations with minimal code

Real-time Streaming

Built-in support for real-time document and collection updates

Smart Pagination

Advanced pagination with hasMore indicators and cursor-based navigation

Batch Updates

Efficient batch operations for updating multiple documents

Document Counting

Real-time document counting with custom filtering

Type Safety

Full generic type support with custom JSON serialization

Getting Started

Firecraft uses the singleton pattern, so you can access it anywhere in your app:

Installation

Add Firecraft to your pubspec.yaml:

dependencies:
  firecraft:

Initialize the service:

final firecraft = Firecraft.instance;

Basic Operations

Fetching Documents

Single Document:

// Fetch a user by ID
final user = await firecraft.fetchDocument<User>(
  documentPath: 'users/123',
  fromJson: User.fromJson,
);

if (user != null) {
  print('Found user: ${user.name}');
}

Collection:

// Fetch all users
final users = await firecraft.fetchCollection<User>(
  collectionPath: 'users',
  fromJson: User.fromJson,
);

// Fetch with custom query and limit
final activeUsers = await firecraft.fetchCollection<User>(
  collectionPath: 'users',
  fromJson: User.fromJson,
  queryBuilder: (query) => query.where('isActive', isEqualTo: true),
  limit: 10,
);

Adding and Updating Documents

Adding Documents:

// Add a new user
final docRef = await firecraft.addDocument(
  collectionPath: 'users',
  data: {
    'name': 'John Doe',
    'email': 'john@example.com',
    'createdAt': FieldValue.serverTimestamp(),
  },
);

print('New user ID: ${docRef.id}');

Setting Documents:

// Create or overwrite a document with specific ID
await firecraft.setDocument(
  collectionPath: 'users',
  docId: 'user_123',
  data: {
    'name': 'Jane Smith',
    'email': 'jane@example.com',
  },
);

Updating Documents:

// Update specific fields
await firecraft.updateDocument(
  documentPath: 'users/123',
  data: {
    'lastLogin': FieldValue.serverTimestamp(),
    'loginCount': FieldValue.increment(1),
  },
);

Deleting Documents

await firecraft.deleteDocument(documentPath: 'users/123');

Real-time Streaming

One of Firecraft’s most powerful features is its built-in streaming capabilities.

Document Streaming

// Stream a single document
firecraft.streamDocument<User>(
  documentPath: 'users/123',
  fromJson: User.fromJson,
).listen((user) {
  if (user != null) {
    print('User updated: ${user.name}');
  } else {
    print('User deleted or does not exist');
  }
});

Field-specific Streaming

// Stream only a specific field
firecraft.streamDocumentField<bool>(
  documentPath: 'users/123',
  key: 'isOnline',
).listen((isOnline) {
  print('User online status: $isOnline');
});

Collection Streaming

// Stream an entire collection with real-time updates
firecraft.streamCollection<User>(
  collectionPath: 'users',
  fromJson: User.fromJson,
  queryBuilder: (query) => query.where('isActive', isEqualTo: true),
).listen((users) {
  print('Active users count: ${users.length}');
});

Advanced Features

Pagination Made Easy

Firecraft provides sophisticated pagination support that handles cursor-based navigation automatically.

Initial Page:

final firstPage = await firecraft.fetchInitialPage<Post>(
  collectionPath: 'posts',
  fromJson: Post.fromJson,
  queryBuilder: (query) => query.orderBy('createdAt', descending: true),
  limit: 20,
);

print('Posts: ${firstPage.items.length}');
print('Has more: ${firstPage.hasMore}');

Next Page:

if (firstPage.hasMore && firstPage.lastDocument != null) {
  final nextPage = await firecraft.fetchNextPage<Post>(
    collectionPath: 'posts',
    fromJson: Post.fromJson,
    lastDocument: firstPage.lastDocument!,
    queryBuilder: (query) => query.orderBy('createdAt', descending: true),
    limit: 20,
  );
}

Real-time Paginated Streaming:

firecraft.paginatedCollection<Post>(
  collectionPath: 'posts',
  fromJson: Post.fromJson,
  queryBuilder: (query) => query.orderBy('createdAt', descending: true),
  limit: 20,
).listen((result) {
  print('Page items: ${result.items.length}');
  print('Has more: ${result.hasMore}');
});

Batch Updates with Conditions

Update multiple documents efficiently based on custom conditions:

final updatedCount = await firecraft.updateWhereField(
  collectionPath: 'users',
  fieldToUpdate: 'status',
  newValue: 'inactive',
  condition: (data) {
    final lastLogin = data['lastLogin'] as Timestamp?;
    if (lastLogin == null) return true;
    
    final daysSinceLogin = DateTime.now()
        .difference(lastLogin.toDate())
        .inDays;
    
    return daysSinceLogin > 30; // Inactive for 30+ days
  },
  queryBuilder: (query) => query.where('status', isEqualTo: 'active'),
);

print('Updated $updatedCount users to inactive status');

Real-time Document Counting

// Count all active users
firecraft.streamDocumentCount(
  collectionPath: 'users',
  queryBuilder: (query) => query.where('isActive', isEqualTo: true),
).listen((count) {
  print('Active users: $count');
});

// Count with additional filtering
firecraft.streamDocumentCount(
  collectionPath: 'posts',
  additionalCondition: (data) {
    final content = data['content'] as String?;
    return content != null && content.length > 100;
  },
).listen((count) {
  print('Long posts: $count');
});

Error Handling

Firecraft automatically catches and rethrows FirebaseExceptions, allowing you to handle them appropriately:

try {
  final user = await firecraft.fetchDocument<User>(
    documentPath: 'users/123',
    fromJson: User.fromJson,
  );
} on FirebaseException catch (e) {
  print('Firestore error: ${e.message}');
  // Handle specific error cases
  switch (e.code) {
    case 'permission-denied':
      // Handle permission error
      break;
    case 'unavailable':
      // Handle network error
      break;
  }
}

Best Practices

1. Model Classes with fromJson

Create model classes with fromJson constructors for type safety:

class User {
  final String id;
  final String name;
  final String email;
  final DateTime? createdAt;

  User({
    required this.id,
    required this.name,
    required this.email,
    this.createdAt,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
      createdAt: json['createdAt']?.toDate(),
    );
  }
}

2. Use Query Builders for Complex Queries

Leverage the queryBuilder parameter for complex filtering:

final premiumActiveUsers = await firecraft.fetchCollection<User>(
  collectionPath: 'users',
  fromJson: User.fromJson,
  queryBuilder: (query) => query
      .where('isPremium', isEqualTo: true)
      .where('isActive', isEqualTo: true)
      .orderBy('createdAt', descending: true),
);

3. Dispose of Stream Subscriptions

Always dispose of stream subscriptions to prevent memory leaks:

class UserService {
  StreamSubscription<User?>? _userSubscription;

  void startListening(String userId) {
    _userSubscription = firecraft.streamDocument<User>(
      documentPath: 'users/$userId',
      fromJson: User.fromJson,
    ).listen((user) {
      // Handle user updates
    });
  }

  void dispose() {
    _userSubscription?.cancel();
  }
}

Conclusion

Firecraft transforms the way you interact with Cloud Firestore by providing a clean, intuitive API that handles common patterns while maintaining the full power and flexibility of Firestore. Whether you’re building a simple app or a complex application with real-time features, Firecraft reduces boilerplate code and helps you focus on what matters most – building great user experiences.

The library’s type-safe approach, combined with powerful features like real-time streaming, smart pagination, and batch operations, makes it an excellent choice for Flutter developers working with Firestore.

Try Firecraft in your next Flutter project and experience the difference a well-designed abstraction can make!