riverpod_crud 0.0.2
riverpod_crud: ^0.0.2 copied to clipboard
A lightweight Riverpod CRUD helper for loading, error, refresh, pagination, create, update, and delete states.
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_crud/riverpod_crud.dart';
void main() {
runApp(const UserApp());
}
class User {
final int id;
final String name;
const User({
required this.id,
required this.name,
});
User copyWith({
int? id,
String? name,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
);
}
}
class FakeUserRepository implements CrudRepository<User, int> {
final List<User> _users = List.generate(
34,
(index) => User(id: index + 1, name: 'User ${index + 1}'),
);
// In a real app, these methods would call an API, database, or service.
@override
Future<List<User>> fetch({
int page = 1,
int limit = 20,
}) async {
await Future<void>.delayed(const Duration(milliseconds: 350));
final start = (page - 1) * limit;
if (start >= _users.length) {
return [];
}
final end = start + limit;
return _users.sublist(start, end > _users.length ? _users.length : end);
}
@override
Future<User> create(User item) async {
await Future<void>.delayed(const Duration(milliseconds: 250));
_users.insert(0, item);
return item;
}
@override
Future<User> update(int id, User item) async {
await Future<void>.delayed(const Duration(milliseconds: 250));
final index = _users.indexWhere((user) => user.id == id);
if (index == -1) {
throw const CrudException('User not found');
}
_users[index] = item;
return item;
}
@override
Future<void> delete(int id) async {
await Future<void>.delayed(const Duration(milliseconds: 250));
_users.removeWhere((user) => user.id == id);
}
}
// The provider wires the generic CRUD notifier to the user repository.
final usersProvider = crudNotifierProvider<User, int>(
repository: FakeUserRepository(),
getId: (user) => user.id,
paginationConfig: const PaginationConfig(limit: 10),
);
class UserApp extends StatelessWidget {
const UserApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'riverpod_crud example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
home: const UserPage(),
);
}
}
class UserPage extends StatefulWidget {
const UserPage({super.key});
@override
State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
late final ProviderContainer _container;
late final ProviderSubscription<CrudState<User>> _subscription;
late final CrudNotifier<User, int> _notifier;
CrudState<User> _state = const CrudState<User>();
@override
void initState() {
super.initState();
_container = ProviderContainer();
_notifier = _container.read(usersProvider.notifier);
_subscription = _container.listen<CrudState<User>>(
usersProvider,
(_, next) {
if (mounted) {
setState(() => _state = next);
}
},
fireImmediately: true,
);
Future<void>.microtask(
_notifier.fetchInitial,
);
}
@override
void dispose() {
_subscription.close();
_container.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Users'),
actions: [
IconButton(
tooltip: 'Refresh',
onPressed: _state.isRefreshing ? null : _notifier.refresh,
icon: const Icon(Icons.refresh),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
final id = DateTime.now().millisecondsSinceEpoch;
_notifier.create(User(id: id, name: 'New user $id'));
},
icon: const Icon(Icons.add),
label: const Text('Add'),
),
body: Column(
children: [
if (_state.error != null)
MaterialBanner(
content: Text(_state.error!),
actions: [
TextButton(
onPressed: _notifier.clearError,
child: const Text('Dismiss'),
),
],
),
if (_state.isLoading)
const Expanded(
child: Center(child: CircularProgressIndicator()),
)
else
Expanded(
child: RefreshIndicator(
onRefresh: _notifier.refresh,
child: ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 88),
itemCount: _state.items.length + 1,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
if (index == _state.items.length) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: FilledButton.icon(
onPressed: _state.hasMore && !_state.isLoadingMore
? _notifier.loadMore
: null,
icon: _state.isLoadingMore
? const SizedBox.square(
dimension: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.expand_more),
label: Text(
_state.hasMore ? 'Load more' : 'No more users',
),
),
),
);
}
final user = _state.items[index];
return ListTile(
title: Text(user.name),
subtitle: Text('ID: ${user.id}'),
trailing: Wrap(
spacing: 4,
children: [
IconButton(
tooltip: 'Update',
onPressed: () {
_notifier.update(
user.id,
user.copyWith(name: '${user.name} updated'),
);
},
icon: const Icon(Icons.edit),
),
IconButton(
tooltip: 'Delete',
onPressed: () => _notifier.delete(user.id),
icon: const Icon(Icons.delete_outline),
),
],
),
);
},
),
),
),
],
),
);
}
}