Repository on GitHub | Report an Issue

firebase_core_plus Package

A Flutter package that extends Firebase Core with strong typing and serialization capabilities for Firestore operations.

Features

  • Strong Typing: Type-safe Firestore operations with generic classes
  • Automatic Serialization: Built-in JSON serialization/deserialization
  • CRUD Operations: Complete Create, Read, Update, Delete operations
  • Real-time Streams: Live data updates with typed streams
  • Search Capabilities: Advanced search with multiple field support
  • Sub-collections: Full support for Firestore sub-collections
  • Interface-based: Enforce consistent model structure across your app

Installation

dependencies:
  firebase_core_plus: <latest_version>

Example

Here is a complete example using all main features of the package:

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core_plus/firebase_core_plus.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: const FirebaseOptions(
      apiKey: "YOUR_API_KEY",
      authDomain: "YOUR_PROJECT.firebaseapp.com",
      projectId: "YOUR_PROJECT",
      storageBucket: "YOUR_PROJECT.appspot.com",
      messagingSenderId: "YOUR_SENDER_ID",
      appId: "YOUR_APP_ID",
    ),
  );
  FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080);
  runApp(const MyApp());
}

// Model implementing InterfacePlus
class Cart implements InterfacePlus {
  Cart({required this.uid, required this.name, required this.price});

  @override
  String? uid;
  final String name;
  final double price;

  @override
  Map<String, dynamic> get json => {
    'uid': uid,
    'name': name,
    'price': price,
  };

  static Cart withMap(Map<String, dynamic> map) => Cart(
    uid: map['uid'],
    name: map['name'],
    price: (map['price'] as num).toDouble(),
  );

  double get priceWithTVA => price * 1.2;
  bool get isExpensive => price > 50;
}

// Sub-collection model
class CartItem implements InterfacePlus {
  CartItem({required this.uid, required this.label, required this.qty});

  @override
  String? uid;
  final String label;
  final int qty;

  @override
  Map<String, dynamic> get json => {
    'uid': uid,
    'label': label,
    'qty': qty,
  };

  static CartItem withMap(Map<String, dynamic> map) => CartItem(
    uid: map['uid'],
    label: map['label'],
    qty: map['qty'],
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FirestorePlus Demo',
      theme: ThemeData(primarySwatch: Colors.indigo),
      home: const CartDemoPage(),
    );
  }
}

class CartDemoPage extends StatefulWidget {
  const CartDemoPage({super.key});

  @override
  State<CartDemoPage> createState() => _CartDemoPageState();
}

class _CartDemoPageState extends State<CartDemoPage> {
  late final FirestorePlus<Cart> cartCollection;
  String? lastAddedId;
  List<CartItem> cartItems = [];

  @override
  void initState() {
    super.initState();
    cartCollection = FirestorePlus<Cart>.instance(
      tConstructor: Cart.withMap,
      path: 'carts',
    );
  }

  Future<void> _addCart() async {
    final cart = Cart(uid: '', name: 'Produit ${DateTime.now().millisecondsSinceEpoch}', price: 19.99);
    final id = await cartCollection.add(object: cart);
    setState(() => lastAddedId = id);
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Ajouté avec id: $id')));
  }

  Future<void> _addItemToCart() async {
    if (lastAddedId == null) return;
    
    final cartPlus = FirestorePlus<Cart>.instance(
      tConstructor: Cart.withMap,
      path: 'carts',
      uid: lastAddedId,
    );
    
    final itemsPlus = cartPlus.subCollection<CartItem>(
      subPath: 'items',
      itemUid: null,
      tConstructor: CartItem.withMap,
    );
    
    final item = CartItem(uid: '', label: 'Item ${DateTime.now().millisecondsSinceEpoch}', qty: 1);
    await itemsPlus.add(object: item);
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Item ajouté au panier.')));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('FirestorePlus Demo')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: _addCart,
                    child: const Text('Add Cart'),
                  ),
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: ElevatedButton(
                    onPressed: _addItemToCart,
                    child: const Text('Add Item'),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 24),
            const Text('Real-time Carts Stream:', style: TextStyle(fontWeight: FontWeight.bold)),
            Expanded(
              child: StreamBuilder<List<Cart>>(
                stream: cartCollection.streams,
                builder: (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const Center(child: CircularProgressIndicator());
                  }
                  final carts = snapshot.data ?? [];
                  if (carts.isEmpty) {
                    return const Center(child: Text('No carts available.'));
                  }
                  return ListView.builder(
                    itemCount: carts.length,
                    itemBuilder: (context, index) {
                      final cart = carts[index];
                      return ListTile(
                        title: Text(cart.name),
                        subtitle: Text('Price: ${cart.price} € | TTC: ${cart.priceWithTVA.toStringAsFixed(2)} €'),
                        trailing: cart.isExpensive
                            ? const Icon(Icons.warning, color: Colors.orange)
                            : const Icon(Icons.check_circle, color: Colors.green),
                      );
                    },
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Parameters

FirestorePlus

Parameter Type Required Description
tConstructor T Function(Map<String, dynamic>) Yes Constructor function for type-safe object creation
path String Yes Firestore collection path
uid String? No Document ID (default: "none")
app String? No Firebase app name
limit int No Query limit (default: 10)
subPath String? No Sub-collection path

CRUD Operations

Method Return Type Description
add(object) Future<String> Add document and return ID
update(object) Future<String> Update existing document
delete Future<String> Delete document
addByUid(object) Future<void> Add document with specific UID

Stream Operations

Property Return Type Description
stream Stream<T> Stream of single document
streams Stream<List<T>> Stream of collection
streamSubCollection Stream<Map<String, Stream<List<T>>>> Stream of sub-collections

Future Operations

Property Return Type Description
future Future<T> Single document
futures Future<List<T>> Collection
futureSubCollection Future<Map<String, Stream<List<T>>>> Sub-collections

Search Operations

Method Parameters Description
futureWithSearch searchField, search Search by single field
futureWithSearch2 searchField1, search1, searchField2, search2 Search by two fields
streamWithSearch searchField, search Stream search by single field
streamWithSearch2 searchField1, search1, searchField2, search2 Stream search by two fields

Sub-collection Operations

Method Parameters Description
subCollection<U> subPath, itemUid, tConstructor Create sub-collection instance

InterfacePlus

All models must implement this interface:

Property/Method Type Description
uid String? Document ID
json Map<String, dynamic> JSON serialization
withMap(map) InterfacePlus JSON deserialization

Best Practices

  1. Always implement InterfacePlus in your models for consistency
  2. Use meaningful model names and properties
  3. Handle errors appropriately in your streams and futures
  4. Use sub-collections for related data (e.g., user posts, order items)
  5. Leverage streams for real-time updates in your UI
  6. Use type-safe constructors for reliable object creation
  7. Implement business logic in your model classes (getters, methods)

Libraries

firestore_core_plus
Firebase Core Plus - A Flutter package for type-safe Firestore operations.
firestore_plus
interface_plus