flutter
/

GetX Multi‑Module Project: Scaling Flutter Apps with Feature Packages

Last Sync: Today

On this page

14
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

GetX Multi‑Module Project: Scaling Flutter Apps with Feature Packages

Introduction

As your Flutter app grows, a single codebase can become hard to maintain, test, and scale across multiple teams. A multi‑module project splits the app into independent feature modules (packages) that communicate through well‑defined interfaces. GetX’s dependency injection, navigation, and modular design make it an excellent choice for building such architectures. This guide shows you how to structure a Flutter app with multiple GetX modules, manage dependencies, and keep the codebase clean.

What is a Multi‑Module Project?

A multi‑module project (often called a monorepo) organizes your app’s code into separate packages (or modules) that each represent a feature (e.g., authentication, products, cart). These modules can be developed, tested, and versioned independently, while still being assembled into a final app. GetX facilitates this by allowing each module to define its own routes, bindings, and controllers, and to communicate through shared services.

Benefits

  • Team Scalability – Different teams can work on different modules without merge conflicts.
  • Code Reusability – Modules can be reused across multiple apps (e.g., a login module used in both customer and admin apps).
  • Faster Builds – Changes in one module don’t force recompilation of the entire app if using proper dependency management.
  • Isolated Testing – Each module can be unit‑tested independently.
  • Clear Boundaries – Enforces separation of concerns and prevents spaghetti code.
  • Lazy Loading – Modules can be loaded only when needed, reducing startup time.

Recommended Project Structure (Monorepo)

TEXTRead-only
1
my_flutter_app/                 # Root folder
├── packages/                    # All modules (packages)
│   ├── core/                    # Shared code (themes, utils, base classes)
│   │   ├── lib/
│   │   │   ├── core.dart        # Exports all core functionality
│   │   │   ├── services/
│   │   │   ├── themes/
│   │   │   ├── utils/
│   │   │   └── widgets/
│   │   └── pubspec.yaml
│   ├── auth/                    # Authentication module
│   │   ├── lib/
│   │   │   ├── auth.dart
│   │   │   ├── bindings/
│   │   │   ├── controllers/
│   │   │   ├── views/
│   │   │   ├── models/
│   │   │   └── repositories/
│   │   └── pubspec.yaml
│   ├── products/                # Products module
│   │   ├── lib/
│   │   │   ├── products.dart
│   │   │   └── ...
│   │   └── pubspec.yaml
│   └── cart/                    # Cart module
│       ├── lib/
│       └── pubspec.yaml
├── app/                         # The main app (or app entry point)
│   ├── lib/
│   │   ├── main.dart
│   │   ├── app_binding.dart
│   │   └── app_pages.dart
│   └── pubspec.yaml
└── pubspec.yaml                 # Root pubspec (for workspace management)

Setting Up the Workspace

Create a root folder and initialize a Flutter project (optional). Then add a packages/ folder. Each module is a Dart package. Use relative dependencies in the root pubspec.yaml to reference local packages.

YAMLRead-only
1
# Root pubspec.yaml (workspace)
name: my_flutter_app
version: 1.0.0

environment:
  sdk: ">=3.0.0 <4.0.0"

dependencies:
  flutter:
    sdk: flutter
  get: ^4.6.6
  core:
    path: packages/core
  auth:
    path: packages/auth
  products:
    path: packages/products
  cart:
    path: packages/cart

# ...

Each module’s pubspec.yaml should declare its own dependencies, including get and any other modules it uses (e.g., core). This ensures modules are self‑contained.

Building a Module (Example: Auth)

DARTRead-only
1
// packages/auth/lib/auth.dart
library auth;

export 'bindings/auth_binding.dart';
export 'controllers/auth_controller.dart';
export 'views/login_page.dart';
export 'views/register_page.dart';
export 'models/user_model.dart';
// Optionally, export a service if needed
DARTRead-only
1
// packages/auth/lib/bindings/auth_binding.dart
import 'package:get/get.dart';
import '../controllers/auth_controller.dart';
import '../repositories/auth_repository.dart';

class AuthBinding extends Bindings {
  @override
  void dependencies() {
    // Register the repository and controller
    Get.lazyPut<AuthRepository>(() => AuthRepository());
    Get.lazyPut<AuthController>(() => AuthController(Get.find()));
  }
}
DARTRead-only
1
// packages/auth/lib/controllers/auth_controller.dart
import 'package:get/get.dart';
import 'package:core/core.dart'; // shared services
import '../repositories/auth_repository.dart';

class AuthController extends GetxController {
  final AuthRepository repository;
  AuthController(this.repository);

  var isLoading = false.obs;
  var user = Rxn<User>();

  Future<void> login(String email, String password) async {
    isLoading.value = true;
    try {
      final userData = await repository.login(email, password);
      user.value = userData;
      // Notify other modules via a shared service
      Get.find<AuthService>().setUser(userData);
      Get.offAllNamed('/home');
    } finally {
      isLoading.value = false;
    }
  }
}
DARTRead-only
1
// packages/auth/lib/views/login_page.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../bindings/auth_binding.dart';
import '../controllers/auth_controller.dart';

class LoginPage extends GetView<AuthController> {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Obx(() => Center(
        child: ElevatedButton(
          onPressed: controller.isLoading.value ? null : () => controller.login('test@test.com', 'pass'),
          child: controller.isLoading.value ? CircularProgressIndicator() : Text('Login'),
        ),
      )),
    );
  }
}

Core Module: Shared Code

