generateView method

String generateView(
  1. String name, {
  2. required List<String> useCases,
  3. List<String> uiSignals = const ['isLoading', 'activeTabIndex'],
  4. Map<String, String> uiSignalTypes = const {},
  5. bool pureDart = false,
})

Generate a complete view file with ControlledWidget, FragmentBuilder, and SignalBuilder scaffolding.

By default the view targets Flutter apps (imports package:zuraffa_flutter/zuraffa_flutter.dart and builds a Widget). With pureDart set, the emitted view imports only package:zuraffa/zuraffa.dart and wires fragments through ViewContext.attach — compilable and mountable inside any pure-Dart host (spec 038 FR-005/SC-003).

Implementation

String generateView(
  String name, {
  required List<String> useCases,
  List<String> uiSignals = const ['isLoading', 'activeTabIndex'],
  Map<String, String> uiSignalTypes = const {},
  bool pureDart = false,
}) {
  final className = '${name}View';
  final presenterName = '${name}Presenter';
  final fileName = '${snakeCase(name)}_view.dart';
  final filePath = p.join(outputDir, fileName);

  final library = cb.Library((b) {
    if (pureDart) {
      b.directives.add(cb.Directive.import('package:zuraffa/zuraffa.dart'));
    } else {
      b.directives.add(cb.Directive.import('package:flutter/material.dart'));
      // zuraffa_flutter re-exports zuraffa core AND provides
      // ControlledWidget, FragmentBuilder, and SignalBuilder.
      b.directives.add(
        cb.Directive.import('package:zuraffa_flutter/zuraffa_flutter.dart'),
      );
    }
    b.directives.add(
      cb.Directive.import('${snakeCase(name)}_presenter.dart'),
    );

    b.body.add(
      cb.Class((c) {
        c
          ..name = className
          ..extend = cb.refer('ControlledWidget<$presenterName>')
          ..constructors.add(
            cb.Constructor((ctor) {
              ctor
                // Flutter views stay `const`-constructible with an optional
                // key; pure-Dart views have no key concept (no widget tree
                // identity), so they take only the controller.
                ..constant = !pureDart
                // code_builder renders named parameters from
                // optionalParameters; named entries in requiredParameters
                // would emit invalid positional syntax.
                ..optionalParameters.addAll([
                  if (!pureDart)
                    cb.Parameter((p) {
                      p
                        ..name = 'key'
                        ..type = cb.refer('Key?')
                        ..named = true
                        ..toSuper = true;
                    }),
                  cb.Parameter((p) {
                    p
                      ..name = 'controller'
                      ..type = cb.refer(presenterName)
                      ..named = true
                      ..required = true
                      ..toSuper = true;
                  }),
                ]);
            }),
          );

        // onInit
        c.methods.add(
          cb.Method((m) {
            m
              ..name = 'onInit'
              ..returns = cb.refer('void')
              ..annotations.add(cb.refer('override'))
              ..body = cb.Block((bl) {
                for (final uc in useCases) {
                  // slice(key) takes the use-case name as its string key;
                  // refresh() on the nullable return is guarded with `?.
                  // Pure-Dart views instead touch the generated late-final
                  // slice FIELD (same name as the slice key): touching it
                  // bootstraps the lazy binding, so the refresh always
                  // reaches a real slice (spec 038 SC-001 — no manual
                  // wiring, no null-assert).
                  bl.statements.add(
                    cb.Code(
                      pureDart
                          ? 'controller.domain.$uc.refresh();'
                          : "controller.domain.slice('$uc')?.refresh();",
                    ),
                  );
                }
              });
          }),
        );

        // build
        c.methods.add(
          cb.Method((m) {
            m
              ..name = 'build'
              ..returns = cb.refer(pureDart ? 'Object?' : 'Widget')
              ..annotations.add(cb.refer('override'))
              ..requiredParameters.add(
                cb.Parameter((p) {
                  p
                    ..name = 'context'
                    ..type = cb.refer(
                      pureDart ? 'ViewContext' : 'BuildContext',
                    );
                }),
              )
              ..body = pureDart
                  ? _pureDartBuildBody(useCases, uiSignals, uiSignalTypes)
                  : cb.Block((bl) {
                      bl.statements.add(cb.Code('return Scaffold('));
                      bl.statements.add(cb.Code('  body: Column('));
                      bl.statements.add(cb.Code('    children: ['));

                      for (final uc in useCases) {
                        bl.statements.add(cb.Code('      // $uc slice'));
                        bl.statements.add(
                          cb.Code('      FragmentBuilder<dynamic>('),
                        );
                        bl.statements.add(
                          cb.Code(
                            "        slice: controller.domain.slice('$uc')!,",
                          ),
                        );
                        bl.statements.add(
                          cb.Code(
                            '        onLoading: (context) => const CircularProgressIndicator(),',
                          ),
                        );
                        bl.statements.add(
                          cb.Code(
                            '        onError: (context, error) => Text(error.message),',
                          ),
                        );
                        bl.statements.add(
                          cb.Code(
                            '        builder: (context, data) => Text(data.toString()),',
                          ),
                        );
                        bl.statements.add(cb.Code('      ),'));
                      }

                      if (uiSignals.isNotEmpty) {
                        bl.statements.add(cb.Code('      // UI signals'));
                        for (final signal in uiSignals) {
                          final type =
                              uiSignalTypes[signal] ??
                              _defaultUiSignalType(signal);
                          bl.statements.add(
                            cb.Code('      SignalBuilder<$type>('),
                          );
                          bl.statements.add(
                            cb.Code(
                              "        signal: controller.view.$signal,",
                            ),
                          );
                          bl.statements.add(
                            cb.Code(
                              '        builder: (context, value) => Text("$signal: \$value"),',
                            ),
                          );
                          bl.statements.add(cb.Code('      ),'));
                        }
                      }

                      bl.statements.add(cb.Code('    ],'));
                      bl.statements.add(cb.Code('  ),'));
                      bl.statements.add(cb.Code(');'));
                    });
          }),
        );
      }),
    );
  });

  final emitter = cb.DartEmitter();
  final raw = library.accept(emitter).toString();
  var formatted = raw;
  try {
    formatted = _formatter.format(raw);
  } on FormatterException {
    // Fallback: unformatted code is better than a crash. Narrow the catch
    // to FormatterException so unrelated generator bugs surface instead of
    // silently writing invalid output.
  }

  writeFile(filePath, formatted);
  return filePath;
}