Skip to main content

Properties, state and rendering

Properties

All external configuration is passed through the constructor. Flutter widgets are immutable — use final for every field:

foo.dart
// ✅ Good — immutable, final fields
const ZetaFoo({
super.key,
super.rounded,
required this.label,
this.disabled = false,
});

final String label;
final bool disabled;

// ❌ Bad — mutable field, breaks Flutter's build model
String label = '';

Reactive state

Use ZetaStatefulWidget + State only when the component owns UI state that is not derivable from its parameters (e.g. animation progress, focus node, scroll controller). Prefer lifting state to the parent:

zeta_accordion.dart
class ZetaAccordion extends ZetaStatefulWidget {
const ZetaAccordion({super.key, super.rounded, required this.title, required this.child});

final String title;
final Widget child;


State<ZetaAccordion> createState() => _ZetaAccordionState();
}

class _ZetaAccordionState extends State<ZetaAccordion> {
bool _expanded = false;


Widget build(BuildContext context) { ... }
}

Internal references

Store controllers and focus nodes as State fields when they must be created once and shared across builds:

foo.dart
class _ZetaFooState extends State<ZetaFoo> {
late final FocusNode _focusNode = FocusNode();
late final TextEditingController _controller = TextEditingController();


void dispose() {
_focusNode.dispose();
_controller.dispose();
super.dispose();
}
}

Always dispose controllers and focus nodes in dispose().

Communicating out

Expose callbacks using Flutter's standard callback typedefs. Prefer typed callbacks over Function:

foo.dart
// ✅ Good
final VoidCallback? onTap;
final ValueChanged<bool>? onChanged;
final ValueChanged<String>? onTextChanged;

// ❌ Bad
final Function? onTap;
final Function(dynamic)? onChanged;

Use onTap for taps, onChanged for value-producing interactions, onLongPress for long-press — match Flutter SDK conventions.

Keep render focused

Extract complex sub-trees into private helper methods. Keep build() readable as a structural overview:

foo.dart

Widget build(BuildContext context) {
final colors = Zeta.of(context).colors;
return Column(
children: [
_buildLabel(context, colors),
_buildInput(context, colors),
_buildHint(colors),
].nonNulls.toList(),
);
}

Widget? _buildHint(ZetaColors colors) {
if (hint == null) return null;
return Text(hint!, style: ZetaTextStyles.bodySmall);
}

Conditional rendering

Use if spreads in lists. Avoid SizedBox() as a no-op placeholder — it inserts unnecessary nodes:

foo.dart
// ✅ Good
children: [
if (leadingIcon != null) ZetaIcon(leadingIcon!),
Text(label),
if (trailingIcon != null) ZetaIcon(trailingIcon!),
],

// ❌ Bad
children: [
leadingIcon != null ? ZetaIcon(leadingIcon!) : const SizedBox(),
Text(label),
],

Content and children

Name child parameters after their semantic role, not their position:

foo.dart
/// The leading widget displayed before [label].
final Widget? leading;

/// The trailing widget displayed after [label].
final Widget? trailing;

Do not expose generic child / children unless the component design explicitly shows it as a slot. Use child for a single child, children for multiple children.