search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

Request Lifecycle

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

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

┌─────────────────────────────────────────────────────────┐
│                    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:

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:

'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)
class AppServiceProvider extends ServiceProvider {
  @override
  void register() {
    // Runs first - just bind services
    app.bind('api', () => ApiService());
  }

  @override
  Future 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;
    });
  }
}

Routing

Routes are defined in lib/routes/:

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

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

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:

class EnsureAuthenticated extends MagicMiddleware {
  @override
  Future 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:

// 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:

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

[!TIP] Understanding this lifecycle helps you place logic in the right location: configuration in providers, authorization in middleware, business logic in controllers, and UI in views.