Create your own CLI tool with Dart for the Flutter project

Create your own CLI tool with Dart for the Flutter project

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!🚀

Introduction

Introduction

Have you ever thought about creating your own CLI tool for your Flutter project?

I think maybe everyone has their own project architecture or uses some common architecture. If you are using GetX then you can use get_cli to generate the architecture, it can help you reduce your work and save time.

For example, one of my GetX project structure as below

The project base on GetX

For this project, there is the same structure with page items, I need to create each file manually if I want to add a new page.

So, I want to create a CLI tool to help it!

Create a console app with Dart

Use the below command to create a console project and name to CLI


dart create -t console-full cli

It will create the following files in the folder

The structure as below

Try to run it

dart run bin/cli.dart

You will get the following result

Hello world: 42!

And you can compile it to an executable file

dart compile exe bin/cli.dart

Ok, this is the base for creating a console app, but we need more. I will show you how to generate a simple template as I mentioned above.

DCli

DCli is the console SDK for Dart. Use the DCli console SDK to build cross platform, command line (CLI) applications and scripts using the Dart programming language. The DCli (pronounced d-kleye) console SDK includes command line tools and an extensive API for building CLI apps.

This is a powerful SDK for helping to create a console app.

DCli has the following aims:

  • make building CLI apps as easy as walking.
  • fully utilise the expressiveness of Dart.
  • works seamlessly with the core Dart libraries.
  • provide a cross platform API for Windows, OSx and Linux.
  • call any CLI app.
  • make debugging CLI apps easy.
  • generate error messages that make it easy to resolve problems.
  • provide quality documentation and examples.

You can install it as below command

dart pub global activate dcli
dcli install

You can find the detailed usage on their official website. They provided comprehensive documentation, and I will show you how to create and generate the codes in this article 😀.

Create Template

For the above example, we need to create the following folder structure

So we can create the template files and folders in lib folder

For a simple example, the controller.template content as below

class {{className}}Controller extends GetxController with BaseControllerMixin {
@override
String get builderId => '{{pageName}}';

{{className}}Controller();

@override
void onInit() {
super.onInit();
}

/// Whether to monitor lifecycle events
@override
bool get listenLifecycleEvent => true;

/// When listenLifecycle Event is set to true, the following lifecycle methods will be called
@override
void onDetached() {
log('onDetached');
}

@override
void onHidden() {
log('onHidden');
}

@override
void onInactive() {
log('onInactive');
}

@override
void onPaused() {
log('onPaused');
}

@override
void onResumed() {
log('onResumed');
}
}

Please note that this is only for demo, so the code is not complete, and there are no required libraries to be imported in this template.

The view.template file

class {{className}}Page extends GetView<{{className}}Controller> {
const {{className}}Page({super.key});
}

The index.template file

library {{pageName}};

export 'controller.dart';
export 'view.dart';

As you can see, there are some variables in the template, and we will replace them dynamically with the CLI command.

Implement the CLI functions

Add the dcli dependence into pubspec.yaml

dependencies:
dcli: ^7.0.3 #use the latest version

For a common CLI tool, should be supported, such as -help or -version arguments to let the user know how to use and what the current version.

So we need to handle these arguments first:

final parser =
ArgParser()
..addFlag(
'help',
abbr: 'h',
help: 'Show usage information.',
negatable: false,
)
..addFlag(
'version',
abbr: 'v',
help: 'Show version information.',
negatable: false,
);

final results = parser.parse(arguments);

if (results['help']) {
printUsage(parser);
exit(0);
}

if (results['version']) {
printVersion();
exit(0);
}

I want to use this CLI to create a new page as below the command based on template files


cli create page:home

Suppose after running the above command, it will generate the following files and folders

Now we need to handle the create page:homearguments

final command = arguments.sublist(1).join(' ');

if (!command.startsWith('page:')) {
print('Invalid command format. Expected: create page:<pagename>');
printUsage(parser);
exit(1);
}

final pageName = command.split(':')[1];
createPage(pageName);

Create the pages folder by command and overwrite if it exists

final dirPath = path.join(Directory.current.path, 'pages', pageName);

// Delete directory if it exists
if (exists(dirPath)) {
find(
'*',
workingDirectory: dirPath,
recursive: true,
types: [Find.file],
).forEach(delete);
deleteDir(dirPath);
print('Deleted existing directory $dirPath');
}

// Create directory
createDir(dirPath, recursive: true);
print('Created directory $dirPath');

Load template files and dynamically replace the variables

// PascalCase conversion for class names
final className = toPascalCase(pageName);

// Load templates from the package
final templateDir = 'lib/templates/page';
final controllerTemplatePath = path.join(templateDir, 'controller.template');
final viewTemplatePath = path.join(templateDir, 'view.template');
final indexTemplatePath = path.join(templateDir, 'index.template');

final controllerTemplate = loadAsset(controllerTemplatePath);
final viewTemplate = loadAsset(viewTemplatePath);
final indexTemplate = loadAsset(indexTemplatePath);

