Skip to content

A modern Flutter + Supabase boilerplate designed to help developers kickstart production-ready mobile apps with a clean architecture, modular structure, and built-in authentication flow.

Notifications You must be signed in to change notification settings

prodevcom/flutter_supabase_boilerplate

Repository files navigation

Flutter Supabase Boilerplate

A production-ready Flutter boilerplate with Supabase integration, dependency injection, routing, and localization.

🚀 Tech Stack & Versions

Core

  • Flutter SDK: ^3.9.2
  • Dart: ^3.9.2

Main Dependencies

  • 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

Development

  • Build Runner: ^2.4.15
  • Injectable Generator: ^2.8.1
  • Auto Route Generator: ^10.2.3
  • Intl Utils: ^2.8.10

📦 Installation

  1. Clone the repository:
git clone [email protected]:prodevcom/flutter_supabase_boilerplate.git
cd flutter_supa_boilerplate
  1. Install dependencies:
flutter pub get
  1. Create a .env file in the root directory:
SUPABASE_URL=your_supabase_url
SUPABASE_ANON_KEY=your_supabase_anon_key
  1. Generate code (routes, dependency injection):
flutter pub run build_runner build --delete-conflicting-outputs
  1. Generate localization files:
flutter pub run intl_utils:generate

🏗️ Project Structure

lib/
├── 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

📝 Creating a New Module

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

Example: Creating a profile module

  1. Create the module directory structure:
mkdir -p lib/modules/profile/controller
mkdir -p lib/modules/profile/pages
mkdir -p lib/modules/profile/widgets
  1. Create the controller (see section below)

  2. Create the page (see section below)

  3. 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
  1. Add to main module export (lib/modules/module.dart):
export 'profile/profile.dart';

🎮 Creating a Controller

Controllers handle business logic and state management for a page.

Steps:

  1. Create the controller file: lib/modules/your_module/controller/your_module_controller.dart

  2. Extend BaseController and 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();
  }
}
  1. Key points:

    • Always extend BaseController
    • Use @injectable annotation for dependency injection
    • Use .obs..disposeBy(this) for reactive variables to auto-dispose
    • Use startLoading() and stopLoading() for loading states
    • Use logger for logging (already available from BaseController)
  2. Regenerate DI code:

flutter pub run build_runner build --delete-conflicting-outputs

📄 Creating a Page

Pages are the UI layer that display your content.

Steps:

  1. Create the page file: lib/modules/your_module/pages/your_module_page.dart

  2. 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'),
                ),
              ],
            ),
          ),
      ),
    );
  }
}
  1. Key points:

    • Always use @RoutePage() annotation for auto_route
    • Define routeName as a static const
    • Extend BasePage<YourPage, YourController>
    • Access controller via controller (automatically injected)
    • Access localization via localization
    • Use Obx() for reactive UI updates
    • Use ScreenUtil for responsive sizing (.w, .h, .sp)
  2. 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,
  ),
];
  1. Regenerate routes:
flutter pub run build_runner build --delete-conflicting-outputs

💉 Dependency Injection

This project uses get_it and injectable for dependency injection.

Setup

  1. Annotate your class:

    • @injectable - Factory (new instance each time)
    • @singleton - Singleton (one instance for the app)
    • @lazySingleton - Lazy singleton (created on first access)
  2. Inject dependencies:

@injectable
class YourService {
  final IAuthService authService;
  
  YourService(this.authService); // Automatically injected
}
  1. Use in controllers:
@injectable
class YourController extends BaseController {
  final YourService service;
  
  YourController(this.service); // Automatically injected
  
  void someMethod() {
    service.doSomething();
  }
}
  1. Access via GetIt (if needed):
import '../../../di/di.dart';

final service = getIt<YourService>();
  1. Regenerate DI code:
flutter pub run build_runner build --delete-conflicting-outputs

DI Configuration

The DI setup is in lib/di/di_entry_point.dart. The generated code is in lib/di/di_entry_point.config.dart.

🌍 Localization (Translations)

This project uses intl and intl_utils for localization.

Setup

  1. 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"
  }
}
  1. Use in code:
// In pages
Text(localization.yourModuleTitle)

// In controllers
String message = localization.formButtonSave;
  1. Generate localization files:
flutter pub run intl_utils:generate
  1. 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"
}
  1. Configure in pubspec.yaml:
flutter_intl:
  main_location: pt_BR
  enabled: true

Best Practices

  • Use descriptive keys: login_email_hint instead of email_hint
  • Add descriptions with @ prefix for documentation
  • Keep translations organized by feature/module
  • Always regenerate after adding new translations

🛠️ Development Commands

# 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

📚 Key Concepts

BaseController

  • Provides lifecycle methods (onInit, onPostFrame, dispose)
  • Manages loading states
  • Auto-disposes reactive variables
  • Provides context access

BasePage

  • Automatically injects controller
  • Manages controller lifecycle
  • Provides localization access
  • Handles context updates from controller

Guards

  • AuthGuard: Protects routes requiring authentication
  • Located in lib/core/guards/

Services

  • Business logic layer
  • Use interfaces for abstraction
  • Annotate with @singleton or @injectable

🔒 Security

  • Never commit .env files (already in .gitignore)
  • Use environment variables for sensitive data
  • Keep Supabase keys secure
  • Use guards for route protection

📄 License

This project is open source and available under the MIT License.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

A modern Flutter + Supabase boilerplate designed to help developers kickstart production-ready mobile apps with a clean architecture, modular structure, and built-in authentication flow.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages