A production-ready Flutter boilerplate with Supabase integration, dependency injection, routing, and localization.
- Flutter SDK: ^3.9.2
- Dart: ^3.9.2
- Supabase: ^2.8.0
- Get_it: ^8.2.0
- Injectable: ^2.5.2
- Auto Route: ^10.1.2
- Get: ^4.7.2
- Flutter Form Builder: ^10.1.0
- Flutter Screenutil: ^5.9.3
- Google Fonts: ^6.3.1
- Build Runner: ^2.4.15
- Injectable Generator: ^2.8.1
- Auto Route Generator: ^10.2.3
- Intl Utils: ^2.8.10
- Clone the repository:
git clone [email protected]:prodevcom/flutter_supabase_boilerplate.git
cd flutter_supa_boilerplate- Install dependencies:
flutter pub get- Create a
.envfile in the root directory:
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_supabase_anon_key- Generate code (routes, dependency injection):
flutter pub run build_runner build --delete-conflicting-outputs- Generate localization files:
flutter pub run intl_utils:generatelib/
├── application.dart # App entry point
├── config/ # Configuration files
├── core/ # Core functionality (base classes, router, theme, guards)
├── data/ # Data models and DTOs
├── di/ # Dependency injection setup
├── l10n/ # Localization files
├── modules/ # Feature modules
│ ├── home/
│ ├── login/
│ ├── registration/
│ └── welcome/
├── services/ # Business logic services
└── widgets/ # Reusable widgets
A module represents a feature in your application. Follow this structure:
lib/modules/
└── your_module/
├── your_module.dart # Module exports
├── controller/
│ ├── controller.dart # Controller exports
│ └── your_module_controller.dart
├── pages/
│ └── your_module_page.dart
└── widgets/ # Optional: module-specific widgets
└── widgets.dart
- Create the module directory structure:
mkdir -p lib/modules/profile/controller
mkdir -p lib/modules/profile/pages
mkdir -p lib/modules/profile/widgets-
Create the controller (see section below)
-
Create the page (see section below)
-
Create module export files:
lib/modules/profile/profile.dart:
export 'controller/controller.dart';
export 'pages/profile_page.dart';lib/modules/profile/controller/controller.dart:
export 'profile_controller.dart';lib/modules/profile/widgets/widgets.dart:
// Export your widgets here- Add to main module export (
lib/modules/module.dart):
export 'profile/profile.dart';Controllers handle business logic and state management for a page.
-
Create the controller file:
lib/modules/your_module/controller/your_module_controller.dart -
Extend
BaseControllerand annotate with@injectable:
import 'package:injectable/injectable.dart';
import '../../../core/core.dart';
import '../../../services/services.dart';
@injectable
class YourModuleController extends BaseController {
// Your reactive variables
final RxString name = ''.obs..disposeBy(this);
final RxBool isLoading = false.obs..disposeBy(this);
// Lifecycle methods
@override
void onInit() {
super.onInit();
// Initialize your controller here
loadData();
}
// Your business logic methods
Future<void> loadData() async {
startLoading();
try {
// Your logic here
// Example: await someService.getData();
} catch (e) {
logger.e('Error loading data: $e');
} finally {
stopLoading();
}
}
void someAction() {
// Your action logic
name.value = 'New Name';
}
@override
void dispose() {
// Clean up if needed
super.dispose();
}
}-
Key points:
- Always extend
BaseController - Use
@injectableannotation for dependency injection - Use
.obs..disposeBy(this)for reactive variables to auto-dispose - Use
startLoading()andstopLoading()for loading states - Use
loggerfor logging (already available from BaseController)
- Always extend
-
Regenerate DI code:
flutter pub run build_runner build --delete-conflicting-outputsPages are the UI layer that display your content.
-
Create the page file:
lib/modules/your_module/pages/your_module_page.dart -
Create a StatefulWidget that extends
BasePage:
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/core.dart';
import '../controller/your_module_controller.dart';
@RoutePage()
class YourModulePage extends StatefulWidget {
static const String routeName = '/your-module';
const YourModulePage({super.key});
@override
State<YourModulePage> createState() => _YourModulePageState();
}
class _YourModulePageState extends BasePage<YourModulePage, YourModuleController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(localization.yourModuleTitle), // Use localization
),
body: Obx(() => controller.isLoading.value
? const Center(child: CircularProgressIndicator())
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(controller.name.value),
ElevatedButton(
onPressed: controller.someAction,
child: const Text('Action'),
),
],
),
),
),
);
}
}-
Key points:
- Always use
@RoutePage()annotation for auto_route - Define
routeNameas a static const - Extend
BasePage<YourPage, YourController> - Access controller via
controller(automatically injected) - Access localization via
localization - Use
Obx()for reactive UI updates - Use
ScreenUtilfor responsive sizing (.w,.h,.sp)
- Always use
-
Add route to router (
lib/core/router/router.dart):
// For public routes:
List<AutoRoute> get publicRoutes => [
// ... existing routes
AutoRoute(path: YourModulePage.routeName, page: YourModuleRoute.page),
];
// For private routes (requires authentication):
List<AutoRoute> get privateRoutes => [
// ... existing routes
AutoRoute(
guards: defaultGuards,
path: YourModulePage.routeName,
page: YourModuleRoute.page,
),
];- Regenerate routes:
flutter pub run build_runner build --delete-conflicting-outputsThis project uses get_it and injectable for dependency injection.
-
Annotate your class:
@injectable- Factory (new instance each time)@singleton- Singleton (one instance for the app)@lazySingleton- Lazy singleton (created on first access)
-
Inject dependencies:
@injectable
class YourService {
final IAuthService authService;
YourService(this.authService); // Automatically injected
}- Use in controllers:
@injectable
class YourController extends BaseController {
final YourService service;
YourController(this.service); // Automatically injected
void someMethod() {
service.doSomething();
}
}- Access via GetIt (if needed):
import '../../../di/di.dart';
final service = getIt<YourService>();- Regenerate DI code:
flutter pub run build_runner build --delete-conflicting-outputsThe DI setup is in lib/di/di_entry_point.dart. The generated code is in lib/di/di_entry_point.config.dart.
This project uses intl and intl_utils for localization.
- Add translations to
lib/l10n/intl_en.arb:
{
"@@locale": "en",
"your_module_title": "Your Module",
"your_module_description": "This is a description",
"form_button_save": "Save",
"@form_button_save": {
"description": "Save button text"
}
}- Use in code:
// In pages
Text(localization.yourModuleTitle)
// In controllers
String message = localization.formButtonSave;- Generate localization files:
flutter pub run intl_utils:generate- Add new locale (e.g., Portuguese):
Create lib/l10n/intl_pt.arb:
{
"@@locale": "pt",
"your_module_title": "Seu Módulo",
"your_module_description": "Esta é uma descrição",
"form_button_save": "Salvar"
}- Configure in
pubspec.yaml:
flutter_intl:
main_location: pt_BR
enabled: true- Use descriptive keys:
login_email_hintinstead ofemail_hint - Add descriptions with
@prefix for documentation - Keep translations organized by feature/module
- Always regenerate after adding new translations
# Install dependencies
flutter pub get
# Generate code (routes, DI, serialization)
flutter pub run build_runner build --delete-conflicting-outputs
# Watch mode for code generation (recommended during development)
flutter pub run build_runner watch --delete-conflicting-outputs
# Generate localization files
flutter pub run intl_utils:generate
# Run the app
flutter run
# Run tests
flutter test
# Build for production
flutter build apk --release # Android
flutter build ios --release # iOS- Provides lifecycle methods (
onInit,onPostFrame,dispose) - Manages loading states
- Auto-disposes reactive variables
- Provides context access
- Automatically injects controller
- Manages controller lifecycle
- Provides localization access
- Handles context updates from controller
AuthGuard: Protects routes requiring authentication- Located in
lib/core/guards/
- Business logic layer
- Use interfaces for abstraction
- Annotate with
@singletonor@injectable
- Never commit
.envfiles (already in.gitignore) - Use environment variables for sensitive data
- Keep Supabase keys secure
- Use guards for route protection
This project is open source and available under the MIT License.
Contributions are welcome! Please feel free to submit a Pull Request.