magic 1.0.0-alpha.2 copy "magic: ^1.0.0-alpha.2" to clipboard
magic: ^1.0.0-alpha.2 copied to clipboard

A Laravel-inspired Flutter framework with Eloquent ORM, routing, and MVC architecture.

[Magic Logo]

Magic

The Laravel Experience for Flutter.
Build production-ready Flutter apps with Facades, Eloquent ORM, Service Providers, and IoC Container — zero boilerplate.

pub package CI License: MIT pub points GitHub stars

Documentation · pub.dev · Issues


Alpha Release — Magic is under active development. APIs may change before stable. Star the repo to follow progress.

Why Magic? #

Flutter gives you widgets, but not architecture. Building a real app means wiring up HTTP clients, auth flows, caching, validation, routing, and state management — all from scratch, every time.

Magic fixes this. If you know Laravel, you already know Magic:

// Before — the Flutter way
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
final response = await dio.get('/users/1');
final user = User.fromJson(response.data['data']);
// ...manually handle tokens, errors, caching, state...

// After — the Magic way
final user = await User.find(1);

Features #

Feature Description
🏗️ IoC Container Service Container with singleton, bind, and instance registration
🎭 16 Facades Auth, Http, Cache, DB, Event, Gate, Log, Route, Lang, Storage, Vault, Crypt and more
🗄️ Eloquent ORM Models, QueryBuilder, migrations, seeders, factories — hybrid API + SQLite persistence
🛣️ Routing GoRouter integration with middleware, named routes, and context-free navigation
🔐 Authentication Token-based auth with guards (Bearer, BasicAuth, ApiKey), session restore, auto-refresh
🛡️ Authorization Gates, policies, MagicCan / MagicCannot widgets
Validation Laravel-style rules: Required, Email, Min, Max, In, Confirmed
📡 Events Pub/sub event system with MagicEvent and MagicListener
💾 Caching Memory and file drivers with TTL and remember()
🌍 Localization JSON-based i18n with :attribute placeholders
🎨 Wind UI Built-in Tailwind CSS-like styling with className syntax
🧰 Magic CLI Artisan-style code generation: magic make:model, magic make:controller

Quick Start #

1. Add Magic to Your Project #

# Create a new Flutter project (or use an existing one)
flutter create my_app
cd my_app

# Add Magic as a dependency
flutter pub add magic

2. Scaffold with Magic CLI #

Magic CLI is bundled with the package — no global install needed:

# Initialize Magic with all features
dart run magic:magic install

# Or exclude specific features
dart run magic:magic install --without-database --without-auth

The CLI sets up everything: directory structure, config files, service providers, environment files, and bootstraps main.dart.

Tip

For convenience, you can also activate the CLI globally: dart pub global activate magic_cli, then use magic install directly.

3. Build #

class UserController extends MagicController with MagicStateMixin<List<User>> {
  static UserController get instance => Magic.findOrPut(UserController.new);

  Future<void> fetchUsers() async {
    setLoading();
    final response = await Http.get('/users');
    response.successful
        ? setSuccess(response.data['data'].map((e) => User.fromMap(e)).toList())
        : setError(response.firstError ?? 'Failed to load');
  }

  Widget index() => renderState(
    (users) => ListView.builder(
      itemCount: users.length,
      itemBuilder: (_, i) => ListTile(title: Text(users[i].name ?? '')),
    ),
    onLoading: const CircularProgressIndicator(),
    onError: (msg) => Text('Error: $msg'),
  );
}

Facades #

Auth #

// Login with token and user model
final response = await Http.post('/login', data: credentials);
final user = User.fromMap(response['data']['user']);
await Auth.login({'token': response['data']['token']}, user);

// Check authentication
if (Auth.check()) {
  final user = Auth.user<User>();
}

// Restore session on app start
await Auth.restore();

// Logout
await Auth.logout();

HTTP Client #

