Hey folks! I know i'm not the only one who struggles and wastes time when i create every new flutter project. So i've decided to make a tool for helping me bootstraping my projects. It saves me around 1 hour of my time everytime i start a new personal project.
The problem
Let's start by describing what i do when i start every project:
- create a new flutter project with the name in it
- create a
src
folder - create a bunch of utilities folder arranged like this:
The transitions.dart
file for example hold some code for page transitions using animations. Here is what it looks like:
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
class Transitions {
static Route sharedAxisPageTransition(Widget screen,
{bool isHorizontal = true}) {
final SharedAxisTransitionType _transitionType = isHorizontal
? SharedAxisTransitionType.horizontal
: SharedAxisTransitionType.vertical;
return PageRouteBuilder<SharedAxisTransition>(
pageBuilder: (context, animation, secondaryAnimation) => screen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: _transitionType,
child: child,
);
});
}
}
- and finally create a folder with respective view (
entity_view.dart
) and logic (entity_viewmodel.dart
) for each entity (screen) i need. I'm pointing out that i'm using the stacked package as a state management.
The solution
Ok, now that we saw what were the steps took, let's try to automate them. I decided to use NodeJS as i was comfortable with it but you can use any tool that you like to build CLI.
Create our project
First, let's create our project. Again, you can use any package manager that you like, it doesn't matter.
Create a folder to hold your pretty CLI and let's start:
mkdir fluttertool && cd fluttertool && yarn init -y && code .
Nothing special here, it will create our folder, create a package.json
file inside it and open VSCode for us.
Now that we have our starting point, let's add some informations inside our package.json
file.
Initially, we have something like this:
{
"name": "fluttertool",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
Let's add some lines to tell that we will use this project as a cli tool.
{
"name": "flutternodetool",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module"
"bin": {
"fluttertools": "./index.js"
},
}
Ok. That last line specifies a command-line interface (CLI) for the module. When the module is installed, a command-line tool called fluttertools will be created that can be used to run the module's functionality.
The bin
property maps the name of the command-line tool to the file that should be executed when the tool is run, in this case index.js.
Let's create this index.js
file:
#! /usr/bin/env node --es-module-specifier-resolution=node
console.log("Hello from my fluttertool");
The first line of code, #! /usr/bin/env node
, is known as a "shebang" or "hashbang" and is used to specify the interpreter for the script.
In our case, it specifies that the script should be run using the node interpreter, which is typically used for executing JavaScript code on the command line.
Ok, let's test our code!
sudo npm i -g .
With this line, we will install our current package globally, so we will be able to run our tool using our alias previously set: "fluttertool".
Now, try to run:
fluttertool
inside your terminal and you will see the message we set before inside our index.js
file.
Now we have our starting point. Let's add some fancy tools to it.
Dependencies
We will add some dependencies to help us deal with CLI behaviours (inquirer) and text coloring (chalk).
yarn add inquirer chalk
ESLint
Let's also add eslint
to help us with checking syntax and find problems in our code. Run this command:
yarn eslint --init
Answer to these questions when prompted:
? How would you like to use ESLint?
❯ To check syntax and find problems
? What type of modules does your project use?
❯ JavaScript modules (import/export)
? Which framework does your project use?
❯ None of these
? Does your project use TypeScript? › No
? Where does your code run? … (Press <space> to select, <a> to toggle all, <i> to invert selection)
✔ Node
? What format do you want your config file to be in?
❯ JavaScript
Successfully created .eslintrc.cjs
Now that've got our tools set, we can jump into our commands.
Expected Commands
We will have a bunch of command to implement. But for now, let's implement the necessary ones:
Project creation
What my tool will mainly do is create a flutter project for me, add my helper files and init project with an entry file (defaults to "Home").
What i'm expecting is when i run this:
fluttertool
My tool will ask me the name of my app and the organization name, create a flutter project with these informations, create an src directory and organize folders and files like we described previously:
And finally add my helper files. Ok, let's jump back into our index.js
file.
#! /usr/bin/env node --es-module-specifier-resolution=node
import inquirer from "inquirer";
async function run() {
const { choice } = await inquirer.prompt({
type: "list",
name: "choice",
message: "What do you want to do?",
choices: [
"1 - Create a new project",
"2 - Add a new stacked module",
],
});
}
run();
What's going on here. Well we are calling inquirer
module which will help us to interact with the user. We use the prompt
method to display informations to the user and, in that case, we are waiting for actions by showing him the options that our tool offers.
Let's re-run our command. As our tool has been installed globally, every changes we make in our code are automatically applied.
> fluttertool
? What do you want to do? (Use arrow keys)
❯ 1 - Create a new project
2 - Add a new stacked module
As you can see, the user can select an option by using the arrow keys and the result will be stored inside the choice
variable.
Let's now implement the actions and the log helper. Create a file named log.js
and actions.js
.
// log.js
import chalk from "chalk";
const log = console.log;
const text = (text) => log(chalk.white(`${text}`));
const success = (text) => log(chalk.green(`✔ ${text}`));
const info = (text) => log(chalk.blue(`${text}`));
const error = (text) => log(chalk.red(`✖ ${text}`));
const renderTitle = () =>
log(
chalk.bold.magenta(`
░█▀▀░█░░░█░█░▀█▀░▀█▀░█▀▀░█▀▄░░░▀█▀░█▀█░█▀█░█░░░█▀▀
░█▀▀░█░░░█░█░░█░░░█░░█▀▀░█▀▄░░░░█░░█░█░█░█░█░░░▀▀█
░▀░░░▀▀▀░▀▀▀░░▀░░░▀░░▀▀▀░▀░▀░░░░▀░░▀▀▀░▀▀▀░▀▀▀░▀▀▀
`)
);
export { success, info, text, error, renderTitle };
Pretty cool hun? You can generate your own text here
Now let's return to our actions.js
file and write this:
const createProject = () => {}
const createModule = () => {}
const actions = (option) => {
if (option.startsWith(1)) {
createProject();
}
if (option.startsWith(2)) {
createModule();
}
};
export default actions;
And change our index.js
file to:
#! /usr/bin/env node --es-module-specifier-resolution=node
import inquirer from "inquirer";
import actions from "./actions";
async function run() {
const { choice } = await inquirer.prompt({
type: "list",
name: "choice",
message: "What do you want to do?",
choices: ["1 - Create a new project", "2 - Add a new stacked module"],
});
actions(choice);
}
run();
I think that it's pretty obvious here that actions will dispatch our action according to the option the user gave.
Let's implement our createProject
function.
// actions.js
const createProject = async () => {
info("\n========================================");
info("OK! Let's create your wonderful project!");
info("========================================\n");
const { project, organization } = await inquirer.prompt([
{
type: "input",
name: "project",
message: "What is the name of your project?",
default: "myproject",
},
{
type: "input",
name: "organization",
message: "What is the name of your organization?",
default: "test",
},
]);
text("Creating project skeleton...");
const command = spawn("flutter", [
"create",
project,
"--org",
`com.${organization}.${project}`,
]);
command.stderr.on("data", (data) => {
error(data);
process.exit(0);
});
};
What our code is doing here is when the user selects the createProject
option, we will ask him for the project name (defaults to "myproject") and the organization name (defaults to "test") and then the tool will launch a command like you generally do when creating a flutter project.
flutter create <projectname> --org <organizationname>
Now will wait for the command to complete and then we will make the next operations: adding dependencies (animations and stacked)
...
command.stderr.on("data", (data) => {
error(data);
process.exit(0);
});
command.on("close", () => {
text("Adding dependencies...");
const dependencies = spawn(
"flutter",
["pub", "add", "animations", "stacked"],
{ cwd: project }
);
});
The spawn
command here needs to be executed inside our newly created project, that's why we are changing our working directory to the name of the project here.
Again it will add dependencies like you would do in your regular flutter application.
Next we will create our main project structure:
For the sake of readability, let's create another file called: utils.js
.
// utils.js
import { resolve } from "path";
import { error, success } from "./log";
import { existsSync, mkdirSync } from "fs";
const createFolders = (project) => {
const folders = [
"src",
"src/views",
"src/views/screens",
"src/views/widgets",
"src/services",
"src/helpers",
"src/models",
];
try {
for (const folder of folders) {
const _path = resolve(project, "lib", folder);
if (!existsSync(_path)) {
mkdirSync(_path);
}
}
success("Folders and files are correctly created");
} catch (err) {
error(err);
process.exit(0);
}
};
export { createFolders };
Now let's update our actions.js
file to use this:
// actions.js
...
command.on("close", () => {
text("Adding dependencies...");
const dependencies = spawn(
"flutter",
["pub", "add", "animations", "stacked"],
{ cwd: project }
);
dependencies.stderr.on("data", (data) => {
error(`Cannot find folder ${project} - ${data}`);
process.exit(0);
});
dependencies.on("close", () => {
success(`Dependencies are correctly added...`);
info(`Creating folders...`);
createFolders(project);
});
});
That closes our first part of the article. What we've done so far:
- created a cli node project
- interact with user about the application informations
- create a project using these informations
- create folder structure along with it
Please like the article or leave a comment if you'd like to see the part 2 where we will:
- add template files
- implement the module creation action
Thanks! 😄 See you next time! 👋
Top comments (2)
Very useful since I use the same folder structure like yours!!! Can't wait to see the part 2 and use this tool for my next flutter project creation
Very interesting