-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor/cubit to notifier migration #42
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
Open
aydinguven-leancode
wants to merge
14
commits into
refactor/forms-rework
Choose a base branch
from
refactor/cubit-to-notifier-migration
base: refactor/forms-rework
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
1f42255
[LMG-394] Migration from Cubit to Value Notifier based field state ma…
aydinguven-leancode b0203ec
[LMG-404] updated readme.md and changelog entries on LMG-394
aydinguven-leancode c27ab28
Potential fix for pull request finding
aydinguven-leancode a9ea596
Potential fix for pull request finding
aydinguven-leancode 7386026
A few minor fixes on PR
aydinguven-leancode e775e70
Merge branch 'refactor/cubit-to-notifier-migration' of https://github…
aydinguven-leancode 7035ad0
PR fixes
aydinguven-leancode f1a28f3
Changed context.read to context.select in examples
aydinguven-leancode 90fc15e
Replaced .map with for loop
aydinguven-leancode 8c3e4a8
Refactor on widget structure
aydinguven-leancode 5e13970
Renamed FormGroupController into FormGroup
aydinguven-leancode 6f66cac
Removed `state` alias from FieldState and FormState classes
aydinguven-leancode aed4e73
Renamed all classes with an "Advanced" prefix
aydinguven-leancode b6682ec
Changed the extension of ValueNotifier into ValueListenable
aydinguven-leancode File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| # Examples | ||
|
|
||
| ## Using `FieldBuilder` (recommended) | ||
|
|
||
| Common form scenarios using `FieldBuilder` — a thin wrapper around `ValueListenableBuilder` that saves typing the `<FieldState<T, E>>` type argument and reads as "build for a field" at call sites. | ||
|
|
||
| ### 1. A simple text field with an error message | ||
|
|
||
| ```dart | ||
| class _MyError {} | ||
|
|
||
| final field = TextFieldController<_MyError>( | ||
| initialValue: '', | ||
| validator: filled(_MyError()), | ||
| ); | ||
|
|
||
| // In the widget tree: | ||
| FieldBuilder<String, _MyError>( | ||
| field: field, | ||
| builder: (context, state) { | ||
| return TextFormField( | ||
| controller: field.textController, // <-- no manual wiring | ||
| decoration: InputDecoration( | ||
| errorText: state.error != null ? 'Required' : null, | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
| ``` | ||
|
|
||
| Note the `controller: field.textController` — `TextFieldController` owns its own `TextEditingController`, kept in two-way sync with the field value. Programmatic resets (`field.reset()`, `field.clear()`) propagate to the visible text automatically. | ||
|
|
||
| ### 2. A form with submit-time validation | ||
|
|
||
| ```dart | ||
| class SimpleFormController extends FormController { | ||
| SimpleFormController() { | ||
| registerFields([firstName, email]); | ||
| } | ||
|
|
||
| final firstName = TextFieldController( | ||
| initialValue: 'John', | ||
| validator: filled(ValidationError.empty), | ||
| ); | ||
|
|
||
| final email = TextFieldController( | ||
| validator: filled(ValidationError.empty), | ||
| ); | ||
|
|
||
| void submit() { | ||
| if (validate()) { | ||
| print('First: ${firstName.value.value}'); | ||
| print('Email: ${email.value.value}'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Provider (using the `provider` package): | ||
| ChangeNotifierProvider<SimpleFormController>( | ||
| create: (_) => SimpleFormController(), | ||
| child: const SimpleForm(), | ||
| ) | ||
|
|
||
| // Inside the SimpleForm widget — context.read is the right call here, | ||
| // because we want a stable reference for the onPressed callback (no | ||
| // subscription to rebuilds). | ||
| ElevatedButton( | ||
| onPressed: () => context.read<SimpleFormController>().submit(), | ||
| child: const Text('Submit'), | ||
| ) | ||
| ``` | ||
|
|
||
| ### 3. Async email validation | ||
|
|
||
| ```dart | ||
| class SignupController extends FormController { | ||
| SignupController() { | ||
| registerFields([email]); | ||
| } | ||
|
|
||
| final email = TextFieldController( | ||
| validator: filled(ValidationError.empty), | ||
| asyncValidator: _checkEmail, | ||
| asyncValidationDebounce: const Duration(milliseconds: 500), | ||
| ); | ||
|
|
||
| Future<ValidationError?> _checkEmail(String value) async { | ||
| final taken = ['taken@example.com']; | ||
| await Future<void>.delayed(const Duration(milliseconds: 700)); | ||
| return taken.contains(value) ? ValidationError.emailTaken : null; | ||
| } | ||
| } | ||
|
|
||
| // Let's select field reference | ||
| final email = context.select<SignupController, TextFieldController<ValidationError>>( | ||
| (c) => c.email, | ||
| ); | ||
|
|
||
| return FieldBuilder<String, ValidationError>( | ||
| field: email, | ||
| builder: (context, state) => TextFormField( | ||
| controller: email.textController, | ||
| decoration: InputDecoration( | ||
| errorText: state.error?.toString(), | ||
| suffix: state.isValidating | ||
| ? const SizedBox.square( | ||
| dimension: 16, | ||
| child: CircularProgressIndicator(), | ||
| ) | ||
| : null, | ||
| ), | ||
| ), | ||
| ); | ||
| ``` | ||
|
|
||
| ### 4. Cross-field validation (password / repeat-password) | ||
|
|
||
| ```dart | ||
| class PasswordFormController extends FormController { | ||
| PasswordFormController() { | ||
| registerFields([password, repeatPassword]); | ||
| } | ||
|
|
||
| final password = TextFieldController( | ||
| validator: atLeastLength(8, ValidationError.toShort), | ||
| ); | ||
|
|
||
| late final repeatPassword = TextFieldController<ValidationError>( | ||
| validator: (value) => | ||
| value == password.value.value ? null : ValidationError.doesNotMatch, | ||
| )..subscribeToFields([password]); | ||
| } | ||
| ``` | ||
|
|
||
| `subscribeToFields` listens to the given fields and re-runs this field's validator whenever any of their values change. | ||
|
|
||
| ### 5. A reusable custom form widget | ||
|
|
||
| Wrap `FieldBuilder` in a `StatelessWidget` to keep call sites tidy across a real app: | ||
|
|
||
| ```dart | ||
| class FormTextField<E extends Object> extends StatelessWidget { | ||
| const FormTextField({ | ||
| super.key, | ||
| required this.field, | ||
| required this.translateError, | ||
| this.labelText, | ||
| }); | ||
|
|
||
| final TextFieldController<E> field; | ||
| final ErrorTranslator<E> translateError; | ||
| final String? labelText; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return FieldBuilder<String, E>( | ||
| field: field, | ||
| builder: (context, state) => TextFormField( | ||
| controller: field.textController, | ||
| decoration: InputDecoration( | ||
| labelText: labelText, | ||
| errorText: state.error != null ? translateError(state.error!) : null, | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Call site: | ||
| ```dart | ||
| FormTextField( | ||
| field: controller.email, | ||
| translateError: validatorTranslator, | ||
| labelText: 'Email', | ||
| ) | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Using `ValueListenableBuilder` directly | ||
|
|
||
| `FieldBuilder` is shorthand. When you want the SDK primitive instead, drop down to `ValueListenableBuilder`. Reasons to reach for it: | ||
|
|
||
| - **The `child:` optimization** — keep an expensive subtree out of the rebuild path. | ||
| - **No extra import** beyond `flutter/widgets.dart` (`FieldBuilder` adds a dependency on `package:leancode_forms`). | ||
| - **Consistency with other notifier-based code** in your codebase. | ||
|
|
||
| Trade-off: you have to type the `<FieldState<T, E>>` type argument explicitly. | ||
|
|
||
| ### 1. A simple text field — explicit form | ||
|
|
||
| ```dart | ||
| ValueListenableBuilder<FieldState<String, _MyError>>( | ||
| valueListenable: field, | ||
| builder: (context, state, _) => TextFormField( | ||
| controller: field.textController, | ||
| decoration: InputDecoration( | ||
| errorText: state.error != null ? 'Required' : null, | ||
| ), | ||
| ), | ||
| ); | ||
| ``` | ||
|
|
||
| Same output as the `FieldBuilder` version. Six extra characters of type argument, one extra `_` for the unused `child` slot. | ||
|
|
||
| ### 2. Using the `child:` optimization | ||
|
|
||
| If part of the subtree is expensive but invariant in the field state, pass it as `child:` so it's built once and reused on every rebuild: | ||
|
|
||
| ```dart | ||
| ValueListenableBuilder<FieldState<String, _MyError>>( | ||
| valueListenable: field, | ||
| child: const _ExpensiveLeadingIcon(), // built once | ||
| builder: (context, state, child) { | ||
| return Row( | ||
| children: [ | ||
| child!, // reused on every rebuild | ||
| Expanded( | ||
| child: TextFormField( | ||
| controller: field.textController, | ||
| decoration: InputDecoration( | ||
| errorText: state.error != null ? 'Required' : null, | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ); | ||
| }, | ||
| ); | ||
| ``` | ||
|
|
||
| `FieldBuilder` doesn't expose `child:` (the goal there is brevity, not knobs). If you need this optimization, `ValueListenableBuilder` is the right tool. | ||
|
|
||
| ### 3. Watching only a derived slice — combine with `ValueListenable` adapters | ||
|
|
||
| `FieldController` is a full `ValueListenable<FieldState<T, E>>`. If a part of your UI cares only about, say, `state.isValidating`, you can wrap it once and listen to the derived notifier directly. (Pattern using `ValueListenableBuilder.builder` with a small selector helper.) | ||
|
|
||
| ```dart | ||
| ValueListenableBuilder<FieldState<String, _MyError>>( | ||
| valueListenable: field, | ||
| builder: (context, state, _) { | ||
| // Only this subtree rebuilds; surrounding widgets can subscribe separately. | ||
| if (state.isValidating) { | ||
| return const LinearProgressIndicator(); | ||
| } | ||
| if (state.error != null) { | ||
| return Text('Error: ${state.error}', style: const TextStyle(color: Colors.red)); | ||
| } | ||
| return const SizedBox.shrink(); | ||
| }, | ||
| ); | ||
| ``` | ||
|
|
||
| You could of course do the same inside a `FieldBuilder`. The point is that with `ValueListenableBuilder` you stay on SDK types end to end — useful if you're already composing with other `ValueListenable`s elsewhere in the screen (animations, scroll positions, theme observers). | ||
|
|
||
| ### 4. Listening outside a widget tree | ||
|
|
||
| Both `FieldBuilder` and `ValueListenableBuilder` are widgets. If you need to react to changes from a non-widget context (a service, another controller, a `late final` initializer), skip both and use `addListener` directly: | ||
|
|
||
| ```dart | ||
| void _onChange() { | ||
| print('value is now ${field.value.value}, status ${field.value.status}'); | ||
| } | ||
|
|
||
| field.addListener(_onChange); | ||
| // later: | ||
| field.removeListener(_onChange); | ||
| ``` | ||
|
|
||
| Same primitive `FieldBuilder` and `ValueListenableBuilder` use internally — just without the widget plumbing. | ||
|
|
||
| --- | ||
|
|
||
| ## When to pick which | ||
|
|
||
| | Situation | Use | | ||
| | --- | --- | | ||
| | Most form widgets | `FieldBuilder` | | ||
| | Migrating from 0.1.x code that already used `FieldBuilder` | `FieldBuilder` (just rename the field type) | | ||
| | Need the `child:` optimization for a static subtree | `ValueListenableBuilder` | | ||
| | Already composing with other `ValueListenable`s on the screen | `ValueListenableBuilder` | | ||
| | Reacting outside a widget tree | `field.addListener(...)` directly | | ||
|
|
||
| There's no behavioral difference between `FieldBuilder` and `ValueListenableBuilder` — the first is the second wrapped in 30 lines. Pick whichever reads better at the call site. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.