// Standard requests
final response = await Http.get('/users', query: {'page': 1});
await Http.post('/users', data: {'name': 'John', 'email': 'john@example.com'});
await Http.put('/users/1', data: {'name': 'Jane'});
await Http.delete('/users/1');

// RESTful resource helpers
final users = await Http.index('users', filters: {'role': 'admin'});
final user = await Http.show('users', '1');
await Http.store('users', {'name': 'John'});
await Http.update('users', '1', {'name': 'Jane'});
await Http.destroy('users', '1');

// File upload
await Http.upload('/avatar', data: {'user_id': '1'}, files: {'photo': file});

// Response API
response.successful   // 200-299
response.failed       // >= 400
response.data         // Map<String, dynamic>
response['key']       // Shorthand access
response.errors       // Laravel validation errors (422)
response.firstError   // First error message

Routing #

// Define routes
MagicRoute.page('/home', () => HomeController.instance.index());
MagicRoute.page('/users/:id', () => UserController.instance.show());

// Route groups with middleware
MagicRoute.group(
  prefix: '/admin',
  middleware: [AuthMiddleware()],
  routes: () {
    MagicRoute.page('/dashboard', () => AdminController.instance.index());
  },
);

// Navigate (context-free)
MagicRoute.to('/users');
MagicRoute.toNamed('user.show', params: {'id': '1'});
MagicRoute.back();
MagicRoute.replace('/login');

Cache #

// Store and retrieve
await Cache.put('key', 'value', ttl: Duration(minutes: 30));
final value = await Cache.get('key');

// Remember pattern — fetch once, cache for duration
final users = await Cache.remember<List<User>>(
  'all_users',
  Duration(minutes: 5),
  () => fetchUsersFromApi(),
);

// Check and forget
if (Cache.has('key')) {
  await Cache.forget('key');
}
await Cache.flush(); // Clear all

Eloquent ORM #

class User extends Model with HasTimestamps, InteractsWithPersistence {
  // Typed getters — always use get<T>()
  int? get id => get<int>('id');
  String? get name => get<String>('name');
  String? get email => get<String>('email');

  // Setters
  set name(String? v) => set('name', v);
  set email(String? v) => set('email', v);

  // Required overrides
  @override String get table => 'users';
  @override String get resource => 'users';
  @override List<String> get fillable => ['name', 'email'];

  // Static finders
  static Future<User?> find(dynamic id) =>
      InteractsWithPersistence.findById<User>(id, User.new);
  static Future<List<User>> all() =>
      InteractsWithPersistence.allModels<User>(User.new);
}

// CRUD — hybrid persistence (API + local SQLite)
final user = await User.find(1);     // SQLite first → API fallback → sync
final users = await User.all();
await user.save();                    // POST (create) or PUT (update)
await user.delete();                  // DELETE
await user.refresh();                 // Re-fetch from API

Validation #

final validator = Validator.make(
  {'email': email, 'password': password},
  {
    'email': [Required(), Email()],
    'password': [Required(), Min(8)],
  },
);

if (validator.fails()) {
  print(validator.errors()); // {'email': 'The email field is required.'}
}

// Or throw on failure
final data = validator.validate(); // Throws ValidationException if invalid

Authorization #

// Define abilities
Gate.define('update-post', (user, post) => user.id == post.userId);
Gate.before((user, ability) {
  if (user.isAdmin) return true;
  return null; // Fall through to specific check
});

// Check in code
if (Gate.allows('update-post', post)) {
  // Show edit button
}

// Check in widgets
MagicCan(
  ability: 'update-post',
  arguments: post,
  child: EditButton(),
)

Forms #

final form = MagicFormData({
  'name': user.name ?? '',
  'email': user.email ?? '',
});

// In widget tree
MagicForm(
  formData: form,
  child: Column(children: [
    WFormInput(label: 'Name', controller: form['name']),
    WFormInput(label: 'Email', controller: form['email']),
    WButton(
      onTap: () async {
        if (form.validate()) {
          await form.process(() => controller.updateProfile(form.data));
        }
      },
      isLoading: form.isProcessing,
      child: Text('Save'),
    ),
  ]),
)

