A powerful local caching system for Flutter applications that work with JSON APIs. This package provides automatic model caching, reactive programming with streams, local persistence, and seamless HTTP operations.
- 🚀 Automatic Model Caching - In-memory caching with automatic lifecycle management
- 🔄 Reactive Programming - Stream-based updates for real-time UI synchronization
- 💾 Local Persistence - Secure local storage with
flutter_secure_storage - 🌐 HTTP Integration - Built-in CRUD operations over REST APIs
- 🔗 Model Associations - Support for single and multi-model relationships
- 📊 Stream States - Loading, reloading, and loaded states for better UX
- 🎯 Type Safety - Full generic type support throughout the API
Add this package to your pubspec.yaml:
dependencies:
flutter_model_cache: ^0.0.4Then run:
flutter pub getimport 'package:flutter_model_cache/flutter_model_cache.dart';
class Animal extends Model {
final String name;
final String species;
Animal({
required this.name,
required this.species,
super.id,
super.lastUpdated,
});
static const String classCollectionName = 'animals';
@override
String get collectionName => classCollectionName;
factory Animal.fromJson(Map<String, Object?> json) {
return Animal(
name: json['name'] as String,
species: json['species'] as String,
id: json['id'] as int?,
lastUpdated: Model.dateTimeFromJsonOrNull(json['lastUpdated']),
);
}
@override
Map<String, Object?> toJson() {
return {
...super.toJson(),
'name': name,
'species': species,
};
}
}You should do this during app setup for all your models.
void main() async {
final modelFactory = ModelFactory();
// Register model class with factory
modelFactory.registerModelClass(
Animal.classCollectionName,
(json) => Animal.fromJson(json),
);
runApp(MyApp());
}// Find all animals (retrieve them from the server)
Future<List<Animal>> getAllAnimals() async {
return await ModelFactory().findModels<Animal>(Animal.classCollectionName);
}
// Find specific animal
Future<Animal?> getAnimal(int id) async {
return await ModelFactory().findModel<Animal>(Animal.classCollectionName, id);
}
// Save animal
Future<Animal> saveAnimal(Animal animal) async {
return await animal.save<Animal>();
}
// Delete animal
Future<Animal?> deleteAnimal(Animal animal) async {
return await animal.delete<Animal>();
}class AnimalDetailWidget extends StatelessWidget {
final int animalId;
const AnimalDetailWidget({required this.animalId});
@override
Widget build(BuildContext context) {
final modelFactory = ModelFactory();
return StreamBuilder<Animal?>(
stream: modelFactory.findAndStreamModel<Animal>(
Animal.classCollectionName,
animalId,
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
final animal = snapshot.data;
if (animal == null) {
return Text('Animal not found');
}
return Column(
children: [
Text('Name: ${animal.name}'),
Text('Species: ${animal.species}'),
],
);
},
);
}
}class AnimalListWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final modelFactory = ModelFactory();
return StreamBuilder<List<Animal>>(
stream: modelFactory.findAndStreamModels<Animal>(
Animal.classCollectionName,
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
final animals = snapshot.data ?? [];
return ListView.builder(
itemCount: animals.length,
itemBuilder: (context, index) {
final animal = animals[index];
return ListTile(
title: Text(animal.name),
subtitle: Text(animal.species),
);
},
);
},
);
}
}class AnimalListWithStatesWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final modelFactory = ModelFactory();
return StreamBuilder<ModelListStream<Animal>>(
stream: modelFactory.findAndModelListStreamModels<Animal>(
Animal.classCollectionName,
),
builder: (context, snapshot) {
final modelListStream = snapshot.data;
if (modelListStream?.streamState == StreamStates.loading) {
return CircularProgressIndicator();
}
if (modelListStream?.streamState == StreamStates.reloading) {
return Column(
children: [
LinearProgressIndicator(),
Expanded(child: _buildList(modelListStream)),
],
);
}
return _buildList(modelListStream);
},
);
}
Widget _buildList(ModelListStream<Animal>? animals) {
if (animals == null || animals.isEmpty) {
return Text('No animals found');
}
return ListView.builder(
itemCount: animals.length,
itemBuilder: (context, index) {
final animal = animals[index];
return ListTile(
title: Text(animal.name),
subtitle: Text(animal.species),
);
},
);
}
}class Post extends Model {
final String title;
final String content;
final SingleAssociation<User> author;
Post({
required this.title,
required this.content,
required this.author,
super.id,
super.lastUpdated,
});
static const String classCollectionName = 'posts';
@override
String get collectionName => classCollectionName;
factory Post.fromJson(Map<String, Object?> json) {
return Post(
title: json['title'] as String,
content: json['content'] as String,
author: SingleAssociation.fromJson(
User.classCollectionName,
json['author'] as Map<String, Object?>,
),
id: json['id'] as int?,
lastUpdated: Model.dateTimeFromJsonOrNull(json['lastUpdated']),
);
}
}
// Usage
class PostWidget extends StatelessWidget {
final Post post;
const PostWidget({required this.post});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(post.title),
Text(post.content),
StreamBuilder<User?>(
stream: post.author.findAndStream,
builder: (context, snapshot) {
final author = snapshot.data;
return Text('By: ${author?.name ?? 'Loading...'}');
},
),
],
);
}
}void main() {
final modelFactory = ModelFactory();
// Register custom HTTP client for authentication, logging, etc.
modelFactory.registerHttpClient(
AuthenticatedHttpClient(), // Your custom http.BaseClient
);
}Future<List<Animal>> getAnimalsBySpecies(String species) async {
final modelFactory = ModelFactory();
return await modelFactory.queryModels<Animal>(
Animal.classCollectionName,
queryParams: {'species': species},
);
}// Get cached model without triggering network request
Animal? getCachedAnimal(int id) {
final modelFactory = ModelFactory();
return modelFactory.peekModel<Animal>(
collection: Animal.classCollectionName,
id: id,
);
}
// Get all cached models
List<Animal> getAllCachedAnimals() {
final modelFactory = ModelFactory();
return modelFactory.peekModels<Animal>(
collection: Animal.classCollectionName,
);
}Base class for all cached models.
Properties:
int? id- Unique identifierDateTime lastUpdated- Last modification timestampbool deleted- Deletion flagString collectionName- Collection identifier (abstract)
Methods:
Future<T> save<T>()- Save model to server and cacheFuture<T?> delete<T>()- Delete model from server and cacheFuture<T> reload<T>()- Reload model from serverMap<String, Object?> toJson()- Serialize to JSON
Singleton factory for model operations.
Key Methods:
registerModelClass<T>(String, ModelFromJson<T>)- Register model typeFuture<T?> findModel<T>(String, int)- Find single modelFuture<List<T>> findModels<T>(String)- Find all modelsFuture<List<T>> queryModels<T>(String, {Map<String, dynamic>?})- Query with parametersStream<T?> findAndStreamModel<T>(String, int)- Stream single modelStream<List<T>> findAndStreamModels<T>(String)- Stream model listT? peekModel<T>({required String, required int})- Get cached modelList<T> peekModels<T>({required String})- Get cached models
Available stream states for loading management:
StreamStates.none- No operationStreamStates.loading- Initial loadStreamStates.reloading- Refresh operationStreamStates.loaded- Load completed
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.