Learn the basics
The main idea of ShipFlutter is get you started fast! We use a simple, yet effective approach, for structuring the code, manage state and inject dependencies to make it easy to understand and statisfy most of the needs to launch a project with Flutter.
This guide covers the basic structure and patterns of the project that is common to all modules.
Project Structure
The project follows a simple structure, with a root folder for each top-level module (e.g. app
, functions
, landing
)
and root-level configuration files. Inside the Flutter project the the structure is divided in the following way:
Directory.github/workflows/ Actions to deploy the project
- …
Directoryapp Flutter project root folder
Directorylib
Directoryenv/ The generated env class with envied
- …
Directoryi18n
- strings.i18n.json Json that defines the app localization
Directorymodules/ All included modules
Directorybase/
- …
Directoryonboarding/
- …
Directorymonitoring/
- …
- …
Directorytheme/ Theme configuration logic
- …
- app.dart Root widget with routing and theming setup
- main.dart Main entry point and startup logic
- navigation.dart Navigation component configuration
Directoryassets/ All images and resources files
- …
Directoryandroid/ios/web/ Platform specific folders
- …
- .env Environment variables file
- pubspec.yaml Dependencies and Flutter project config
- setup.sh Small script to run the setup commands
- slang.yaml Localization configuration
- …
Directoryfunctions Only present for functions module
- …
Directorylanding Only present for landing module
- …
- firebase.json
- … Other configuration files
- README.md
Feature structure
Each UI-feature is strucutred following this pattern:
Directoryhome
- home_view.dart
- home_route.dart
- home_controller.dart // The business logic of this view
- home_service.dart //
Directorywidgets/ // Any widget needed by the home_view.dart
- …
- View: The feature main widget/view that wires the controller and displays the state.
- Route: Defines how to create a route for this view (used by the navigation). Either a NavigationRoute for routes that are part of the bottom/rail/drawer navigation or an AppRoute for all the other routes
- Controller: The business logic of the view. Mainly responsible to map the service/model with the view state.
- Service: Responsible to fetch the model date and update it.
Theme, icons and splash screen
Theme
ShipFlutter uses MaterialTheme with M3. By default we use the colors that you defined in the builders.
You can modify them and add on to the theme, by modifying the app_theme.dart
file.
To use the theme in the app you can access it via the context
extension:
To control light/dark mode we use the settingsController
singleton. It has a signal themeMode
that
holds the current theme mode. If changed, it will store it in the user preferences and automatically
change the mode in the MainApp
widget in app.dart
.
App icon and splash screen
ShipFlutter uses the flutter_launcher_icons and flutter_native_splash packages to generate the app icon and the splash screen for Android, iOS and Web targets.
Generate app icon
To generate a new icon follow these steps:
-
Create a base icon with at least 512x512 pixels.
-
Replace the icon in the
assets/images/icon-512.png
. -
Then run the following command:
Generate splash screen
To generate a new splash screen for Android and iOS follow these steps:
-
Modify
flutter_native_splash
configuration inpubspecs.yaml
as you wish (e.g. change the main image). -
Then run the following command:
For web, the splash screen is done manually with a smooth pulse animation using css and js.
If you want to modify it, you will need to change the index.html
and the flutter_bootstrap.js
files.
State Management
The ShipFlutter project uses Signals together with context_watch for state management.
Signals are quite powerful but at the same time simple to use. The project only uses the basic
functionality to make it easier to understand. Here is an example of the details_controller.dart
The state
variable holds the signal with the Future returned by the service. When the signal is observed
it will the fetch the data and update the state. It will initially return an AsyncLoading
state, once
the value is available it will return an AsyncData
state. In case of error it will return an AsyncError
state.
The details_view
then uses the state
to react to state changes and display the view.
Similar pattern with some variations is applied across the codebase. For more information on Signals check the officials docs.
Dependency Injection
Instead of using complex libraries, we use Lite Ref, a lightweight dependency injection library for Dart and Flutter with a scope approach that makes it easy to use but still testable and flexible.
The basics are simple. There are scoped
and singleton
instances.
Scoped instances
Used for controllers and context
bound instances, that should be disposed when the widget/context
is destroyed/removed. The usage is simple. First, declare it as a top-level variable:
Then, get the insitance from withing any widget or context
:
lite_ref also provides a disposable mechanism, that would call dispose
to any class that implements Disposable
.
Singleton instances
Used for services and classes that needs to be used across the different app screens. The usage is similar to scoped instances. First, declare it as a top-level variable:
Then access it from everywhere of your app with:
You can also create asynSingleton
instances to ensure the class has been initialized before
using it. Quite useful for libraries that requires some sort of async initialization.
Localization
An important bit of any app is localization. ShipFlutter setup allows you to easily localize the app in any language by using Slang and Slang-GPT.
The concept is simple, you can define your project localization using JSON and use code generation to do the wiring.
Directoryapp
Directorylib
Directoryi18n
- strings.i18n.json Default localization definition
- strings_es.i18n.json Language specific localization
- …
- translations.g.dart Generated localization file
Adding or updating values
- Add or update a key/value pair to
strings.i18n.json
- Run the following command:
- Use the new key
Adding a new locale
To add a new locale, you can simply duplicate the default strings.i18n.json
and rename it to strings_LOCALE.i18n.json
.
Then, manually translate the values or use the Slang-GPT to generate it for you.
- Configure Slang-GPT in
app/slang.yaml
. - Provide a description/context for your app
- Run the following command:
Read further configuration options at Slang-GPT
Code Generation
A powerful tool that allows you to generate code and speed up the development process is build_runner
.
ShipFlutter uses it to generate environment variables (envied)
and data classes (freezed).
To run the code generator, execute the following command:
This will automatically generate .freezed.dart
and g.dart
files for you. Those files contains
all the boilerplate code for getters/setters, equals, hashcode, copyWith, JSON serialization, etc…
Everytime you modify a file that contains a part
statement, make sure to run the builder.
Alternatively you can run it in watch mode (but sometimes is problematic)
Navigation
A crucial component of any app is navigation. ShipFlutter uses go_router to manage the navigation between screens.
The initial setup is slighlty complex, but ShipFlutter has already done it and provides a simple interface to customize it.
Create a route
A route should be associated with a view/screen. ShipFlutter abstracts the concept with the AppRoute
class.
-
Create a new class that implements
AppRoute
-
Include the route when creating the router in
app.dart
-
Navigate to a route with the
go_router
interface. For example:
You can also, define a route within a route, by manually invoking the createRoute
method:
In such case, the route path is a combination of the parent route and the child route.
Create a Navigation route
A navigation route is an abstraction done with the NavigationRoute
class used for routes that are
part of a navigation bar (either bottom or rail). The main difference with the AppRoute
class is
that it adds a new createDestination
method that defines the icon and label used in the navigation UI.
ShipFlutter handles the logic to show a bottom, rail or top navigation bar for you. In addition, you can
also define main/details screens that split the screen in two when there is enough space by using the AppScaffoldContent
class.
Check the home_route.dart
and details_route.dart
files for an example.