Events #

// Define event
class UserRegistered extends MagicEvent {
  final User user;
  UserRegistered(this.user);
}

// Dispatch
await Event.dispatch(UserRegistered(user));

// Listen (register in ServiceProvider)
Event.listen<UserRegistered>(() => SendWelcomeEmail());

Service Providers #

class AppServiceProvider extends ServiceProvider {
  @override
  void register() {
    // Sync — bind to container
    app.singleton('payment', () => StripeService());
  }

  @override
  Future<void> boot() async {
    // Async — other services available
    Auth.registerModel<User>(User.fromMap);
  }
}

// Register in config
final appConfig = {
  'app': {
    'providers': [
      (app) => AppServiceProvider(app),
    ],
  },
};

Wind UI #

Magic includes Wind UI — a utility-first styling engine inspired by Tailwind CSS. Build UIs with className strings instead of nested widget trees.

WDiv(
  className: 'flex flex-col gap-4 p-6 bg-white dark:bg-gray-900 rounded-xl shadow-lg',
  children: [
    WText('Dashboard', className: 'text-2xl font-bold text-gray-900 dark:text-white'),
    WButton(
      onTap: _refresh,
      className: 'bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg',
      child: Text('Refresh'),
    ),
  ],
)

See Wind UI Documentation for the complete widget reference and utility class guide.

CLI Commands #

dart run magic:magic make:model User -mcf        # Model + migration + controller + factory
dart run magic:magic make:controller User         # Controller
dart run magic:magic make:view Login              # View class
dart run magic:magic make:migration create_users  # Migration
dart run magic:magic make:seeder UserSeeder       # Database seeder
dart run magic:magic make:policy Post             # Authorization policy
dart run magic:magic make:provider Payment        # Service provider
dart run magic:magic make:event OrderShipped      # Event class
dart run magic:magic make:listener SendEmail      # Event listener
dart run magic:magic make:middleware Auth          # Middleware
dart run magic:magic make:request StoreUser       # Form request
dart run magic:magic make:lang tr                 # Language file
dart run magic:magic make:enum Status             # Enum class

Tip

If you activated the CLI globally (dart pub global activate magic_cli), you can use the shorter magic <command> syntax instead.

Architecture #

Magic.init() → Env.load() → configFactories → providers register() → providers boot() → app ready
lib/
├── config/              # Configuration files (app, auth, cache, database)
├── app/
│   ├── controllers/     # Request handlers (MagicController)
│   ├── models/          # Eloquent models
│   └── policies/        # Authorization policies
├── database/
│   ├── migrations/      # Schema migrations
│   ├── seeders/         # Database seeders
│   └── factories/       # Model factories
├── resources/views/     # UI view classes
├── routes/              # Route definitions
└── main.dart            # Entry point

Documentation #

Full docs at magic.fluttersdk.com.

Topic
Installation Setup and requirements
Configuration Environment and config files
Service Providers Provider lifecycle
Routing Routes and navigation
Controllers Request handlers
Views UI layer
HTTP Client Network requests
Middleware Request pipeline
Forms Form handling and validation

AI Agent Integration #

Use Magic with AI coding assistants like Claude Code, Cursor, or GitHub Copilot. The magic-framework skill teaches your AI the correct patterns — Facades, Eloquent ORM, Service Providers, controllers, routing, and common anti-patterns — so it generates correct Magic code on the first try.

Setup instructions and skill files: fluttersdk/ai

Contributing #

git clone https://github.com/fluttersdk/magic.git
cd magic && flutter pub get
flutter test && dart analyze

Report a bug · Request a feature

License #

MIT — see LICENSE for details.


Built with care by FlutterSDK
If Magic saves you time, give it a star — it helps others discover it.