Create a CLI tool to help bootstraping Flutter project using Node.JS - Part 2
AILI Fida Aliotti Christino
Posted on February 16, 2023
What we've done so far
In the first part of the article, we've:
- created a cli node project
- interact with user about the application informations
- create a project using these informations
- create folder structure along with it
Now that we've got our folder structure, in this article, we will add the files according what we want to add by default and implement the module creation part.
Here are the utility files that we want to add:
-
app.dart
: entry point of our app -
transitions.dart
: helps us to add a custom animation for page transitions -
navigation.dart
: contains a function to change page using our custom animation -
spacing.dart
: contains some functions to help us deal with spacing
We will also change the main.dart
to match the adding of the app.dart
Template contents
Let's create a folder where we will hold our template files. Of course, i named mine templates (how original! š±) and create our files.
Let's start by creating the spacing.dart
and paste this:
import 'package:flutter/material.dart';
SizedBox space({double? width, double? height}) {
return SizedBox(
width: width ?? 0,
height: height ?? 0,
);
}
double width(BuildContext context) {
return MediaQuery.of(context).size.width;
}
double height(BuildContext context) {
return MediaQuery.of(context).size.height;
}
As you can see, it's just a bunch of functions like for example space which will help me set a space where i want.
Now, let's add the transitions.dart
which will use the animations module:
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,
);
});
}
}
Like i said up there, it's a class that will implement a custom animation on page transitions.
Now let's write the navigation.dart
:
import 'package:flutter/material.dart';
import 'transitions.dart';
void goto(BuildContext context, Widget screen, {bool isReplaced = false}) {
isReplaced
? Navigator.of(context)
.pushReplacement(Transitions.sharedAxisPageTransition(screen))
: Navigator.of(context)
.push(Transitions.sharedAxisPageTransition(screen));
}
As you can see, this will just use our class declared in our previous transitions.dart
.
We've got our utilities now, let's now see what is inside our app.dart
file:
import 'package:flutter/material.dart';
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '#title',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: null,
);
}
}
Notice that we've set the title value to: #title
. We will talk about it later.
And finally, our main.dart
file:
import 'package:flutter/material.dart';
import './src/app.dart';
void main() {
runApp(const App());
}
Ok, we got all of our file for now. What we need to do is to copy the utilities inside the src/helpers folder, the app.dart
inside the src folder and change the main.dart
file.
For simplicity, let's create a helpers folder inside our templates and move these files inside it: spacing.dart, transitions.dart, navigation.dart
.
Let's get back to the utils
and these lines:
// utils.js
import { resolve, join, dirname } from "path";
import { error, success, info } from "./log";
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync } from "fs";
const createFolders ...
...
const copyDirectoryContent = (source, destination) => {
mkdirSync(destination, { recursive: true }); // Create the destination directory if it doesn't exist
// Read our directory
readdirSync(source).forEach((file) => {
const srcPath = join(source, file);
const destPath = join(destination, file);
if (statSync(srcPath).isDirectory()) {
copyDirectoryContent(srcPath, destPath); // Recursively copy subdirectories
} else {
copyFileSync(srcPath, destPath); // Copy files
}
});
};
const copyFiles = (project) => {
const templateDir = resolve(dirname(""), "templates");
// Copy all our files from inside our templates/helpers dir to the project
copyDirectoryContent(
resolve(templateDir, "helpers"),
resolve(project, "lib/src/helpers")
);
};
export { createFolders, copyFiles };
Now, let's update our actions.js
file to use it:
// actions.js
...
dependencies.on("close", () => {
success(`Dependencies are correctly added...`);
info(`Creating folders...`);
createFolders(project);
info(`Copying utility files...`);
copyFiles(project);
success("Project created correctly");
});
...
Don't forget to import the copyFiles from the utils.js
file.
Now, let's test our code and... Yes! It will first create the project, add dependencies and add our helper files.
Ok, so far so good š š
Let's not forget our main.dart
and app.dart
files.
// utils.js
...
const copyFiles = (project) => {
const templateDir = resolve(dirname(""), "templates");
const appFile = resolve(project, "lib/src/app.dart");
copyDirectoryContent(
resolve(templateDir, "helpers"),
resolve(project, "lib/src/helpers")
);
copyFileSync(
resolve(templateDir, "main.dart"),
resolve(project, "lib/main.dart")
);
copyFileSync(resolve(templateDir, "app.dart"), appFile);
};
...
OK. I've add the main.dart
file to the list of the file to copy. Now, time to add app.dart
. Let's talk about it:
If you remember, our app.dart
file has a weird line inside it:
return MaterialApp(
title: '#title',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: null,
);
You called it, it's the title: '#title'
. That because we will replace it by the name of our project. But we need to copy it first then replace its content.
// utils.js
...
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const copyFiles = (project) => {
const templateDir = resolve(dirname(""), "templates");
const appFile = resolve(project, "lib/src/app.dart");
copyDirectoryContent(
resolve(templateDir, "helpers"),
resolve(project, "lib/src/helpers")
);
copyFileSync(
resolve(templateDir, "main.dart"),
resolve(project, "lib/main.dart")
);
copyFileSync(resolve(templateDir, "app.dart"), appFile);
const data = readFileSync(appFile, "utf8").replace(
"#title",
capitalize(project)
);
writeFileSync(appFile, data);
};
...
That's it! We copy the app.dart
first, then we will read it and replace the string #title by the name of the project capitalized.
Time to test! š
> fluttertool
? What do you want to do? 1 - Create a new project
========================================
OK! Let's create your wonderful project!
========================================
? What is the name of your project? myproject
? What is the name of your organization? test
Creating project skeleton...
Adding dependencies...
ā Dependencies are correctly added...
Creating folders...
ā Folders and files are correctly created
Copying utility files...
ā Project created correctly
Yes! We made it! Time for the next step: creating module.
Stacked Module
I will not discuss about the stacked module as it will require an entire about it (let me know if you want one).
A module consists of two files: the view and the viewmodel that we will copy inside lib/src/views/screens folder.
Let's create our module template inside our templates directory.
Don't worry about the files being red.
Let's see what inside them:
// module_viewmodel.dart
import 'package:stacked/stacked.dart';
class ModuleViewModel extends BaseViewModel {}
// module_view.dart
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import './module_viewmodel.dart';
class ModuleView extends StatelessWidget {
const ModuleView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ModuleViewModel>.reactive(
viewModelBuilder: () => ModuleViewModel(),
builder: (
BuildContext context,
ModuleViewModel model,
Widget? child,
) {
return Scaffold(
body: Center(
child: Text(
'ModuleView',
),
),
);
},
);
}
}
We will apply the same process as we did with the app.dart
.
But we will add some extra lines inside utils.js
:
// utils.js
import { resolve, join, dirname } from "path";
import { error, success, info } from "./log";
import {
existsSync,
mkdirSync,
readdirSync,
statSync,
copyFileSync,
readFileSync,
writeFileSync,
} from "fs";
let SCREENPATH = "lib/src/views/screens";
...
const renameModuleFiles = (path, module, filename) => {
const changes = readFileSync(path, "utf8")
.replaceAll("Module", capitalize(module))
.replace("module", module);
writeFileSync(
`${SCREENPATH}/${module}/${filename.replace(
"module",
module.toLowerCase()
)}`,
changes
);
};
const createStackedModuleBoilerplate = (module) => {
const templateDir = resolve(dirname(""), "../templates/modules");
const files = ["module_view.dart", "module_viewmodel.dart"];
try {
const _path = resolve(SCREENPATH, module);
if (!existsSync(_path)) {
mkdirSync(_path);
}
files.forEach((file) => {
const _path = resolve(SCREENPATH, module, file);
if (!existsSync(_path)) {
renameModuleFiles(resolve(templateDir, file), module, file);
}
});
success(`${capitalize(module)} module is correctly created`);
} catch (err) {
error(err);
}
};
...
export { createFolders, copyFiles, createStackedModuleBoilerplate };
Ok, what's going on here:
- We create a folder according to the name the user gave
- We copy the files from templates/modules inside that folder
- Rename those file copied and replace all placeholder string inside it by the name of the module
All we have to do is to call this function inside our actions.js
// actions.js
...
const createModule = async () => {
const {module} = await inquirer.prompt([
{
type: "input",
name: "project",
message: "What is the name of your module?",
},
]);
if (module) {
createStackedModuleBoilerplate(module);
return;
}
error("You need to specify a module name!");
};
...
ā Careful!, as we will create the module inside the lib/src/views/screens we need to be in the root of the project to have it working.
Test time ā¼ļø
> fluttertool
? What do you want to do? 2 - Add a new stacked module
? What is the name of your module? signin
ā Signin module is correctly created
Yay! š„³š„³
Let's do a full example by creating a project and adding a home module to it:
> fluttertool
? What do you want to do? 1 - Create a new project
========================================
OK! Let's create your wonderful project!
========================================
? What is the name of your project? myproject
? What is the name of your organization? test
Creating project skeleton...
Adding dependencies...
ā Dependencies are correctly added...
Creating folders...
ā Folders and files are correctly created
Copying utility files...
ā Project created correctly
> cd myproject && fluttertool
? What do you want to do? 2 - Add a new stacked module
? What is the name of your module? home
ā Home module is correctly created
What we've got now:
Let's see what we have inside our home_view.dart
and home_viewmodel.dart
What we've done so far:
- adding our template files and arrange them correctly
- change our
main.dart
file when we addapp.dart
- implement our module creator
What we will see next in the last step is how we can add shortcuts to our cli tool like:
> fluttertool -m <modulename>
> flutter -h
Stay tune! See you next time! š
Posted on February 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 16, 2023