riverpod_sugar 1.0.0
riverpod_sugar: ^1.0.0 copied to clipboard
Essential Flutter widgets and utilities for flutter_riverpod. Includes RxWidget, AsyncValue helpers, form validation, and debouncing tools to reduce boilerplate code.
๐ฏ Riverpod Sugar #
Sweet utilities for flutter_riverpod
that reduce boilerplate and improve developer ergonomics without hiding the ref
object.
โจ Features #
- ๐ฏ RxWidget & RxBuilder: Cleaner widget definitions with less boilerplate
- โก AsyncValue.easyWhen: Simplified async state handling with sensible defaults
- ๐ Debouncer: Intelligent input debouncing for search and user interactions
- ๐ FormManager: Effortless form validation state management
- ๐ Provider Combiners: Combine multiple providers elegantly
- ๐ ๏ธ Common Validators: Pre-built validation functions for forms
๐ฆ Installation #
Add riverpod_sugar
to your pubspec.yaml
:
dependencies:
riverpod_sugar: ^1.0.0
Then import it in your Dart code:
import 'package:riverpod_sugar/riverpod_sugar.dart';
๐ Quick Start #
RxWidget - Cleaner ConsumerWidget #
Before (Standard Riverpod):
class CounterWidget extends ConsumerWidget {
const CounterWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
After (With RxWidget):
class CounterWidget extends RxWidget {
const CounterWidget({super.key});
@override
Widget buildRx(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('Count: $count');
}
}
easyWhen - Simplified AsyncValue Handling #
Before (Standard Riverpod):
ref.watch(userProvider).when(
data: (user) => Text('Hello ${user.name}!'),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
)
After (With easyWhen):
ref.watch(userProvider).easyWhen(
data: (user) => Text('Hello ${user.name}!'),
// loading & error widgets provided automatically!
)
Debouncer - Smart Search Input #
class SearchWidget extends RxStatefulWidget {
@override
State<SearchWidget> createState() => _SearchWidgetState();
}
class _SearchWidgetState extends RxState<SearchWidget> {
final _debouncer = Debouncer(milliseconds: 300);
@override
Widget buildRx(BuildContext context, WidgetRef ref) {
return TextField(
onChanged: (query) {
_debouncer.run(() {
ref.read(searchProvider.notifier).updateQuery(query);
});
},
);
}
@override
void dispose() {
_debouncer.dispose();
super.dispose();
}
}
๐ Detailed Examples #
Form Validation with FormManager #
final formManagerProvider = StateNotifierProvider<FormManager, FormState>((ref) {
return FormManager();
});
class RegistrationForm extends RxWidget {
@override
Widget buildRx(BuildContext context, WidgetRef ref) {
final formState = ref.watch(formManagerProvider);
final formManager = ref.read(formManagerProvider.notifier);
return Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
errorText: formState.getError('email'),
),
onChanged: (value) {
formManager.validateField(
'email',
value,
CommonValidators.combine([
CommonValidators.required('Email is required'),
CommonValidators.email('Please enter a valid email'),
]),
);
},
),
ElevatedButton(
onPressed: formState.isValid ? () => _submitForm() : null,
child: Text('Submit'),
),
],
);
}
}
Provider Combination #
final userProvider = FutureProvider<User>((ref) => fetchUser());
final settingsProvider = FutureProvider<Settings>((ref) => fetchSettings());
// Combine multiple providers
final combinedProvider = ProviderCombiners.combine2(
userProvider,
settingsProvider,
);
// Use in widget
class UserDashboard extends RxWidget {
@override
Widget buildRx(BuildContext context, WidgetRef ref) {
final combined = ref.watch(combinedProvider);
return combined.easyWhen(
data: ((user, settings)) => DashboardContent(user, settings),
);
}
}
Advanced AsyncValue Operations #
final userProvider = FutureProvider<User>((ref) => fetchUser());
// Transform async data
final userNameProvider = userProvider.mapData((user) => user.name);
// Check conditions on async data
final isAdminProvider = Provider<bool>((ref) {
final user = ref.watch(userProvider);
return user.hasDataWhere((user) => user.role == 'admin');
});
// Combine async providers
final dashboardDataProvider = AsyncProviderCombiners.combine3(
userProvider,
postsProvider,
notificationsProvider,
);
๐ฏ Core Components #
RxWidget Family #
Widget | Use Case |
---|---|
RxWidget |
Replaces ConsumerWidget with cleaner syntax |
RxStatefulWidget |
Replaces ConsumerStatefulWidget |
RxBuilder |
Inline reactive widgets without creating new classes |
RxShow |
Conditional rendering based on provider state |
AsyncValue Extensions #
Extension | Purpose |
---|---|
easyWhen() |
Simplified async state handling with defaults |
mapData() |
Transform data while preserving async state |
hasDataWhere() |
Check conditions on async data |
dataOrNull |
Get data or null safely |
Form Management #
Component | Purpose |
---|---|
FormManager |
Manages form validation state |
CommonValidators |
Pre-built validation functions |
FormState |
Immutable form state with error tracking |
Utilities #
Utility | Purpose |
---|---|
Debouncer |
Delay function execution (search, API calls) |
AdvancedDebouncer |
Advanced debouncing with leading/trailing options |
ProviderCombiners |
Combine multiple providers |
AsyncProviderCombiners |
Combine async providers intelligently |
๐ง Advanced Usage #
Custom Loading and Error Widgets #
userProvider.easyWhen(
data: (user) => UserProfile(user),
loading: () => CustomShimmer(),
error: (error, stack) => CustomErrorWidget(error),
)
Advanced Debouncing #
final advancedDebouncer = AdvancedDebouncer(
milliseconds: 300,
maxWait: 2000, // Force execution after 2 seconds
leading: true, // Execute immediately on first call
trailing: true, // Execute after delay
);
Multiple Field Validation #
formManager.validateFields({
'email': (emailValue, CommonValidators.email()),
'password': (passwordValue, CommonValidators.minLength(8)),
'confirmPassword': (confirmValue, CommonValidators.matches(passwordValue)),
});
๐จ Best Practices #
โ Do's #
- Use RxWidget for widgets that only consume providers
- Dispose Debouncer in StatefulWidgets
- Combine related providers for better performance
- Use CommonValidators for standard validation
- Leverage easyWhen for most async operations
โ Don'ts #
- Don't override both
build
andbuildRx
methods - Don't forget to dispose resources (Debouncer, controllers)
- Don't use RxWidget for widgets that don't need providers
- Don't ignore form validation errors
Performance Tips #
// โ
Good: Combine related providers
final dashboardProvider = ProviderCombiners.combine2(userProvider, settingsProvider);
// โ Avoid: Multiple separate watches
// final user = ref.watch(userProvider);
// final settings = ref.watch(settingsProvider);
// โ
Good: Use debouncer for expensive operations
final debouncer = Debouncer(milliseconds: 300);
onChanged: (query) => debouncer.run(() => search(query));
// โ Avoid: Direct expensive calls
// onChanged: (query) => search(query);
๐งช Testing #
Riverpod Sugar works seamlessly with Riverpod's testing utilities:
void main() {
test('RxWidget updates when provider changes', () async {
final container = ProviderContainer();
// Test your RxWidget with the container
await tester.pumpWidget(
ProviderScope(
parent: container,
child: MyRxWidget(),
),
);
// Verify behavior
expect(find.text('Initial Value'), findsOneWidget);
// Update provider and test
container.read(myProvider.notifier).state = 'Updated Value';
await tester.pump();
expect(find.text('Updated Value'), findsOneWidget);
});
}
๐ Migration Guide #
From ConsumerWidget to RxWidget #
-
Change the parent class:
// Before class MyWidget extends ConsumerWidget { // After class MyWidget extends RxWidget {
-
Rename the build method:
// Before Widget build(BuildContext context, WidgetRef ref) { // After Widget buildRx(BuildContext context, WidgetRef ref) {
-
That's it! Your existing code works unchanged.
From Manual AsyncValue Handling #
- Replace verbose when calls:
// Before asyncValue.when( data: (data) => MyWidget(data), loading: () => CircularProgressIndicator(), error: (e, s) => Text('Error: $e'), ) // After asyncValue.easyWhen( data: (data) => MyWidget(data), )
๐ค Contributing #
We welcome contributions! Here's how you can help:
- ๐ Report bugs - Open an issue with reproduction steps
- ๐ก Suggest features - We'd love to hear your ideas
- ๐ Improve docs - Help make the documentation even better
- ๐งช Add tests - More test coverage is always appreciated
- ๐ป Submit PRs - Bug fixes and features are welcome
Development Setup #
# Clone the repository
git clone https://github.com/mukhbit0/riverpod_sugar.git
# Install dependencies
flutter pub get
# Run tests
flutter test
# Run example app
cd example && flutter run
๐ License #
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments #
- Riverpod - The amazing state management solution this package extends
- Flutter Team - For the incredible framework
- Community - For feedback and suggestions that make this package better
๐ Package Stats #
- โ Null Safety: Full null safety support
- โ Flutter 3.10+: Compatible with latest Flutter versions
- โ Dart 3.0+: Uses latest Dart features
- โ Well Tested: Comprehensive test coverage
- โ Well Documented: Extensive documentation and examples
- โ
Zero Dependencies: Only depends on
flutter_riverpod
Made with โค๏ธ for the Flutter community
If you like this package, please give it a โญ on GitHub and a ๐ on pub.dev!