generate method
Generates a new Flutter project according to config at targetPath.
Implementation
Future<void> generate(ProjectConfig config, String targetPath) async {
final progress = _logger.progress(
'Generating project "${config.projectName}" at $targetPath...');
try {
// 1. Run flutter create
progress.update('Running flutter create...');
final platformsString = config.platforms.join(',');
final createResult = await Process.run('flutter', [
'create',
'--org',
config.org,
'--project-name',
config.projectName.replaceAll('-', '_'),
if (platformsString.isNotEmpty) ...['--platforms', platformsString],
targetPath,
]);
if (createResult.exitCode != 0) {
progress.fail('Failed to run flutter create');
_logger.error('Flutter create stderr:\n${createResult.stderr}');
throw Exception('Flutter create failed');
}
final projectDir = Directory(targetPath);
if (!projectDir.existsSync()) {
progress.fail('Project directory was not created');
throw Exception('Project directory was not created');
}
// 2. Clear out standard lib/ contents
progress.update('Setting up architecture: ${config.architecture}...');
final libDir = Directory(p.join(targetPath, 'lib'));
if (libDir.existsSync()) {
libDir.deleteSync(recursive: true);
}
libDir.createSync();
// 3. Create architecture directories
List<String> directories = [];
switch (config.architecture) {
case 'Clean Architecture':
final smLower = config.stateManagement.toLowerCase();
String stateFolder = 'bloc';
if (smLower.contains('provider')) {
stateFolder = 'providers';
} else if (smLower.contains('cubit')) {
stateFolder = 'cubit';
} else if (smLower.contains('getx')) {
stateFolder = 'controllers';
} else if (smLower.contains('riverpod')) {
stateFolder = 'providers';
}
directories = [
'core/env',
'core/routes',
'core/theme',
'core/errors',
'core/network',
'core/extensions',
'core/services',
'core/usecases',
'core/utils',
'shared/components',
'shared/theme',
'shared/validation',
'di/modules',
'features/auth/data/datasources',
'features/auth/data/models',
'features/auth/data/repositories',
'features/auth/domain/entities',
'features/auth/domain/repositories',
'features/auth/domain/usecases',
'features/auth/presentation/$stateFolder',
'features/auth/presentation/pages',
'features/auth/presentation/widgets',
];
break;
case 'MVVM':
directories = [
'core/errors',
'core/network',
'core/utils',
'core/theme',
'core/routes',
'core/env',
'models',
'views',
'viewmodels',
'services',
'utils',
'injection',
];
break;
case 'MVC':
default:
directories = [
'core/errors',
'core/network',
'core/utils',
'core/theme',
'core/routes',
'core/env',
'models',
'views',
'controllers',
'services',
'utils',
'injection',
];
break;
}
for (final dir in directories) {
final dirFile = Directory(p.join(libDir.path, dir));
dirFile.createSync(recursive: true);
// Create .gitkeep so empty directories are tracked in source control
File(p.join(dirFile.path, '.gitkeep')).createSync();
}
// 4. Generate main.dart
final templateEngine = StringTemplateEngine();
final variables = {
'project_name': config.projectName,
'project_name_pascal': _toPascalCase(config.projectName),
'architecture': config.architecture,
'state_management': config.stateManagement,
};
final mainTemplate =
TemplateBundle.getTemplate('project/main.dart.stub') ?? '';
final mainContent = templateEngine.render(mainTemplate, variables);
File(p.join(libDir.path, 'main.dart')).writeAsStringSync(mainContent);
// 5. Git initialization
if (config.gitInit) {
progress.update('Initializing Git repository...');
await Process.run('git', ['init'], workingDirectory: targetPath);
}
// 5.5 Generate project.yaml
progress.update('Generating project.yaml...');
final configService = YamlConfigService();
configService.setValue('project', config.toYamlMap());
await configService.save(p.join(targetPath, 'project.yaml'));
// 6. Generate README
if (config.generateReadme) {
progress.update('Generating README.md...');
final readmeTemplate =
TemplateBundle.getTemplate('project/README.md.stub') ?? '';
final readmeContent = templateEngine.render(readmeTemplate, variables);
File(p.join(targetPath, 'README.md')).writeAsStringSync(readmeContent);
}
// 6.5 Generate core assets
progress.update('Generating core components & assets...');
final originalDir = Directory.current;
Directory.current = targetPath;
try {
final componentGenerator = ComponentGenerator(_logger, templateEngine);
await componentGenerator.generateComponent('assets', 'assets');
await componentGenerator.generateComponent('theme', 'theme');
await componentGenerator.generateComponent('network', 'network');
await componentGenerator.generateComponent('error', 'error');
await componentGenerator.generateComponent('extensions', 'extensions');
await componentGenerator.generateComponent('routes', 'routes');
} finally {
Directory.current = originalDir;
}
// 7. Add Dependencies
progress.update('Adding selected dependencies...');
final engine =
DependencyEngine(pubspecPath: p.join(targetPath, 'pubspec.yaml'));
final registry = PackageRegistry();
final allDependencies = Set<String>.from(config.dependencies)
..addAll(['dio', 'pretty_dio_logger', 'go_router']);
for (final package in allDependencies) {
final version = registry.getVersion(package) ?? 'any';
final isDev = package == 'build_runner' ||
package == 'lints' ||
package == 'flutter_lints' ||
package.endsWith('_generator');
await engine.addDependency(package, version, isDev: isDev);
}
// 8. Post-generation commands
progress.update('Running flutter pub get...');
final pubGetResult = await Process.run('flutter', ['pub', 'get'],
workingDirectory: targetPath);
if (pubGetResult.exitCode != 0) {
_logger.error('flutter pub get failed:\n${pubGetResult.stderr}');
}
progress.update('Running dart format...');
final formatResult = await Process.run('dart', ['format', '.'],
workingDirectory: targetPath);
if (formatResult.exitCode != 0) {
_logger.error('dart format failed:\n${formatResult.stderr}');
}
progress
.complete('Successfully generated project "${config.projectName}"!');
} catch (e) {
progress.fail('Failed to generate project "${config.projectName}"');
rethrow;
}
}