The core module contains shared code used by other modules: base classes, utilities, themes, and global services. It should have no knowledge of specific features.

DARTRead-only
1
// packages/core/lib/services/auth_service.dart
import 'package:get/get.dart';

class AuthService extends GetxService {
  var user = Rxn<User>();
  var isLoggedIn = false.obs;

  void setUser(User? user) {
    this.user.value = user;
    isLoggedIn.value = user != null;
  }
}

// packages/core/lib/core.dart
export 'services/auth_service.dart';
export 'themes/app_theme.dart';
export 'widgets/loading_button.dart';
// ...

Integrating Modules in the Main App

The main app (or the app entry point) imports all modules and defines the global routing table. It registers the core services in an initial binding.

DARTRead-only
1
// app/lib/main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:core/core.dart';
import 'package:auth/auth.dart';
import 'package:products/products.dart';
import 'package:cart/cart.dart';

void main() async {
  await GetStorage.init();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Multi-Module Demo',
      initialBinding: AppBinding(),
      initialRoute: '/',
      getPages: [
        GetPage(
          name: '/',
          page: () => LoginPage(),
          binding: AuthBinding(),
        ),
        GetPage(
          name: '/home',
          page: () => HomePage(),
          binding: HomeBinding(),
        ),
        GetPage(
          name: '/products',
          page: () => ProductListPage(),
          binding: ProductBinding(),
        ),
        GetPage(
          name: '/cart',
          page: () => CartPage(),
          binding: CartBinding(),
        ),
      ],
    );
  }
}

class AppBinding extends Bindings {
  @override
  void dependencies() {
    Get.put(AuthService(), permanent: true);
    // any other global dependencies
  }
}

Inter‑Module Communication

Modules should not directly import each other. Instead, they communicate through shared services located in the core module. For example, the AuthService holds the logged‑in user state; the Products module can listen to it to know whether to show user‑specific content.

DARTRead-only
1
// In products module controller
class ProductController extends GetxController {
  final AuthService auth = Get.find();

  var products = <Product>[].obs;

  @override
  void onInit() {
    super.onInit();
    ever(auth.isLoggedIn, (_) => fetchProducts());
    fetchProducts();
  }

  Future<void> fetchProducts() async {
    if (!auth.isLoggedIn.value) return;
    // fetch products using the user's token
  }
}

Dependency Management

Use version constraints in each module’s pubspec.yaml to ensure compatibility. The root pubspec.yaml references local paths during development, but for production you can publish modules to a private pub repository or use git dependencies.

YAMLRead-only
1
# packages/products/pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  get: ^4.6.6
  core:
    path: ../core    # relative path during development

Best Practices

  • Define clear module boundaries – Each module should represent a single business feature.
  • Use core for shared code – Avoid duplicating utilities, themes, and base classes.
  • Keep modules loosely coupled – Communicate only via core services, not by importing other feature modules directly.
  • Use Get.lazyPut inside bindings – Delay creation until the module’s route is accessed.
  • Version your modules – Even when using path dependencies, consider semantic versioning for production.
  • Test modules in isolation – Write unit tests inside each module; integration tests can go in the main app.
  • Use code generation for serialization – (e.g., json_serializable) inside each module.

Common Mistakes

  • ❌ Creating circular dependencies between modules – Module A imports Module B and vice versa. ✅ Use core services to break cycles.
  • ❌ Placing UI code inside core – Core should be framework‑agnostic as much as possible. ✅ Keep UI in feature modules.
  • ❌ Using Get.put inside widgets instead of bindings – Scatters registration and can lead to duplicate instances. ✅ Always use bindings for module dependencies.
  • ❌ Hardcoding routes in views – Makes refactoring hard. ✅ Use route constants from a shared file (or core).

FAQ

  • Q: Can I use different versions of GetX across modules?
    A: It’s best to use the same version to avoid conflicts. The root pubspec.yaml will resolve the version if you declare a single dependency there.
  • Q: How to handle navigation between modules?
    A: Use named routes (constants defined in core or in the main app) and Get.toNamed. Modules should not know the exact route names of other modules; they can use route constants from core.
  • Q: How do I share a theme between modules?
    A: Define the theme in the core module and use it in the main app’s GetMaterialApp. All modules will inherit it.
  • Q: What about state persistence across modules?
    A: Use GetStorage or a shared service that handles persistence. For example, AuthService could store/load the token from GetStorage.
  • Q: Can I lazy load modules?
    A: Yes, because each module’s binding uses Get.lazyPut, the controllers are only created when the route is visited. The module’s code is already imported, but the actual instances are lazy.

Conclusion

A multi‑module architecture with GetX gives you the scalability and maintainability needed for large Flutter projects. By separating features into independent packages, you enable parallel development, code reuse, and better testability. With GetX’s dependency injection and navigation, modules stay loosely coupled and can be assembled into a final app with minimal effort.

Test Your Knowledge

Q1
of 3

What is the primary benefit of splitting a Flutter app into multiple modules?

A
Smaller app size
B
Better team collaboration and code reuse
C
Faster execution
D
Automatic state management
Q2
of 3

How should modules communicate with each other in a GetX multi‑module setup?

A
By importing each other's packages
B
Through shared services in the core module
C
Using global variables
D
Through direct function calls
Q3
of 3

What is the recommended way to register dependencies inside a module?

A
Using Get.put inside views
B
Using a binding that uses Get.lazyPut
C
Using a static variable
D
Using initState of the page

Previous

getx feature modular architecture

Next

getx with firebase

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.