appwrite_offline 0.0.5 appwrite_offline: ^0.0.5 copied to clipboard
A Flutter Data adapter for Appwrite that provides offline support, real-time updates, and seamless integration with Flutter Data's powerful features.
Appwrite Offline #
A Flutter Data adapter for Appwrite that provides offline support, real-time updates, and seamless integration with Flutter Data's powerful features.
Features #
- 🔄 Automatic Synchronization: Seamlessly sync data between local storage and Appwrite
- 📱 Offline Support: Work with your data even when offline
- ⚡ Real-time Updates: Listen to changes in your Appwrite collections in real-time
- 🔍 Advanced Querying: Supported operators: '==', '!=', '>', '>=', '<', '<=', 'startsWith', 'endsWith', 'contains', 'search', 'between', 'in', 'isNull', 'isNotNull'.
- 🎯 Type-safe: Fully typed models, queries, & partial updates
- 🪄 Easy Integration: Simple setup process with minimal configuration
Prerequisites #
Before using this package, make sure you:
- Have a working Appwrite backend setup
- Understand and follow Flutter Data's setup guide carefully
- Are using Riverpod for state management
This package only handles the Appwrite integration with Flutter Data. All other Flutter Data configurations must be properly set up.
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
appwrite_offline: ^0.0.5
Setup #
- Initialize Appwrite Offline in your app:
void main() {
// Initialize Appwrite Offline
AppwriteOffline.initialize(
projectId: 'your_project_id',
databaseId: 'your_database_id',
endpoint: 'https://your-appwrite-instance.com/v1',
jwt: 'optional-jwt-token', // If using authentication
);
runApp(
ProviderScope(
child: MyApp(),
),
);
}
- Create your model class:
@DataRepository([AppwriteAdapter])
@JsonSerializable()
class Product extends DataModel<Product> {
@override
@JsonKey(readValue: $)
final String? id;
final String name;
final double price;
final BelongsTo<Category>? category;
final HasMany<Review>? reviews;
@JsonKey(readValue: $)
final DateTime? createdAt;
@JsonKey(readValue: $)
final DateTime? updatedAt;
Product({
this.id,
required this.name,
required this.price,
this.category,
this.reviews,
this.createdAt,
this.updatedAt,
});
factory Product.fromJson(Map<String, dynamic> json) =>
_$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
- Run the code generator:
dart run build_runner build -d
OR
dart run build_runner watch
Usage #
Basic Operations #
class ProductsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch all products with offline sync support
final productsState = ref.products.watchAll(
syncLocal: true,
);
if (productsState.isLoading) {
return CircularProgressIndicator();
}
if (productsState.hasException) {
return Text('Error: ${productsState.exception}');
}
final products = productsState.model;
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
);
},
);
}
}
Creating and Updating #
class ProductFormScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
// Create new product
final newProduct = await Product(
name: 'New Product',
price: 99.99,
).save();
// Update existing product
final updatedProduct = await Product(
name: 'Updated Product',
price: 149.99,
).withKeyOf(existingProduct).save();
// Partially update existing product
final updatedProduct = await Product(
name: 'Product Name',
price: 239.99,
).withKeyOf(existingProduct).save(
params: {
"updatedFields": jsonEncode(['price']),
// This informs the adapter to only send these fields, hence improving performance
},
);
},
child: Text('Save Product'),
);
}
}
Watching with Relationships #
class ProductDetailsScreen extends ConsumerWidget {
final String productId;
const ProductDetailsScreen({required this.productId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productState = ref.products.watchOne(
productId,
syncLocal: true,
alsoWatch: (prod) => [
prod.category,
prod.reviews,
],
);
if (productState.isLoading) {
return CircularProgressIndicator();
}
if (productState.hasException) {
return Text('Error: ${productState.exception}');
}
if (!productState.hasModel) {
return Text('Product not found');
}
final product = productState.model;
final category = product.category?.value;
final reviews = product.reviews?.toList() ?? [];
return Column(
children: [
Text(product.name),
if (category != null)
Text('Category: ${category.name}'),
Text('Reviews (${reviews.length}):'),
...reviews.map((review) => Text(review.text)),
],
);
}
}
Real-time Updates #
class ProductUpdatesScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return StreamBuilder<RealtimeMessage>(
stream: ref.products.appwriteAdapter.subscribe(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final message = snapshot.data!;
return ListTile(
title: Text('Collection Update'),
subtitle: Text('Event: ${message.event}'),
);
}
return SizedBox();
},
);
}
}
Advanced Queries #
class FilteredProductsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsState = ref.products.watchAll(
syncLocal: true,
params: {
'where': {
'price': {'>=', 100},
'name': {'contains': 'Premium'}
},
'order': 'price:DESC,name:ASC',
'limit': 10,
'offset': 0,
},
);
if (productsState.isLoading) {
return CircularProgressIndicator();
}
if (productsState.hasException) {
return Text('Error: ${productsState.exception}');
}
final products = productsState.model;
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
);
},
);
}
}
Working with Permissions #
Appwrite permissions can be set when creating or updating documents by passing them as params to the save method.
class ProductFormScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
// Create product with permissions
final newProduct = await Product(
name: 'New Product',
price: 99.99,
).save(
params: {
'permissions': jsonEncode([
{
'action': 'read',
'role': {
'type': 'any',
}
},
{
'action': 'write',
'role': {
'type': 'user',
'value': 'user_id',
}
},
{
'action': 'update',
'role': {
'type': 'team',
'value': 'team_id',
}
},
{
'action': 'delete',
'role': {
'type': 'team:*', // All teams
}
},
]),
},
);
},
child: Text('Save Product'),
);
}
}
Available permission configurations:
- Actions: 'read', 'write', 'create', 'update', 'delete'
- Role Types:
- 'any': Any user
- 'users': All authenticated users
- 'user': Specific user (requires value)
- 'team': Specific team (requires value)
- 'team:*': All teams
Working with HasMany Relationships #
class ProductReviewsScreen extends ConsumerWidget {
final String productId;
const ProductReviewsScreen({required this.productId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productState = ref.products.watchOne(
productId,
syncLocal: true,
alsoWatch: (prod) => [prod.reviews],
);
if (productState.isLoading) {
return CircularProgressIndicator();
}
if (productState.hasException) {
return Text('Error: ${productState.exception}');
}
if (!productState.hasModel) {
return Text('Product not found');
}
final product = productState.model;
// Always use toList() for HasMany relationships
final reviews = product.reviews?.toList() ?? [];
return Column(
children: [
Text('${product.name} - Reviews'),
if (reviews.isEmpty)
Text('No reviews yet'),
...reviews.map((review) => ReviewCard(review)),
],
);
}
}
Important Notes #
DataState Handling #
Always handle all DataState conditions in your widgets:
isLoading
: Initial loading statehasException
: Error state withexception
detailshasModel
: Whether the model is availablemodel
: The actual data
Relationship Best Practices #
-
Always use
toList()
when accessing HasMany relationships:// Correct final reviews = product.reviews?.toList() ?? []; // Incorrect final reviews = product.reviews?.value; // Don't use .value for HasMany
-
Use
alsoWatch
for efficient relationship loading:final productState = ref.products.watchOne( productId, syncLocal: true, alsoWatch: (prod) => [ prod.category, // BelongsTo relationship prod.reviews, // HasMany relationship ], );
Offline Synchronization #
-
Always use
syncLocal: true
when watching data to enable offline support:ref.products.watchAll(syncLocal: true); ref.products.watchOne(id, syncLocal: true);
-
Data will be automatically synchronized when the connection is restored
Convention Guidelines #
- Collection IDs should be the plural form of the model name (e.g., "products" for Product model)
- Model names should be in PascalCase (e.g., ProductVariant)
- All required Appwrite collection attributes should be defined in the model class
- Use
@JsonKey(readValue: $)
for Appwrite metadata fields (id, createdAt, updatedAt) - Follow Flutter Data's relationship conventions for BelongsTo and HasMany
Common Issues and Solutions #
- Collection Not Found: Ensure your Appwrite collection ID matches the plural form of your model name
- Permission Denied: Check if permissions are properly set in the save method params
- Relationship Loading Issues: Make sure to use
alsoWatch
andtoList()
appropriately - Offline Data Not Syncing: Verify
syncLocal: true
is set when watching data
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request. Here's how you can contribute:
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature
) - Commit your changes (
git commit -m 'Add some AmazingFeature'
) - Push to the branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
License #
This project is licensed under the MIT License - see the LICENSE file for details.
Additional Resources #
Support #
If you find this package helpful, please give it a ⭐️ on GitHub!
For bugs or feature requests, please create an issue.
Acknowledgments #
- Thanks to the Flutter Data team for their excellent work
- Thanks to the Appwrite team for their fantastic backend solution
- Thanks to all contributors who help improve this package