crudViewTemplate function
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),
),
);
}
}
''';
}