crudViewTemplate function

String crudViewTemplate(
  1. String name,
  2. List<FieldSpec> fields,
  3. ProjectContext ctx
)

A list view bound to the CRUD controller, with loading/error/empty states. Uses the widget library + design tokens when those modules are installed.

Implementation

String crudViewTemplate(
    String name, List<FieldSpec> fields, ProjectContext ctx) {
  final viewName = '${Naming.pascal(name)}View';
  final modelName = Naming.pascal(name);
  final title = Naming.title(name);
  final provider = '${Naming.camel(name)}ControllerProvider';
  final snake = Naming.snake(name);
  final pkg = ctx.packageName ?? 'app';

  // Pick the primary field to display per row (first non-list field, or first).
  final display = fields.isEmpty
      ? 'item.toString()'
      : 'item.${(fields.firstWhere((f) => !f.fullType.startsWith('List'), orElse: () => fields.first)).displayExpr}';

  final imports = <String>[
    "import 'package:flutter/material.dart';",
    "import 'package:flutter_riverpod/flutter_riverpod.dart';",
    "import '../bindings/${snake}_binding.dart';",
  ];

  // Convention-aware fragments.
  final String appBarTitle;
  final String emptyState;
  final String errorState;
  final String loadingState;
  final String refreshOpen;
  final String refreshClose;

  if (ctx.hasWidgets) {
    imports
      ..add("import 'package:$pkg/app/shared_widgets/my_text.dart';")
      ..add("import 'package:$pkg/app/shared_widgets/empty_state_widget.dart';")
      ..add("import 'package:$pkg/app/shared_widgets/app_error_banner.dart';")
      ..add(
          "import 'package:$pkg/app/shared_widgets/custom_loading_spinner.dart';")
      ..add(
          "import 'package:$pkg/app/shared_widgets/app_refresh_indicator.dart';");
    appBarTitle = "const MyText(title: '$title')";
    emptyState = 'const EmptyStateWidget()';
    errorState = 'AppErrorBanner(message: error.toString())';
    loadingState = 'const CustomLoadingSpinner()';
    refreshOpen =
        'AppRefreshIndicator(\n        onRefresh: () => ref.read($provider.notifier).refresh(),\n        child: ';
    refreshClose = ',\n      )';
  } else {
    appBarTitle = "const Text('$title')";
    emptyState = "const Center(child: Text('No items yet.'))";
    errorState = "Center(child: Text(error.toString()))";
    loadingState = 'const Center(child: CircularProgressIndicator())';
    refreshOpen =
        'RefreshIndicator(\n        onRefresh: () => ref.read($provider.notifier).refresh(),\n        child: ';
    refreshClose = ',\n      )';
  }

  final rowTitle =
      ctx.hasWidgets ? 'MyText(title: $display)' : 'Text($display)';

  return '''
${imports.join('\n')}

class $viewName extends ConsumerWidget {
  const $viewName({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch($provider);
    return Scaffold(
      appBar: AppBar(title: $appBarTitle),
      body: state.when(
        loading: () => $loadingState,
        error: (error, _) => $errorState,
        data: (items) {
          if (items.isEmpty) return $emptyState;
          return ${refreshOpen}ListView.separated(
          itemCount: items.length,
          separatorBuilder: (_, __) => const Divider(height: 1),
          itemBuilder: (context, index) {
            final item = items[index];
            return ListTile(
              title: $rowTitle,
            );
          },
        )$refreshClose;
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // TODO: open a form and call ref.read($provider.notifier).add($modelName(...))
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}
''';
}