if (controllerTemplate == null ||
viewTemplate == null ||
indexTemplate == null) {
print(
'One or more template files are missing in the lib/templates/page directory.',
);
exit(1);
}

// Replace placeholders in templates
final controllerContent = controllerTemplate
.replaceAll('{{className}}', className)
.replaceAll('{{pageName}}', pageName);
final viewContent = viewTemplate.replaceAll('{{className}}', className);
final indexContent = indexTemplate.replaceAll('{{pageName}}', pageName);

// Create or overwrite controller.dart
final controllerFilePath = path.join(dirPath, 'controller.dart');
File(controllerFilePath).writeAsStringSync(controllerContent);
print('Created or overwritten file $controllerFilePath');

// Create or overwrite view.dart
final viewFilePath = path.join(dirPath, 'view.dart');
File(viewFilePath).writeAsStringSync(viewContent);
print('Created or overwritten file $viewFilePath');

// Create or overwrite index.dart
final indexPath = path.join(dirPath, 'index.dart');
File(indexPath).writeAsStringSync(indexContent);
print('Created or overwritten file $indexPath');

The below function will load the template files base project folder

String? loadAsset(String assetPath) {
final scriptFile = Platform.script.toFilePath();
final scriptDir = path.dirname(scriptFile);
final packageRoot = path.join(
scriptDir,
'..',
); // Assuming bin is one level below the root
final filePath = path.join(packageRoot, assetPath);
// final filePath = path.join(scriptDir, assetPath);
try {
final content = File(filePath).readAsStringSync();
return content;
} catch (e) {
print('Failed to load asset: $filePath');
return null;
}
}

I didn't show all of the code above, but just the core code. You can find the complete project here.

Test the CLI

After done, you can run the CLI as a console app below

dart bin/cli.dart -v

You should get below result

cli version v1.0.0

And try to create the page item

dart bin/cli.dart create page:home

It will generate 3 files based on the template files

pages/home.dartfile

class HomeController extends GetxController with BaseControllerMixin {
@override
String get builderId => 'home';

HomeController();

@override
void onInit() {
super.onInit();
}

/// Whether to monitor lifecycle events
@override
bool get listenLifecycleEvent => true;

/// When listenLifecycle Event is set to true, the following lifecycle methods will be called
@override
void onDetached() {
log('onDetached');
}

@override
void onHidden() {
log('onHidden');
}

@override
void onInactive() {
log('onInactive');
}

@override
void onPaused() {
log('onPaused');
}

@override
void onResumed() {
log('onResumed');
}
}

pages/view.dartfile

class HomePage extends GetView<HomeController> {
const HomePage({super.key});
}

pages/index.dart file

library home;

export 'controller.dart';
export 'view.dart';

That's great, we're almost done!

Activate to Global Command

Because there are template files and folders with the CLI , so if you copy the executable file to your project folder and you also need to copy the template files, this is not what we want. I think you will want to use your CLI tool anywhere, right?

Ok, let's do it!

We need to activate the CLI project as a global tool, and also need to include the template files, so that we can update the pubspec.yaml file below

executables:
cli: cli

# Include templates in the package
include:
- lib/templates/page/

And run the below command in the project root folder to activate it to global command

dart pub global activate --source path .

You can use the command below to see whether it has been added to the global

dart pub global list

If you want to remove it, just run the below

dart pub global deactivate cli

Test the Global Version

Now you can use the CLI anywhere

cli create page:home

If you encounter the following problems

Failed to load asset: /Volumes/cli/.dart_tool/pub/bin/cli/../lib/templates/page/controller.template
Failed to load asset: /Volumes/cli/.dart_tool/pub/bin/cli/../lib/templates/page/view.template
Failed to load asset: /Volumes/cli/.dart_tool/pub/bin/cli/../lib/templates/page/index.template

That's because the app can't get the current folder, it will copy to the .dart_tool folder after publishing to the global, the solution is you can hardcode the project folder in loadAsset

 final packageRoot = '/Volumes/cli/'; //hardcode to your CLI project folder

final filePath = path.join(packageRoot, assetPath);
...

Update the New Version

At the end, if you edit the CLI code want to publish again, you will find that's not working, you need to follow below steps:

  1. Delete the .dart_tool folder in your project root.
  2. Get the project dependencies again. (save the pubspec.yaml file again)
  3. Don't need to run the activate global command, but need to run the below for activation again:
cli -h

You can find the complete project below

GitHub - coderblog-winson/cli_demo: The demo for how to create CLI tool by dart

The demo for how to create CLI tool by dart. Contribute to coderblog-winson/cli_demo development by creating an account…

github.com

Conclusion

Actually, the CLI tool not only for Flutter project, you can create any tools that you want, which can save your time for duplicate tasks.

The DCli is also very powerful and lets you do a lot of things, so I suggest you learn and try it if you want to create a CLI 😁

In the end, if you enjoyed this article, please

  1. Clap 10 times (seriously, it motivates me!) 👏
  2. Subscribe for more tutorials.
  3. Follow me to never miss an update.

Got Questions? Ask below! I'll reply to every comment.

Thank you for being a part of the community

Before you go:

Report Page