Skip to content

Refactor/cubit to notifier migration#42

Open
aydinguven-leancode wants to merge 4 commits into
refactor/forms-reworkfrom
refactor/cubit-to-notifier-migration
Open

Refactor/cubit to notifier migration#42
aydinguven-leancode wants to merge 4 commits into
refactor/forms-reworkfrom
refactor/cubit-to-notifier-migration

Conversation

@aydinguven-leancode

Copy link
Copy Markdown

LMG-394

  • Migration from Cubit to a Value Notifier based state management
  • Dropped dependencies: flutter_bloc, rx_dart, flutter_hooks, equatable`
  • Name changes for relevant classes
  • Relevant changes on tests and example project

LMG-404

  • Partially modified Readme.md and Changelog.md files about the above migration
  • Introduced Migration.md file
  • Added a few more examples as well as short descriptions to each example page

aydinguven-leancode and others added 4 commits June 19, 2026 09:57
…nagement

Migrated from Cubit (Bloc) to value notifier.
Removed dependencies: flutter_bloc, rx_dart, equatable,
Changes are reflected on examples
Changes are reflected on tests
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
});

/// The focus node of the field.
final focusNode = FocusNode();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use debugLabel

import 'package:leancode_forms/leancode_forms.dart';

/// A [TextFieldController] that owns a [FocusNode] for scroll-to-error flows.
class FocusableTextFieldController<E extends Object>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All form controls are usually focusable, I don't think having a Focusable... wrapper for each of the ...FieldController is a good approach

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait why is it in example and not in the lib itself?

Comment thread EXAMPLES.md

Side-by-side snippets showing the same form scenario in three flavors:

1. **0.1.x (pre-migration)** — built on `flutter_bloc` + `rxdart`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't care about older versions in EXAMPLE. That's what changelog, git history and migration document is for

Comment thread EXAMPLES.md

1. **0.1.x (pre-migration)** — built on `flutter_bloc` + `rxdart`
2. **0.2.0 with `FieldBuilder`** — the recommended default; tiny wrapper around `ValueListenableBuilder`
3. **0.2.0 with `ValueListenableBuilder`** directly — for cases where you want the SDK primitive (e.g. the `child:` optimization)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use-case for having this approach? Looking at just the API I think it would be better and simpler to keep one API for the builders

Comment thread EXAMPLES.md
FieldBuilder<String, ValidationError>(
field: context.read<SignupController>().email,
builder: (context, state) => TextFormField(
controller: context.read<SignupController>().email.textController,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already passed the field controller, why fetch it again? Please just add the field controller itself into the builder callback params.

Comment thread EXAMPLES.md

// Render:
FieldBuilder<String, ValidationError>(
field: context.read<SignupController>().email,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using context.read instead of watch/select in build() method is an antipattern

Comment thread CHANGELOG.md
Comment on lines +7 to +12
* `FieldCubit` → `FieldController`
* `TextFieldCubit` → `TextFieldController`
* `BooleanFieldCubit` → `BooleanFieldController`
* `SingleSelectFieldCubit` → `SingleSelectFieldController`
* `MultiSelectFieldCubit` → `MultiSelectFieldController`
* `FormGroupCubit` → `FormGroupController`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of the ...Controller naming. It's too close to TextEditingController. I'd suggest maybe Form...Field like FormBooleanField or FormTextField

Comment thread pubspec.yaml
flutter_test:
sdk: flutter
leancode_lint: ^15.0.0
leancode_lint: ^15.0.0 No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep final newline

/// to be able to unambiguously detect lack of errors.
///
/// If autovalidate is true, the validator will be run after each field change.
// ignore_for_file: avoid_positional_boolean_parameters

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package depends on leancode_lint 15, which has this lint disabled (in version 14) – no need to ignore it, I think

///
/// Introducing cycles in forms is not supported and not checked against (most
/// likely will cause a stack overflow somewhere).
// ignore_for_file: avoid_positional_boolean_parameters

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

/// If autovalidate is true, the validator will be run after each field change.
// ignore_for_file: avoid_positional_boolean_parameters
class FieldController<T, E extends Object>
extends ValueNotifier<FieldState<T, E>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to extend ValueNotifier directly, instead of e.g. ValueListenable and adding our own value field? ValueNotifier exposes a public setter, so that anyone can create and set arbitrary FieldState values

Comment on lines +57 to +59
/// The current state. Alias for [value] kept for readability at call sites
/// that previously read `cubit.state`.
FieldState<T, E> get state => value;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't do that tbh. The entire undertaking is extremely breaking anyway, so I don't see a value in keeping a bloc-related alias

Comment on lines +46 to +52
...controller.deliveryList.map(
(e) => ConsumerSubform(
key: ValueKey(e.hashCode),
form: e,
onRemove: controller.removeConsumer,
),
),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a for loop

Comment on lines +38 to +39
child: SingleChildScrollView(
child: Column(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SingleChildScrollView + Column -> ListView?

Comment thread example/pubspec.yaml
flutter_test:
sdk: flutter
leancode_lint: ^15.0.0
leancode_lint: ^6.0.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why such a downgrade?

/// Introducing cycles in forms is not supported and not checked against (most
/// likely will cause a stack overflow somewhere).
// ignore_for_file: avoid_positional_boolean_parameters
class FormGroupController extends ValueNotifier<FormGroupState> {

@cupofme cupofme Jun 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does entity called FormGroupController manages entities called FieldController? Naming looks a bit inconsistent to me

Comment thread EXAMPLES.md
```dart
class SimpleFormController extends FormGroupController {
SimpleFormController() {
registerFields([firstName, email]);

@cupofme cupofme Jun 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be an abstract getter fields in FormGroupController that needs to be implemented in subclasses? This way it'll be required and therefore one less thing that can be forgotten.

Something like this:

class SimpleFormController extends FormGroupController {
  SimpleFormController();
  
  @override
  List<FieldController> get fields => [
    firstName, 
    email,
  ];

...
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having registerFields allows dynamically adding fields as needed (for example, conditional fields). Although for that, an unregisterFields method would be needed 🤔

addTearDown(controller.dispose);

controller.email.setValue('john@email.com');
await Future<void>.delayed(const Duration(seconds: 2));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have a way to "await validation"

Comment on lines +374 to +377
const ListEquality<FieldController<dynamic, dynamic>>()
.equals(fields, other.fields) &&
const SetEquality<FormGroupController>()
.equals(subforms, other.subforms) &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using listEquals and setEquals from Flutter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants