Request Lifecycle
- Introduction
- Lifecycle Overview
- Application Bootstrap
- Service Providers
- Routing
- Middleware
- Controller Dispatch
- View Rendering
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() │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ Load .env Register Configs Boot Providers │
│ │
│ │ │
│ 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
- Load Environment - Reads
.envfile into memory - Merge Configurations - Combines all config factories
- Register Service Providers - Calls
register()on each provider - Boot Service Providers - Calls
boot()on each provider
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
- register() - Bind services to container (no dependencies)
- 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
- URL is matched against defined routes
- Layout is wrapped around the view
- Middleware is executed in order
- 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
- Global middleware (registered in provider)
- Route group middleware
- 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
- findOrPut - Get existing or create new controller
- onInit() - Called when controller is created
- Method execution - Returns view widget
- State updates -
notifyListeners()triggers rebuilds - 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
- Controller calls
setLoading()→ View shows loading - Controller calls
setSuccess(data)→ View renders data - Controller calls
setError(msg)→ View shows error - 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.