Skip to content

Request Lifecycle

View raw .md

Understanding the Magic request lifecycle helps you know exactly where your service providers, middleware, and controllers run relative to each bootstrap step.

On this page

Introduction#

Understanding the Magic request lifecycle will help you build better applications. This document covers how a Magic application starts up, handles navigation requests, and renders views.

Lifecycle Overview#

text
┌─────────────────────────────────────────────────────────┐
│                    main.dart                            │
│                       │                                 │
│                  Magic.init()                           │
│                       │                                 │
│                  Env.load()                             │
│                       │                                 │
│               configFactories evaluated                 │
│                       │                                 │
│             URL strategy applied (web)                  │
│                       │                                 │
│      ┌────────────────┴────────────────┐                │
│      ▼                                 ▼                │
│  providers register() (sync)    (all providers)        │
│                       │                                 │
│      providers boot() (async, all registered)          │
│                       │                                 │
│              router pre-build                           │
│                       │                                 │
│                 runApp(MagicApplication)                │
│                       │                                 │
│              Route Matched (GoRouter)                   │
│                       │                                 │
│         ┌─────────────┼─────────────┐                   │
│         ▼             ▼             ▼                   │
│   Run Middleware   Resolve Layout   Get Controller      │
│                                                         │
│                       │                                 │
│              Controller.method()                        │
│                       │                                 │
│                 Render View                             │
└─────────────────────────────────────────────────────────┘

Application Bootstrap#

The application starts in main.dart:

dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 1. Initialize Magic
  await Magic.init(
    envFileName: '.env',
    configFactories: [
      () => appConfig,
      () => networkConfig,
      () => authConfig,
    ],
  );
  
  // 2. Restore authentication (optional)
  await Auth.restore();
  
  // 3. Run migrations (development)
  if (kDebugMode) {
    await Migrator().run([...migrations]);
  }
  
  // 4. Start the application
  runApp(MagicApplication(
    title: 'My App',
    initialRoute: Auth.check() ? '/dashboard' : '/login',
  ));
}

Magic.init() Steps#

  1. Load Environment - Reads .env file into memory via Env.load()
  2. Evaluate Config Factories - Runs all configFactories now that Env is populated
  3. Apply URL Strategy - Configures path vs hash URL strategy for web (before any widget tree exists)
  4. Register Service Providers - Calls register() on each provider synchronously
  5. Boot Service Providers - Calls boot() on each provider asynchronously
  6. Pre-build Router - Resolves MagicRouter.instance.routerConfig so GoRouter is ready before runApp

Service Providers#

Providers are registered in config/app.dart:

dart
'providers': [
  (app) => NetworkServiceProvider(app),
  (app) => AuthServiceProvider(app),
  (app) => DatabaseServiceProvider(app),
  (app) => CacheServiceProvider(app),
  (app) => LocalizationServiceProvider(app),
  (app) => EventServiceProvider(app),
  (app) => AppServiceProvider(app),
],

Provider Lifecycle#

  1. register() - Bind services to container (no dependencies)
  2. boot() - Initialize services (can use other services)
dart
class AppServiceProvider extends ServiceProvider {
  @override
  void register() {
    // Runs first - just bind services
    app.bind('api', () => ApiService());
  }

  @override
  Future<void> boot() async {
    // Runs after ALL providers register
    // Safe to use other services here
    Gate.before((user, ability) {
      if ((user as User).isAdmin) return true;
      return null;
    });
  }
}

Registering a provider after boot#

A provider registered once the app has already booted is booted on the spot, rather than waiting for a boot phase that has been and gone. register() returns a Future so a caller that needs the provider wired before its next line can wait for it:

dart
// A plugin installing itself lazily, after the app is running.
await Magic.app.register(NotificationsServiceProvider(Magic.app));
Notify.startPolling(); // the provider has finished booting

Ignoring the future is fine when you do not need that guarantee. Before the boot phase it completes immediately, since nothing asynchronous has happened yet.

Registering the same provider instance twice is a no-op the second time, so its register() and boot() each run once. Two different instances of the same provider class both run, which is deliberate: a provider parameterised per plugin is a legitimate shape.

Routing#

Routes are defined in lib/routes/:

dart
// lib/routes/web.dart
void registerRoutes() {
  // Guest routes
  MagicRoute.group(
    layoutId: 'guest',
    layout: (child) => GuestLayout(child: child),
    routes: () {
      MagicRoute.page('/login', () => AuthController.instance.login());
      MagicRoute.page('/register', () => AuthController.instance.register());
    },
  );

  // Authenticated routes
  MagicRoute.group(
    layoutId: 'app',
    middleware: ['auth'],
    layout: (child) => AppLayout(child: child),
    routes: () {
      MagicRoute.page('/dashboard', () => DashboardView());
      MagicRoute.page('/settings', () => SettingsView());
    },
  );
}

Each group with a layout: is its own go_router ShellRoute, so the guest and authenticated groups are separate shells. Groups that share a layoutId share one persistent shell, and shells are emitted in registration order. See Layouts.

Route Resolution#

  1. URL is matched against defined routes
  2. Layout is wrapped around the view
  3. Middleware is executed in order
  4. Controller/View is resolved

Middleware#

Middleware intercepts navigation before the view renders:

dart
class EnsureAuthenticated extends MagicMiddleware {
  @override
  Future<void> handle(void Function() next) async {
    if (Auth.check()) {
      next();  // Allow navigation
    } else {
      MagicRoute.to('/login');  // Redirect
    }
  }
}

Middleware Execution Order#

  1. Global middleware (registered in provider)
  2. Route group middleware
  3. Route-specific middleware

Controller Dispatch#

Controllers are resolved using the findOrPut pattern:

dart
// Route definition
MagicRoute.page('/tasks', () => TaskController.instance.index());

// Controller
class TaskController extends MagicController {
  static TaskController get instance => Magic.findOrPut(TaskController.new);
  
  Widget index() {
    if (isEmpty) _loadTasks();
    return const TaskListView();
  }
}

Controller Lifecycle#

  1. findOrPut - Get existing or create new controller
  2. onInit() - Called when controller is created
  3. Method execution - Returns view widget
  4. State updates - notifyListeners() triggers rebuilds
  5. onClose() - Called when controller is disposed

View Rendering#

Views render using controller state:

dart
class TaskListView extends MagicView {
  const TaskListView({super.key});

  @override
  Widget build(BuildContext context) {
    final controller = TaskController.instance;
    
    return controller.renderState(
      (tasks) => _buildList(tasks),
      onLoading: CircularProgressIndicator(),
      onError: (msg) => ErrorWidget(message: msg),
      onEmpty: EmptyState(),
    );
  }
}

State Flow#

  1. Controller calls setLoading() → View shows loading
  2. Controller calls setSuccess(data) → View renders data
  3. Controller calls setError(msg) → View shows error
  4. Controller calls notifyListeners() → View rebuilds
esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.