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