-
-
Notifications
You must be signed in to change notification settings - Fork 973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
docs(rest_api): add example for REST Api handling #3957
base: master
Are you sure you want to change the base?
Conversation
WalkthroughThis pull request introduces a comprehensive example of a REST API Flutter application using Riverpod for state management and Dio for HTTP requests. The project includes a complete implementation with a user management interface, featuring user listing, adding new users, error handling, and a full test suite. The example demonstrates modern Dart and Flutter development practices, including asynchronous state management, form validation, and comprehensive testing strategies. Changes
Sequence DiagramsequenceDiagram
participant UI as UserListScreen
participant State as UsersNotifier
participant Repo as UserRepository
participant API as REST API
UI->>State: Fetch Users
State->>Repo: fetchUsers()
Repo->>API: GET /users
API-->>Repo: Return User List
Repo-->>State: Update Users
State-->>UI: Render User List
UI->>State: Add New User
State->>Repo: createUser(newUser)
Repo->>API: POST /users
API-->>Repo: Confirm User Creation
Repo-->>State: Update State
State-->>UI: Refresh User List
Possibly related PRs
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (12)
examples/rest_api/analysis_options.yaml (1)
12-26
: Consider enabling additional linting rules for REST API code.For a REST API example, consider enabling these additional rules to enforce better practices:
always_specify_types
: Helps with API model claritysort_constructors_first
: Maintains consistent class organizationApply this diff to add these rules:
rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + always_specify_types: true + sort_constructors_first: trueexamples/rest_api/test/widget_test.dart (3)
11-45
: Enhance test coverage with more comprehensive test data and assertions.The test verifies basic functionality but could be more thorough.
Consider these improvements:
when(mockDio.get('https://jsonplaceholder.typicode.com/users')) .thenAnswer((_) async { await Future.delayed(const Duration(milliseconds: 100)); return Response( data: [ - {'id': 1, 'name': 'Test User', 'email': '[email protected]'} + {'id': 1, 'name': 'Test User', 'email': '[email protected]'}, + {'id': 2, 'name': 'Another User', 'email': '[email protected]'} ], statusCode: 200, requestOptions: RequestOptions(path: ''), ); }); // Act & Assert remains the same... // Add more assertions +expect(find.byType(ListTile), findsNWidgets(2)); +expect(find.text('Another User'), findsOneWidget); +expect(find.text('[email protected]'), findsOneWidget);
47-71
: Improve error handling test coverage.The error handling test could be more robust.
Consider these improvements:
// Act remains the same... // Assert +// Verify loading indicator is gone +expect(find.byType(CircularProgressIndicator), findsNothing); + +// Verify error message expect( find.text('Error: Exception: Failed to load users'), findsOneWidget); + +// Verify retry functionality +await tester.tap(find.byType(RefreshIndicator)); +await tester.pump(); +verify(mockDio.get('https://jsonplaceholder.typicode.com/users')).called(2);
73-106
: Enhance form testing coverage.The test could be more comprehensive in testing form validation and navigation.
Consider adding these test cases:
+// Test form validation +await tester.tap(find.byType(ElevatedButton)); +await tester.pumpAndSettle(); +expect(find.text('Please enter a name'), findsOneWidget); +expect(find.text('Please enter an email'), findsOneWidget); + +// Test invalid email +await tester.enterText(find.byType(TextFormField).first, 'New User'); +await tester.enterText(find.byType(TextFormField).last, 'invalid-email'); +await tester.tap(find.byType(ElevatedButton)); +await tester.pumpAndSettle(); +expect(find.text('Please enter a valid email'), findsOneWidget); + // Fill form with valid data await tester.enterText(find.byType(TextFormField).first, 'New User'); await tester.enterText( find.byType(TextFormField).last, '[email protected]'); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Verify API call verify(mockDio.post( 'https://jsonplaceholder.typicode.com/users', data: anyNamed('data'), )).called(1); + +// Verify navigation +expect(find.byType(AddUserScreen), findsNothing);examples/rest_api/test/user_test.dart (2)
14-71
: Add test cases for error scenarios and request configurations.The tests cover basic functionality but could be more comprehensive.
Consider adding these test cases:
test('fetchUsers handles network error', () async { when(mockDio.get('https://jsonplaceholder.typicode.com/users')) .thenThrow(DioException( requestOptions: RequestOptions(path: ''), error: 'Network error', )); expect( () => repository.fetchUsers(), throwsA(isA<Exception>().having( (e) => e.toString(), 'message', 'Exception: Failed to load users', )), ); }); test('createUser handles validation error', () async { when(mockDio.post( 'https://jsonplaceholder.typicode.com/users', data: any, )).thenThrow(DioException( requestOptions: RequestOptions(path: ''), response: Response( data: {'error': 'Invalid data'}, statusCode: 400, requestOptions: RequestOptions(path: ''), ), )); expect( () => repository.createUser(User(id: 0, name: '', email: '')), throwsA(isA<Exception>()), ); }); test('requests include correct headers', () async { await repository.fetchUsers(); verify(mockDio.get( any, options: argThat( predicate<Options>((options) => options.headers?['Content-Type'] == 'application/json' ), named: 'options', ), )).called(1); });
73-152
: Enhance state management testing coverage.The tests could better verify state transitions and error handling.
Consider adding these test cases:
test('loading state is shown during fetch', () async { // Arrange when(mockDio.get('https://jsonplaceholder.typicode.com/users')) .thenAnswer((_) async { await Future.delayed(const Duration(milliseconds: 100)); return Response( data: [], statusCode: 200, requestOptions: RequestOptions(path: ''), ); }); // Act & Assert expect( container.read(usersProvider), const AsyncValue<List<User>>.loading(), ); }); test('error state is shown on fetch failure', () async { // Arrange when(mockDio.get('https://jsonplaceholder.typicode.com/users')) .thenThrow(Exception('Network error')); // Act await expectLater( container.read(usersProvider.future), throwsException, ); // Assert expect( container.read(usersProvider), isA<AsyncError>(), ); }); test('state is preserved during refresh', () async { // Arrange initial state when(mockDio.get('https://jsonplaceholder.typicode.com/users')) .thenAnswer((_) async => Response( data: [ { 'id': 1, 'name': 'Initial User', 'email': '[email protected]', } ], statusCode: 200, requestOptions: RequestOptions(path: ''), )); // Wait for initial load await container.read(usersProvider.future); // Verify initial state expect(container.read(usersProvider).value?.length, 1); // Mock refresh response when(mockDio.get('https://jsonplaceholder.typicode.com/users')) .thenAnswer((_) async => Response( data: [ { 'id': 1, 'name': 'Updated User', 'email': '[email protected]', } ], statusCode: 200, requestOptions: RequestOptions(path: ''), )); // Act: Refresh the state await container.refresh(usersProvider.future); // Assert: Verify state is updated final users = container.read(usersProvider).value; expect(users?.length, 1); expect(users?.first.name, 'Updated User'); });examples/rest_api/lib/main.dart (3)
21-41
: Enhance the User model with validation and utility methods.The model class could be more robust and developer-friendly.
Consider these improvements:
class User { final int id; final String name; final String email; User({required this.id, required this.name, required this.email}); // Factory constructor for JSON deserialization factory User.fromJson(Map<String, dynamic> json) { + // Validate required fields + if (json['id'] == null) throw FormatException('Missing id'); + if (json['name'] == null) throw FormatException('Missing name'); + if (json['email'] == null) throw FormatException('Missing email'); + return User( id: json['id'], name: json['name'], email: json['email'], ); } // Method for JSON serialization Map<String, dynamic> toJson() => { 'id': id, 'name': name, 'email': email, }; + + // Utility method for creating copies + User copyWith({ + int? id, + String? name, + String? email, + }) => + User( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + ); + + @override + String toString() => 'User(id: $id, name: $name, email: $email)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is User && + other.id == id && + other.name == name && + other.email == email; + + @override + int get hashCode => Object.hash(id, name, email); }
89-106
: Enhance state management with caching and optimistic updates.The state management could be more efficient and user-friendly.
Consider these improvements:
class UsersNotifier extends AsyncNotifier<List<User>> { + static const _cacheTimeout = Duration(minutes: 5); + DateTime? _lastFetch; + @override Future<List<User>> build() async { + _lastFetch = DateTime.now(); final repository = ref.watch(userRepositoryProvider); return repository.fetchUsers(); } Future<void> addUser(User user) async { final repository = ref.watch(userRepositoryProvider); - state = const AsyncValue.loading(); + // Optimistic update + final currentUsers = state.value ?? []; + state = AsyncValue.data([...currentUsers, user]); + + try { + final newUser = await repository.createUser(user); + // Update with actual data from server + state = AsyncValue.data([...currentUsers, newUser]); + } catch (e) { + // Revert on error + state = AsyncValue.data(currentUsers); + rethrow; + } + } + + Future<void> refreshIfNeeded() async { + if (_lastFetch == null || + DateTime.now().difference(_lastFetch!) > _cacheTimeout) { + await build(); + } } }
109-150
: Add search and pagination for better user experience.The list screen could be more functional and user-friendly.
Consider these improvements:
class UserListScreen extends ConsumerWidget { const UserListScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final usersAsync = ref.watch(usersProvider); + final searchController = TextEditingController(); return Scaffold( appBar: AppBar( - title: const Text('Users'), + title: TextField( + controller: searchController, + decoration: const InputDecoration( + hintText: 'Search users...', + border: InputBorder.none, + ), + onChanged: (value) { + // TODO: Implement search functionality + }, + ), ), floatingActionButton: FloatingActionButton( onPressed: () => Navigator.push( context, MaterialPageRoute(builder: (context) => const AddUserScreen()), ), child: const Icon(Icons.add), ), body: switch (usersAsync) { AsyncData(:final value) => RefreshIndicator( onRefresh: () => ref.refresh(usersProvider.future), child: ListView.builder( itemCount: value.length, + // Add pagination + onEndReached: () { + // TODO: Load more users + }, + onEndReachedThreshold: 0.8, itemBuilder: (context, index) { final user = value[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), + onTap: () { + // TODO: Show user details + }, ); }, ), ), - AsyncError(:final error) => Center(child: Text('Error: $error')), + AsyncError(:final error) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Error: $error'), + ElevatedButton( + onPressed: () => ref.refresh(usersProvider), + child: const Text('Retry'), + ), + ], + ), + ), _ => const Center(child: CircularProgressIndicator()), }, ); } }examples/rest_api/pubspec.yaml (3)
1-4
: Remove trailing space on line 3.The
publish_to
line contains a trailing space that should be removed.Apply this diff to fix the formatting:
-publish_to: 'none' +publish_to: 'none'🧰 Tools
🪛 YAMLlint (1.35.1)
[error] 3-3: trailing spaces
(trailing-spaces)
8-14
: Remove unnecessary blank line between dependencies.The dependencies section looks good, with appropriate version constraints. However, there's an unnecessary blank line between
dio
andcupertino_icons
that should be removed for consistency.Apply this diff to fix the formatting:
dio: ^5.7.0 - cupertino_icons: ^1.0.8
24-27
: Remove consecutive blank lines in Flutter configuration.The Flutter configuration is correct, but there are unnecessary consecutive blank lines that should be removed for better readability.
Apply this diff to fix the formatting:
flutter: - uses-material-design: true
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
examples/rest_api/.metadata
(1 hunks)examples/rest_api/README.md
(1 hunks)examples/rest_api/analysis_options.yaml
(1 hunks)examples/rest_api/lib/main.dart
(1 hunks)examples/rest_api/pubspec.yaml
(1 hunks)examples/rest_api/test/user_test.dart
(1 hunks)examples/rest_api/test/user_test.mocks.dart
(1 hunks)examples/rest_api/test/widget_test.dart
(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- examples/rest_api/.metadata
- examples/rest_api/README.md
🧰 Additional context used
🪛 YAMLlint (1.35.1)
examples/rest_api/pubspec.yaml
[error] 3-3: trailing spaces
(trailing-spaces)
⏰ Context from checks skipped due to timeout of 90000ms (19)
- GitHub Check: build (master, packages/riverpod_generator/integration/build_yaml)
- GitHub Check: build (master, packages/riverpod_generator)
- GitHub Check: build (master, packages/riverpod_annotation)
- GitHub Check: build (master, packages/riverpod/example)
- GitHub Check: build (master, packages/riverpod)
- GitHub Check: build (stable, packages/hooks_riverpod/example)
- GitHub Check: build (stable, packages/flutter_riverpod/example)
- GitHub Check: build (stable, packages/flutter_riverpod)
- GitHub Check: riverpod_lint (stable, packages/riverpod_lint_flutter_test)
- GitHub Check: build (stable, examples/todos)
- GitHub Check: riverpod_lint (stable, packages/riverpod_analyzer_utils_tests)
- GitHub Check: build (stable, examples/stackoverflow)
- GitHub Check: build (stable, examples/random_number)
- GitHub Check: riverpod_lint (master, packages/riverpod_lint_flutter_test)
- GitHub Check: build (stable, examples/pub)
- GitHub Check: build (stable, examples/marvel)
- GitHub Check: riverpod_lint (master, packages/riverpod_analyzer_utils_tests)
- GitHub Check: build (stable, examples/counter)
- GitHub Check: check_generation
🔇 Additional comments (6)
examples/rest_api/analysis_options.yaml (3)
1-7
: LGTM! Clear and helpful documentation.The header provides comprehensive information about the analyzer's purpose and usage.
27-28
: LGTM! Helpful reference link provided.The link to additional documentation is valuable for users who need more information.
8-10
: Verify flutter_lints package dependency.The configuration includes flutter_lints, which is good practice. Let's verify that it's properly added to pubspec.yaml.
✅ Verification successful
✓ flutter_lints package is properly configured
The package is correctly added to pubspec.yaml with version ^5.0.0.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check if flutter_lints is properly added to pubspec.yaml # Look for flutter_lints in pubspec.yaml rg -A 2 "flutter_lints:" "examples/rest_api/pubspec.yaml"Length of output: 120
examples/rest_api/test/user_test.mocks.dart (1)
1-1035
: LGTM!This is an auto-generated mock file by Mockito. The implementation looks correct and complete.
examples/rest_api/pubspec.yaml (2)
6-7
: LGTM!The SDK constraint is well-defined and follows best practices by allowing patch and minor updates while preventing major version updates that could introduce breaking changes.
16-22
: LGTM!The dev dependencies are well-chosen for a comprehensive testing setup, including tools for widget testing, mocking, code generation, and static analysis.
class AddUserScreen extends ConsumerStatefulWidget { | ||
const AddUserScreen({super.key}); | ||
|
||
@override | ||
AddUserScreenState createState() => AddUserScreenState(); | ||
} | ||
|
||
class AddUserScreenState extends ConsumerState<AddUserScreen> { | ||
// Form key for validation | ||
final _formKey = GlobalKey<FormState>(); | ||
// Controllers for form fields | ||
final _nameController = TextEditingController(); | ||
final _emailController = TextEditingController(); | ||
|
||
// Clean up controllers when the widget is disposed | ||
@override | ||
void dispose() { | ||
_nameController.dispose(); | ||
_emailController.dispose(); | ||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
// Function to pop the context to avoid using | ||
//context across async gaps | ||
void popContext() { | ||
Navigator.pop(context); | ||
} | ||
|
||
return Scaffold( | ||
appBar: AppBar(title: const Text('Add User')), | ||
body: Padding( | ||
padding: const EdgeInsets.all(16.0), | ||
child: Form( | ||
key: _formKey, | ||
child: Column( | ||
children: [ | ||
// Name input field with validation | ||
TextFormField( | ||
controller: _nameController, | ||
decoration: const InputDecoration(labelText: 'Name'), | ||
validator: (value) { | ||
if (value == null || value.isEmpty) { | ||
return 'Please enter a name'; | ||
} | ||
return null; | ||
}, | ||
), | ||
// Email input field with validation | ||
TextFormField( | ||
controller: _emailController, | ||
decoration: const InputDecoration(labelText: 'Email'), | ||
validator: (value) { | ||
if (value == null || value.isEmpty) { | ||
return 'Please enter an email'; | ||
} | ||
if (!value.contains('@')) { | ||
return 'Please enter a valid email'; | ||
} | ||
return null; | ||
}, | ||
), | ||
const SizedBox(height: 16), | ||
ElevatedButton( | ||
onPressed: () async { | ||
if (_formKey.currentState!.validate()) { | ||
final newUser = User( | ||
id: 0, // API will assign the real ID | ||
name: _nameController.text, | ||
email: _emailController.text, | ||
); | ||
// Add user and return to previous screen | ||
await ref.read(usersProvider.notifier).addUser(newUser); | ||
popContext(); | ||
} | ||
}, | ||
child: const Text('Add User'), | ||
), | ||
], | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Improve form handling and user feedback.
The form could provide better feedback and validation.
Consider these improvements:
class AddUserScreenState extends ConsumerState<AddUserScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
+ bool _isSubmitting = false;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
void popContext() {
Navigator.pop(context);
}
+ String? validateEmail(String? value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter an email';
+ }
+ final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
+ if (!emailRegex.hasMatch(value)) {
+ return 'Please enter a valid email';
+ }
+ return null;
+ }
return Scaffold(
appBar: AppBar(title: const Text('Add User')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Name',
+ prefixIcon: Icon(Icons.person),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a name';
}
+ if (value.length < 2) {
+ return 'Name must be at least 2 characters';
+ }
return null;
},
+ textInputAction: TextInputAction.next,
),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
+ prefixIcon: Icon(Icons.email),
),
- validator: (value) {
- if (value == null || value.isEmpty) {
- return 'Please enter an email';
- }
- if (!value.contains('@')) {
- return 'Please enter a valid email';
- }
- return null;
- },
+ validator: validateEmail,
+ keyboardType: TextInputType.emailAddress,
+ textInputAction: TextInputAction.done,
),
const SizedBox(height: 16),
- ElevatedButton(
+ _isSubmitting
+ ? const CircularProgressIndicator()
+ : ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
+ setState(() => _isSubmitting = true);
final newUser = User(
id: 0,
name: _nameController.text,
email: _emailController.text,
);
- await ref.read(usersProvider.notifier).addUser(newUser);
- popContext();
+ try {
+ await ref.read(usersProvider.notifier).addUser(newUser);
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('User added successfully'),
+ backgroundColor: Colors.green,
+ ),
+ );
+ popContext();
+ } catch (e) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Error: $e'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ } finally {
+ setState(() => _isSubmitting = false);
+ }
}
},
child: const Text('Add User'),
),
],
),
),
),
);
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
class AddUserScreen extends ConsumerStatefulWidget { | |
const AddUserScreen({super.key}); | |
@override | |
AddUserScreenState createState() => AddUserScreenState(); | |
} | |
class AddUserScreenState extends ConsumerState<AddUserScreen> { | |
// Form key for validation | |
final _formKey = GlobalKey<FormState>(); | |
// Controllers for form fields | |
final _nameController = TextEditingController(); | |
final _emailController = TextEditingController(); | |
// Clean up controllers when the widget is disposed | |
@override | |
void dispose() { | |
_nameController.dispose(); | |
_emailController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
// Function to pop the context to avoid using | |
//context across async gaps | |
void popContext() { | |
Navigator.pop(context); | |
} | |
return Scaffold( | |
appBar: AppBar(title: const Text('Add User')), | |
body: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Form( | |
key: _formKey, | |
child: Column( | |
children: [ | |
// Name input field with validation | |
TextFormField( | |
controller: _nameController, | |
decoration: const InputDecoration(labelText: 'Name'), | |
validator: (value) { | |
if (value == null || value.isEmpty) { | |
return 'Please enter a name'; | |
} | |
return null; | |
}, | |
), | |
// Email input field with validation | |
TextFormField( | |
controller: _emailController, | |
decoration: const InputDecoration(labelText: 'Email'), | |
validator: (value) { | |
if (value == null || value.isEmpty) { | |
return 'Please enter an email'; | |
} | |
if (!value.contains('@')) { | |
return 'Please enter a valid email'; | |
} | |
return null; | |
}, | |
), | |
const SizedBox(height: 16), | |
ElevatedButton( | |
onPressed: () async { | |
if (_formKey.currentState!.validate()) { | |
final newUser = User( | |
id: 0, // API will assign the real ID | |
name: _nameController.text, | |
email: _emailController.text, | |
); | |
// Add user and return to previous screen | |
await ref.read(usersProvider.notifier).addUser(newUser); | |
popContext(); | |
} | |
}, | |
child: const Text('Add User'), | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
} | |
class AddUserScreen extends ConsumerStatefulWidget { | |
const AddUserScreen({super.key}); | |
@override | |
AddUserScreenState createState() => AddUserScreenState(); | |
} | |
class AddUserScreenState extends ConsumerState<AddUserScreen> { | |
final _formKey = GlobalKey<FormState>(); | |
final _nameController = TextEditingController(); | |
final _emailController = TextEditingController(); | |
bool _isSubmitting = false; | |
@override | |
void dispose() { | |
_nameController.dispose(); | |
_emailController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
void popContext() { | |
Navigator.pop(context); | |
} | |
String? validateEmail(String? value) { | |
if (value == null || value.isEmpty) { | |
return 'Please enter an email'; | |
} | |
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); | |
if (!emailRegex.hasMatch(value)) { | |
return 'Please enter a valid email'; | |
} | |
return null; | |
} | |
return Scaffold( | |
appBar: AppBar(title: const Text('Add User')), | |
body: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Form( | |
key: _formKey, | |
child: Column( | |
children: [ | |
TextFormField( | |
controller: _nameController, | |
decoration: const InputDecoration( | |
labelText: 'Name', | |
prefixIcon: Icon(Icons.person), | |
), | |
validator: (value) { | |
if (value == null || value.isEmpty) { | |
return 'Please enter a name'; | |
} | |
if (value.length < 2) { | |
return 'Name must be at least 2 characters'; | |
} | |
return null; | |
}, | |
textInputAction: TextInputAction.next, | |
), | |
TextFormField( | |
controller: _emailController, | |
decoration: const InputDecoration( | |
labelText: 'Email', | |
prefixIcon: Icon(Icons.email), | |
), | |
validator: validateEmail, | |
keyboardType: TextInputType.emailAddress, | |
textInputAction: TextInputAction.done, | |
), | |
const SizedBox(height: 16), | |
_isSubmitting | |
? const CircularProgressIndicator() | |
: ElevatedButton( | |
onPressed: () async { | |
if (_formKey.currentState!.validate()) { | |
setState(() => _isSubmitting = true); | |
final newUser = User( | |
id: 0, | |
name: _nameController.text, | |
email: _emailController.text, | |
); | |
try { | |
await ref.read(usersProvider.notifier).addUser(newUser); | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
content: Text('User added successfully'), | |
backgroundColor: Colors.green, | |
), | |
); | |
popContext(); | |
} catch (e) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar( | |
content: Text('Error: $e'), | |
backgroundColor: Colors.red, | |
), | |
); | |
} finally { | |
setState(() => _isSubmitting = false); | |
} | |
} | |
}, | |
child: const Text('Add User'), | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
} |
The changes proposed by Also the failing code-generation check I don't fully understand yet. But I'll investigate it further asap :) |
Related Issues
related to: #3932
Add example for handling
REST API
with riverpod.Please let me know when there is anything to improve :) I'll do it asap 💪 🚀
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]
).I have updated the
CHANGELOG.md
of the relevant packages.Changelog files must be edited under the form:
If this contains new features or behavior changes,
I have updated the documentation to match those changes.
Summary by CodeRabbit
New Features
Documentation
Testing