Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active October 1, 2025 06:12
Show Gist options
  • Save PlugFox/4d186e0d5a191da2a3b2b53d7dc838e1 to your computer and use it in GitHub Desktop.
Save PlugFox/4d186e0d5a191da2a3b2b53d7dc838e1 to your computer and use it in GitHub Desktop.
import 'package:doctorina/src/profile/model/profile_data_codecs.dart';
import 'package:flutter/foundation.dart';
typedef ProfileId = String;
@immutable
class Profile {
const Profile({
required this.id,
required this.name,
required this.isPrimary,
required this.createdAt,
required this.updatedAt,
});
final ProfileId id;
final String name;
final bool isPrimary;
final DateTime createdAt;
final DateTime updatedAt;
@override
String toString() => 'Profile{id: $id, name: $name}';
}
@immutable
class ProfileData {
const ProfileData({
required this.locale,
required this.profileId,
required this.updatedAt,
required this.schemaVersion,
required this.sections,
});
factory ProfileData.fromJson(Map<String, Object?> json) => const ProfileDataJsonDecoder().convert(json);
final ProfileId profileId;
final String locale;
final DateTime updatedAt;
final Object? schemaVersion;
final List<ProfileSection> sections;
@override
String toString() => 'ProfileData{profileId: $profileId, locale: $locale, sections: ${sections.length}}';
}
@immutable
class ProfileSection {
const ProfileSection({
required this.id,
required this.title,
required this.description,
required this.collapsible,
//required this.visibleIf,
required this.fields,
});
factory ProfileSection.fromJson(Map<String, Object?> json) => const ProfileSectionJsonDecoder().convert(json);
final String id;
final String title;
final String? description;
final bool collapsible;
//final Object? visibleIf;
final List<ProfileField> fields;
@override
String toString() => 'ProfileSection{id: $id, title: $title}';
}
/// The type of the profile field.
enum ProfileFieldType {
/// A single line text field.
/// e.g.
/// ```json
/// "type": "text",
/// "value": "Some text"
/// ```
text(),
/// Selection from provided options.
/// e.g.
/// ```json
/// "type": "select",
/// "options": {
/// "male": "Male",
/// "female": "Female",
/// "other": "Other"
/// },
/// value: "male"
/// ```
select(),
/// Multiple selection.
/// e.g.
/// ```json
/// "type": "multiselect",
/// "value": ["tag", "option", "id"],
/// ```
enumeration(),
/// A date field.
/// e.g.
/// ```json
/// "type": "date",
/// "value": "2023-01-01"
/// ```
date();
const ProfileFieldType();
}
@immutable
sealed class ProfileField {
const ProfileField({
required this.id,
required this.label,
required this.placeholder,
required this.required,
//required this.widget,
//required this.visibleIf,
//required this.options,
//required this.value,
});
factory ProfileField.fromJson(Map<String, Object?> json) => const ProfileFieldJsonDecoder().convert(json);
/// The type of the profile field.
abstract final ProfileFieldType type;
/// The unique identifier of the profile field.
final String id;
/// The label of the profile field.
final String label;
/// The placeholder of the profile field.
final String placeholder;
/// Whether the profile field is required.
final bool required;
//final Object? widget;
//final Object? visibleIf;
//final Map<String, String>? options;
//final Object? value;
T map<T>({
required T Function(ProfileField$Text value) text,
required T Function(ProfileField$Select value) select,
required T Function(ProfileField$Enumeration value) enumeration,
required T Function(ProfileField$Date value) date,
});
T maybeMap<T>({
T Function(ProfileField$Text value)? text,
T Function(ProfileField$Select value)? select,
T Function(ProfileField$Enumeration value)? enumeration,
T Function(ProfileField$Date value)? date,
required T Function() orElse,
}) => map(
text: text ?? (_) => orElse(),
select: select ?? (_) => orElse(),
enumeration: enumeration ?? (_) => orElse(),
date: date ?? (_) => orElse(),
);
}
final class ProfileField$Text extends ProfileField {
const ProfileField$Text({
required super.id,
required super.label,
required super.placeholder,
required super.required,
required this.value,
});
@override
ProfileFieldType get type => ProfileFieldType.text;
final String? value;
@override
T map<T>({
required T Function(ProfileField$Text value) text,
required T Function(ProfileField$Select value) select,
required T Function(ProfileField$Enumeration value) enumeration,
required T Function(ProfileField$Date value) date,
}) => text(this);
@override
String toString() => 'ProfileField\$Text{id: $id, label: $label}';
}
final class ProfileField$Select extends ProfileField {
const ProfileField$Select({
required super.id,
required super.label,
required super.placeholder,
required super.required,
required this.options,
required this.value,
});
@override
ProfileFieldType get type => ProfileFieldType.select;
final Map<String, String>? options;
final String? value;
@override
T map<T>({
required T Function(ProfileField$Text value) text,
required T Function(ProfileField$Select value) select,
required T Function(ProfileField$Enumeration value) enumeration,
required T Function(ProfileField$Date value) date,
}) => select(this);
@override
String toString() => 'ProfileField\$Select{id: $id, label: $label}';
}
final class ProfileField$Enumeration extends ProfileField {
const ProfileField$Enumeration({
required super.id,
required super.label,
required super.placeholder,
required super.required,
required this.value,
});
@override
ProfileFieldType get type => ProfileFieldType.enumeration;
final List<String>? value;
@override
T map<T>({
required T Function(ProfileField$Text value) text,
required T Function(ProfileField$Select value) select,
required T Function(ProfileField$Enumeration value) enumeration,
required T Function(ProfileField$Date value) date,
}) => enumeration(this);
@override
String toString() => 'ProfileField\$Enumeration{id: $id, label: $label}';
}
final class ProfileField$Date extends ProfileField {
const ProfileField$Date({
required super.id,
required super.label,
required super.placeholder,
required super.required,
required this.value,
});
@override
ProfileFieldType get type => ProfileFieldType.date;
final DateTime? value;
@override
T map<T>({
required T Function(ProfileField$Text value) text,
required T Function(ProfileField$Select value) select,
required T Function(ProfileField$Enumeration value) enumeration,
required T Function(ProfileField$Date value) date,
}) => date(this);
@override
String toString() => 'ProfileField\$Date{id: $id, label: $label}';
}
{
"data": {
"profile_id": "P_E35bKgX82ioMWRQ9x8V",
"form_schema_link": "/v1/forms/profile/version/1",
"form_data": {
"name": "profile",
"schema_version": "1",
"profile_id": "P_E35bKgX82ioMWRQ9x8V",
"locale": "en",
"updated_at": "2025-09-26T12:47:57.501976Z",
"layout": {
"order": [
"basic",
"body_diet",
"health_profile"
]
},
"sections": [
{
"id": "basic",
"title": "General Information",
"description": null,
"collapsible": true,
"visible_if": null,
"fields": [
{
"id": "first_name",
"type": "text",
"label": "First name",
"placeholder": "John",
"required": false,
"widget": null,
"visible_if": null,
"readonly": false,
"value": null
},
{
"id": "last_name",
"type": "text",
"label": "Last name",
"placeholder": "Doe",
"required": false,
"widget": null,
"visible_if": null,
"readonly": false,
"value": null
},
{
"id": "sex",
"type": "select",
"label": "Sex",
"placeholder": "Please select",
"required": false,
"widget": null,
"visible_if": null,
"options": {
"male": "Male",
"female": "Female",
"other": "Other"
},
"value": null
},
{
"id": "date_of_birth",
"type": "date",
"label": "Date of Birth",
"placeholder": "YYYY-MM-DD",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "phonenumber",
"type": "text",
"label": "Phone number",
"placeholder": "+xxx xxx xxx xxx",
"required": false,
"widget": null,
"visible_if": null,
"readonly": false,
"value": null
},
{
"id": "email",
"type": "text",
"label": "Email",
"placeholder": "[email protected]",
"required": false,
"widget": null,
"visible_if": null,
"readonly": true,
"value": null
},
{
"id": "location",
"type": "text",
"label": "Location",
"placeholder": "e.g. City, Country",
"required": false,
"widget": null,
"visible_if": null,
"readonly": true,
"value": null
}
]
},
{
"id": "body_diet",
"title": "Body & Diet",
"description": null,
"collapsible": true,
"visible_if": null,
"fields": [
{
"id": "height",
"type": "text",
"label": "Height",
"placeholder": "e.g. 180 cm",
"required": false,
"widget": null,
"visible_if": null,
"readonly": true,
"value": null
},
{
"id": "weight",
"type": "text",
"label": "Weight",
"placeholder": "e.g. 75 kg",
"required": false,
"widget": null,
"visible_if": null,
"readonly": true,
"value": null
},
{
"id": "menstrual_cycle",
"type": "text",
"label": "Menstrual Cycle",
"placeholder": "e.g. Regular, Irregular",
"required": false,
"widget": null,
"visible_if": null,
"readonly": true,
"value": null
},
{
"id": "dietary_restrictions",
"type": "select",
"label": "Dietary Restrictions",
"placeholder": "Please select",
"required": false,
"widget": null,
"visible_if": null,
"options": {
"none": "None",
"vegetarian": "Vegetarian",
"vegan": "Vegan",
"gluten_free": "Gluten Free"
},
"value": null
},
{
"id": "bmi",
"type": "text",
"label": "Body Mass Index (BMI)",
"placeholder": "e.g. 24.5",
"required": false,
"widget": null,
"visible_if": null,
"readonly": true,
"value": null
}
]
},
{
"id": "health_profile",
"title": "Health Profile",
"description": null,
"collapsible": true,
"visible_if": null,
"fields": [
{
"id": "chronic_illnesses",
"type": "enumeration",
"label": "Chronic Illnesses",
"placeholder": "e.g. Diabetes, Hypertension",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "past_illnesses",
"type": "enumeration",
"label": "Past Illnesses",
"placeholder": "e.g. Chickenpox",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "surgical_history",
"type": "enumeration",
"label": "Surgical History",
"placeholder": "e.g. Appendectomy",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "occasional_medications",
"type": "enumeration",
"label": "Occasionally used Medications",
"placeholder": "e.g. Aspirin, Metformin",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "regular_medications",
"type": "enumeration",
"label": "Regular Medications",
"placeholder": "e.g. Ibuprofen",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "allergies",
"type": "enumeration",
"label": "Allergies",
"placeholder": "e.g. Peanuts, Shellfish",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "special_conditions",
"type": "enumeration",
"label": "Special Conditions",
"placeholder": "e.g. Pregnancy, Disability",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "family_history",
"type": "enumeration",
"label": "Family History",
"placeholder": "e.g. Heart Disease, Cancer",
"required": false,
"widget": null,
"visible_if": null,
"value": null
},
{
"id": "social_lifestyle_factors",
"type": "enumeration",
"label": "Social & Lifestyle Factors",
"placeholder": "e.g. Smoking, Alcohol consumption",
"required": false,
"widget": null,
"visible_if": null,
"value": null
}
]
}
]
},
"updated_at": "2025-09-26T12:47:57.501976Z"
},
"status": "ok"
}
import 'dart:collection';
import 'dart:convert';
import 'package:doctorina/src/profile/model/profile_data.dart';
import 'package:l/l.dart';
/// JSON decoder for ProfileData
/// Converts a JSON map to a [ProfileData] object.
/// Handles nested structures and type conversions.
/// See also [ProfileSectionJsonDecoder] and [ProfileFieldJsonDecoder].
class ProfileDataJsonDecoder extends Converter<Map<String, Object?>, ProfileData> {
const ProfileDataJsonDecoder();
@override
ProfileData convert(Map<String, Object?> input) {
const sectionDecoder = ProfileSectionJsonDecoder();
List<ProfileSection> sections;
{
final $sections = switch (input['sections']) {
List<Object?> list => list,
_ => const <Object?>[],
};
var valid = true;
final $sectionList = List<ProfileSection?>.filled($sections.length, null, growable: false);
for (var i = 0; i < $sections.length; i++) {
try {
if ($sections[i] case Map<String, Object?> sectionMap) {
$sectionList[i] = sectionDecoder.convert(sectionMap);
} else {
throw FormatException('Invalid section format: ${$sections[i]}');
}
} on Object catch (e, s) {
l.w('Failed to decode section at index $i: $e', s);
valid = false;
}
}
sections =
valid
? List<ProfileSection>.unmodifiable($sectionList)
: List<ProfileSection>.unmodifiable($sectionList.whereType<ProfileSection>());
final layoutOrder = switch (input['layout']) {
<String, Object?>{'order': List<Object?> order} => order.whereType<String>().toList(growable: false),
_ => const <String>[],
};
if (sections.isNotEmpty && layoutOrder.length == sections.length) {
final layoutOrderSet = layoutOrder.toSet();
final sectionsIds = sections.map((e) => e.id).toSet();
if (layoutOrderSet.length == sectionsIds.length && layoutOrderSet.containsAll(sectionsIds)) {
sections.sort((a, b) => layoutOrder.indexOf(a.id).compareTo(layoutOrder.indexOf(b.id)));
}
}
}
return ProfileData(
schemaVersion: input['schema_version'],
profileId: switch (input['profile_id'] ?? input['id']) {
String str => str,
_ => throw const FormatException('Invalid profile ID format'),
},
locale: switch (input['locale']) {
String str => str,
_ => 'en',
},
updatedAt:
switch (input['updated_at']) {
String str => DateTime.tryParse(str),
DateTime dateTime => dateTime,
int timestamp => DateTime.fromMillisecondsSinceEpoch(timestamp),
_ => null,
} ??
DateTime.now(),
sections: sections,
);
}
}
/// JSON decoder for ProfileSection
/// Converts a JSON map to a [ProfileSection] object.
/// Handles nested structures and type conversions.
/// See also [ProfileDataJsonDecoder] and [ProfileFieldJsonDecoder].
class ProfileSectionJsonDecoder extends Converter<Map<String, Object?>, ProfileSection> {
const ProfileSectionJsonDecoder();
@override
ProfileSection convert(Map<String, Object?> input) {
const fieldDecoder = ProfileFieldJsonDecoder();
if (input case {
'id': String $id,
'title': String $title,
'description': String? $description,
'collapsible': bool? $collapsible,
'fields': List<Object?> $fields,
}) {
var valid = true;
var fields = List<ProfileField?>.filled($fields.length, null, growable: false);
for (var i = 0; i < $fields.length; i++) {
try {
if ($fields[i] case Map<String, Object?> fieldMap) {
fields[i] = fieldDecoder.convert(fieldMap);
} else {
throw FormatException('Invalid field format: ${$fields[i]}');
}
} on Object catch (e, s) {
l.w('Failed to decode field at index $i in section ${$id}: $e', s);
valid = false;
}
}
return ProfileSection(
id: $id,
title: $title,
description: $description,
collapsible: $collapsible ?? false,
//visibleIf: input['visible_if'],
fields:
valid
? List<ProfileField>.unmodifiable(fields)
: List<ProfileField>.unmodifiable(fields.whereType<ProfileField>()),
);
} else {
throw FormatException('Invalid section format: $input');
}
}
}
/// JSON decoder for ProfileField
/// Converts a JSON map to a [ProfileField] object.
/// Handles different field types and their specific properties.
/// See also [ProfileDataJsonDecoder] and [ProfileSectionJsonDecoder].
class ProfileFieldJsonDecoder extends Converter<Map<String, Object?>, ProfileField> {
const ProfileFieldJsonDecoder();
@override
ProfileField convert(Map<String, Object?> input) {
if (input case {
'id': String $id,
'label': String $label,
'placeholder': String? $placeholder,
'required': bool? $required,
'type': String $type,
'value': Object? $value,
}) {
switch ($type) {
case 'text':
case 'string':
case 'input':
case 'textfield':
case 'multiline':
case 'multi_line':
case 'longtext':
case 'long_text':
case 'paragraph':
case 'note':
return ProfileField$Text(
id: $id,
label: $label,
placeholder: $placeholder ?? '',
required: $required ?? false,
value: switch ($value) {
String str => str,
_ => null,
},
);
case 'select':
case 'dropdown':
case 'option':
case 'options':
case 'choice':
case 'choices':
return ProfileField$Select(
id: input['id'] as String,
label: input['label'] as String,
placeholder: input['placeholder'] as String? ?? '',
required: input['required'] as bool? ?? false,
options: switch (input['options']) {
Map<String, String> map => UnmodifiableMapView<String, String>(map),
Map<String, Object?> map => UnmodifiableMapView<String, String>(<String, String>{
for (final MapEntry<String, Object?>(:key, :value) in map.entries)
if (value case String valueString) key: valueString,
}),
Map<Object?, Object?> map => UnmodifiableMapView<String, String>(<String, String>{
for (final MapEntry<Object?, Object?>(:key, :value) in map.entries)
if (key case String keyString)
if (value case String valueString) keyString: valueString,
}),
_ => null,
},
value: switch ($value) {
String str => str,
_ => null,
},
);
case 'enumeration':
case 'multiselect':
case 'multi_select':
case 'multiple':
case 'list':
case 'tags':
case 'enums':
return ProfileField$Enumeration(
id: $id,
label: $label,
placeholder: $placeholder ?? '',
required: $required ?? false,
value: switch ($value) {
List<String> list => UnmodifiableListView<String>(list),
Iterable<Object?> list => List<String>.unmodifiable(list.whereType<String>()),
_ => null,
},
);
case 'date':
case 'datetime':
case 'time':
case 'timestamp':
case 'day':
case 'birthday':
case 'birthdate':
case 'calendar':
return ProfileField$Date(
id: $id,
label: $label,
placeholder: $placeholder ?? '',
required: $required ?? false,
value: switch ($value) {
String str => DateTime.tryParse(str),
DateTime dateTime => dateTime,
int timestamp => DateTime.fromMillisecondsSinceEpoch(timestamp),
_ => null,
},
);
default:
throw UnimplementedError('Unsupported field type: ${$type}');
}
} else {
throw FormatException('Invalid field format: $input');
}
}
}
import 'package:doctorina/src/profile/controller/profile_form_controller.dart';
import 'package:flutter/widgets.dart';
/// {@template inherited_profile_form}
/// InheritedProfileForm widget.
/// {@endtemplate}
class InheritedProfileForm extends InheritedNotifier<ProfileFormController> {
/// {@macro inherited_profile_form}
const InheritedProfileForm({
required this.form,
required super.child,
super.key, // ignore: unused_element
}) : super(notifier: form);
/// The [ProfileFormController] to manage the state of the profile form.
final ProfileFormController form;
/// The state from the closest instance of this class
/// that encloses the given context, if any.
/// e.g. `ProfileFieldBuilder.maybeOf(context)`.
static ProfileFormController? maybeOf(BuildContext context, {bool listen = true}) =>
listen
? context.dependOnInheritedWidgetOfExactType<InheritedProfileForm>()?.form
: context.getInheritedWidgetOfExactType<InheritedProfileForm>()?.form;
static Never _notFoundInheritedWidgetOfExactType() =>
throw ArgumentError(
'Out of scope, not found inherited widget '
'a InheritedProfileForm of the exact type',
'out_of_scope',
);
/// The state from the closest instance of this class
/// that encloses the given context.
/// e.g. `ProfileFieldBuilder.of(context)`
static ProfileFormController of(BuildContext context, {bool listen = true}) =>
maybeOf(context, listen: listen) ?? _notFoundInheritedWidgetOfExactType();
}
/// {@template profile_field_builder}
/// ProfileFieldBuilder widget responsible for locating or creating a [ProfileFieldController].
/// [section] and [field] are identifiers for the profile section and field respectively.
/// The [create] function is called only if the controller does not already exist.
/// The optional [changed] callback can be used to determine if the controller's data has changed.
/// The [builder] function is used to build the widget tree based on the current state of the profile field.
/// The optional [form] parameter can be used to provide a specific [ProfileFormController].
/// {@endtemplate}
class ProfileFieldBuilder<Value extends Object?, Controller extends ProfileFieldController<Value>>
extends StatefulWidget {
/// {@macro profile_field_builder}
const ProfileFieldBuilder({
required this.section,
required this.field,
required this.create,
required this.builder,
this.changed,
this.form,
super.key,
});
/// The section identifier of the profile field.
final String section;
/// The field identifier of the profile field.
final String field;
/// A function that creates a new instance of the [Controller].
final Controller Function(int index) create;
/// A function that determines if the profile field has changed.
final bool Function(Value prev, Value next)? changed;
/// The [ProfileFormController] to manage the state of the profile form.
final ProfileFormController? form;
/// A builder function that builds the widget tree
/// based on the current state of the profile field.
final Widget Function(BuildContext context, Controller controller) builder;
@override
State<ProfileFieldBuilder<Value, Controller>> createState() => _ProfileFieldBuilderState<Value, Controller>();
}
/// State for widget ProfileFieldBuilder.
class _ProfileFieldBuilderState<Value extends Object?, Controller extends ProfileFieldController<Value>>
extends State<ProfileFieldBuilder<Value, Controller>> {
int _index = 0; // ignore: unused_field
late Controller _controller;
void _onChanged() {}
Controller _create(int index) {
final controller = widget.create(index);
_index = index;
assert(controller.$section == widget.section);
assert(controller.$field == widget.field);
return controller;
}
/* #region Lifecycle */
@override
void initState() {
super.initState();
_controller = switch (widget.form) {
null => InheritedProfileForm.of(context, listen: false),
final form => form,
}.locate<Value, Controller>(
section: widget.section,
field: widget.field,
create: _create,
changed: widget.changed,
)..addListener(_onChanged);
}
@override
void didUpdateWidget(covariant ProfileFieldBuilder<Value, Controller> oldWidget) {
super.didUpdateWidget(oldWidget);
if (!identical(oldWidget.form, widget.form) ||
oldWidget.section != widget.section ||
oldWidget.field != widget.field) {
_controller.removeListener(_onChanged);
_controller = switch (widget.form) {
null => InheritedProfileForm.of(context, listen: false),
final form => form,
}.locate<Value, Controller>(
section: widget.section,
field: widget.field,
create: _create,
changed: widget.changed,
)..addListener(_onChanged);
}
}
@override
void dispose() {
_controller.removeListener(_onChanged);
super.dispose();
}
/* #endregion */
@override
Widget build(BuildContext context) => widget.builder(context, _controller);
}
import 'dart:collection' show UnmodifiableMapView, UnmodifiableSetView;
import 'dart:math' as math show max;
import 'dart:typed_data' show Uint32List;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'
show ChangeNotifier, TextEditingController, ValueNotifier, protected, visibleForTesting;
/// Controller for managing profile fields and their state.
/// This controller tracks changes to profile fields and notifies listeners
/// when any field is modified. It supports locating or creating field controllers
/// as needed, and maintains an internal state to efficiently track modifications.
class ProfileFormController with ChangeNotifier {
/// Initializes a new instance of [ProfileFormController].
ProfileFormController({int capacity = 32})
: _indexes = <String, Map<String, int>>{},
_controllers = List.filled(capacity, null, growable: false),
_sourceData = List.filled(capacity, null, growable: false),
_flags = Uint32List((capacity / 32).ceil());
/// Indicates whether the controller has been disposed.
bool _disposed = false;
/// Indicates whether any of the profile fields have been modified.
/// If true, at least one field has changed since the last reset or initialization.
/// And false if all fields are unchanged.
bool _dirty = false;
/// Returns true if any controllers have changes.
bool get hasChanges => _dirty;
/// Returns the number of controllers currently managed.
int get controllerCount => _indexCounter + 1;
/// Internal counter for tracking the number of controllers.
int _indexCounter = -1;
/// Generates the next index for storing a new controller.
int _nextIndex() {
final idx = ++_indexCounter;
if (_controllers.length <= idx) _increaseCapacity();
return idx;
}
/// Doubles the capacity of internal storage lists.
void _increaseCapacity() {
final newSize = math.max(_controllers.length << 1, 64);
_controllers = List.filled(newSize, null, growable: false)..setAll(0, _controllers);
_sourceData = List.filled(newSize, null, growable: false)..setAll(0, _sourceData);
_flags = Uint32List((newSize / 32).ceil())..setAll(0, _flags);
}
/// Maps section and field identifiers to their respective controller indices.
Map<String, Map<String, int>> _indexes;
/// List of current field controllers.
List<ProfileFieldController<Object?>?> _controllers;
/// Initial data of field controllers.
/// This is a snapshot of the data when the controller was created.
/// It is used to restore the initial state if needed.
/// Also used to detect changes by comparing current data against this snapshot.
/// After commitChanges() is called, this is updated to match the current data.
List<Object?> _sourceData;
/// Bit flags representing the dirty state of each controller.
/// Each 32-bit integer can store flags for 32 controllers.
Uint32List _flags;
/// Sets the dirty flag for the controller at the given index.
/// Notifies listeners if the overall dirty state changes from false to true.
void _setDirtyFlag(int index) {
final wordIndex = index ~/ 32;
assert(wordIndex < _flags.length, 'Index $index out of bounds');
if (wordIndex >= _flags.length) return; // Out of bounds, ignore
final current = _flags[wordIndex];
final bitIndex = index % 32;
final bitMask = 1 << bitIndex; // Bit mask for this index
if ((current & bitMask) != 0) return; // Already dirty, no change
_flags[wordIndex] |= bitMask; // Mark as dirty
_dirty = true;
notifyListeners(); // Notify listeners of the change
}
/// Clears the dirty flag for the controller at the given index.
/// Notifies listeners if the overall dirty state changes from true to false.
void _clearDirtyFlag(int index) {
final wordIndex = index ~/ 32;
assert(wordIndex < _flags.length, 'Index $index out of bounds');
if (wordIndex >= _flags.length) return; // Out of bounds, ignore
final bitIndex = index % 32;
_flags[wordIndex] &= ~(1 << bitIndex);
if (!_dirty) return; // No change in dirty state
if (_flags.every((f) => f == 0)) _dirty = false; // All flags cleared
notifyListeners();
}
/// Returns true if the controller at the given index is marked as dirty.
@protected
@visibleForTesting
bool isDirtyFlag(int index) {
final wordIndex = index ~/ 32;
assert(wordIndex < _flags.length, 'Index $index out of bounds');
if (wordIndex >= _flags.length) return false;
final bitIndex = index % 32;
return (_flags[wordIndex] & (1 << bitIndex)) != 0;
}
/// Returns true if the controller at the given index is marked as dirty.
@protected
@visibleForTesting
bool isDirtyField(String section, String field) {
final idx = _indexes[section]?[field]; // Get the index for the section/field
if (idx == null) return false; // No controller exists for this section/field
return isDirtyFlag(idx);
}
/// Returns a list of indices of controllers that have been modified.
@protected
@visibleForTesting
List<int> get changedIndexes {
if (!_dirty) return const []; // No changes to report
final result = <int>[];
for (var flagIdx = 0; flagIdx < _flags.length; flagIdx++) {
final f = _flags[flagIdx];
if (f == 0) continue; // Skip if no bits are set in this flag
for (var bitIdx = 0; bitIdx < 32; bitIdx++) {
if ((f & (1 << bitIdx)) == 0) continue; // Skip if this bit is not set
final index = flagIdx * 32 + bitIdx;
if (index <= _indexCounter) result.add(index);
}
}
return result;
}
/// Returns a map of changes made to the profile fields.
/// The outer map's keys are section identifiers, and the inner maps' keys are field identifiers.
/// The values in the inner maps are the current data of the modified fields.
Map<String, Map<String, Object?>> get changes {
if (!_dirty) return const {}; // No changes to report
final result = <String, Map<String, Object?>>{};
for (var flagIdx = 0; flagIdx < _flags.length; flagIdx++) {
final f = _flags[flagIdx];
if (f == 0) continue; // Skip if no bits are set in this flag
for (var bitIdx = 0; bitIdx < 32; bitIdx++) {
if ((f & (1 << bitIdx)) == 0) continue; // Skip if this bit is not set
final index = flagIdx * 32 + bitIdx;
final controller = _controllers[index];
if (controller == null) continue; // Skip if no controller exists at this index
final ProfileFieldController<Object?>($section: section, $field: field, $data: data) = controller;
result.putIfAbsent(section, () => <String, Object?>{})[field] = data;
}
}
return UnmodifiableMapView<String, Map<String, Object?>>(result);
}
/// Commits all changes by updating the previous state to match the current state.
/// Clears the dirty flags for all modified controllers.
void commitChanges() {
if (!_dirty) return; // No changes to commit
for (final index in changedIndexes) _sourceData[index] = _controllers[index]?.$data;
_flags.fillRange(0, _flags.length, 0);
_dirty = false;
notifyListeners();
}
/// Retrieves the initial data of the field at the given [index].
/// Returns null if the index is out of bounds or if no data exists at that index.
Value? sourceIndexData<Value>(int index) {
if (index < 0 || index >= _sourceData.length) return null; // Index out of bounds
return switch (_sourceData[index]) {
Value value => value,
_ => null,
};
}
/// Retrieves the initial data of the field at the given [section] and [field].
/// Returns null if no data exists for the specified section and field.
Value? sourceFieldData<Value>(String section, String field) {
final idx = _indexes[section]?[field]; // Get the index for the section/field
if (idx == null) return null; // No controller exists for this section/field
if (idx < 0 || idx >= _sourceData.length) return null; // Index out of bounds
return switch (_sourceData[idx]) {
Value value => value,
_ => null,
};
}
/// Locate or create a [ProfileFieldController] for the given [section] and [field].
/// [section] and [field] are identifiers for the profile section and field respectively.
/// The [create] function is called only if the controller does not already exist.
/// The optional [changed] callback can be used to determine if the controller's data has changed.
Controller locate<Value extends Object?, Controller extends ProfileFieldController<Value>>({
required String section,
required String field,
required Controller Function(int index) create,
bool Function(Value prev, Value next)? changed,
}) {
final int idx;
{
var i = _indexes[section]?[field];
if (i == null) {
idx = _nextIndex();
_indexes.putIfAbsent(section, () => <String, int>{})[field] = idx;
} else {
idx = i;
}
}
final $changed = changed ?? (prev, next) => prev != next;
switch (_controllers[idx]) {
case Controller controller:
return controller;
case null:
if (_disposed) throw StateError('Cannot locate controller after dispose() has been called');
final controller = _controllers[idx] = create(idx);
final initialData = _sourceData[idx] = controller.$data;
controller.addListener(() {
if (_disposed) return;
final next = controller.$data;
final prev = switch (_sourceData[idx]) {
Value value => value,
_ => initialData,
};
if (identical(prev, next) || !$changed(prev, next)) {
// No actual change in data detected
_clearDirtyFlag(idx);
} else {
// Data has changed
_setDirtyFlag(idx);
}
});
return controller;
default:
throw StateError(
'Incompatible controller type stored at index $idx: '
'expected ${Controller.runtimeType}, found ${_controllers[idx]?.runtimeType}',
);
}
}
/// Clears all controllers and resets the state of the [ProfileFormController].
void clear() {
if (_disposed) throw StateError('Cannot clear controllers after dispose() has been called');
for (var i = 0; i <= _indexCounter; i++) if (_controllers[i] case ChangeNotifier controller) controller.dispose();
_indexes = <String, Map<String, int>>{};
final capacity = math.max(_controllers.length, 32);
_controllers = List.filled(capacity, null, growable: false);
_sourceData = List.filled(capacity, null, growable: false);
_flags = Uint32List((capacity / 32).ceil());
_indexCounter = -1;
_dirty = false;
notifyListeners();
}
/// Disposes the controller and all its managed field controllers.
@override
void dispose() {
_disposed = true;
for (var i = 0; i <= _indexCounter; i++) if (_controllers[i] case ChangeNotifier controller) controller.dispose();
super.dispose();
}
}
/// Interface for profile field controllers.
/// Provides common properties and methods for accessing field data.
abstract interface class ProfileFieldController<Value> implements Listenable {
/// The identifier of the profile section.
abstract final String $section;
/// The identifier of the profile field.
abstract final String $field;
/// Retrieves the current data of the field.
Value get $data;
}
/// Controller for text profile fields.
class ProfileFieldController$Text extends TextEditingController implements ProfileFieldController<String> {
ProfileFieldController$Text({required String section, required String field, super.text})
: $section = section,
$field = field;
@override
final String $section;
@override
final String $field;
@override
String get $data => super.text;
}
/// Controller for select profile fields.
class ProfileFieldController$Select extends ValueNotifier<String?> implements ProfileFieldController<String?> {
ProfileFieldController$Select({required String section, required String field, String? value})
: $section = section,
$field = field,
super(value);
@override
final String $section;
@override
final String $field;
@override
String? get $data => value;
}
/// Controller for enumeration profile fields.
class ProfileFieldController$Enumeration with ChangeNotifier implements ProfileFieldController<Set<String>> {
ProfileFieldController$Enumeration({required String section, required String field, Set<String>? values})
: $section = section,
$field = field,
_values = UnmodifiableSetView<String>(values ?? const <String>{});
@override
final String $section;
@override
final String $field;
@override
Set<String> get $data => _values;
/// The current set of selected values in the enumeration.
Set<String> _values;
/// Adds a value to the enumeration.
void add(String value) {
if (_values.contains(value)) return;
final copy = Set<String>.from(_values);
copy.add(value);
_values = UnmodifiableSetView<String>(copy);
notifyListeners();
}
/// Removes a value from the enumeration.
void remove(String value) {
if (!_values.contains(value)) return;
final copy = Set<String>.from(_values);
copy.remove(value);
_values = UnmodifiableSetView<String>(copy);
notifyListeners();
}
/// Sets the entire set of selected values in the enumeration.
void setAll(Iterable<String> values) {
final newValues = UnmodifiableSetView<String>(Set<String>.from(values));
if (setEquals(_values, newValues)) return;
_values = newValues;
notifyListeners();
}
}
/// Controller for date profile fields.
class ProfileFieldController$Date extends ValueNotifier<DateTime?> implements ProfileFieldController<DateTime?> {
ProfileFieldController$Date({required String section, required String field, DateTime? value})
: $section = section,
$field = field,
super(value);
@override
final String $section;
@override
final String $field;
@override
DateTime? get $data => value;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment