search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

Views

Introduction

Magic Views provide a structured way to separate your UI from your business logic, following the MVC pattern familiar to Laravel developers. Instead of standard Flutter widgets, Magic views extend MagicView or MagicStatefulView.

Why Use MagicView?

  • Auto-Injection: The controller is automatically available via controller property
  • Type Safety: The controller is fully typed (MagicView)
  • Consistency: Matches Laravel's MVC structure
  • Wind UI Integration: Build UIs with utility-first classes

Creating Views

To generate a new view, use the Magic CLI:

dart run magic:magic make:view Dashboard

Stateless Views

Most of your pages will be stateless, relying on the controller for state management:

import 'package:flutter/material.dart';
import 'package:magic/magic.dart';

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

  @override
  Widget build(BuildContext context) {
    return WDiv(
      className: 'p-6 flex flex-col gap-4',
      children: [
        WText('Dashboard', className: 'text-2xl font-bold text-white'),
        WText('Welcome back!', className: 'text-gray-400'),
      ],
    );
  }
}

Stateful Views

Use MagicStatefulView when you need local widget state or form handling:

class LoginView extends MagicStatefulView {
  const LoginView({super.key});

  @override
  State createState() => _LoginViewState();
}

class _LoginViewState extends MagicStatefulViewState {
  late final form = MagicFormData({
    'email': '',
    'password': '',
    'remember_me': false,
  }, controller: controller);

  @override
  void onClose() => form.dispose();

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, _) => _buildForm(),
    );
  }
  
  Widget _buildForm() {
    return MagicForm(
      formData: form,
      child: WDiv(
        className: 'max-w-md p-6 bg-slate-900 rounded-xl',
        children: [
          WFormInput(
            controller: form['email'],
            label: trans('attributes.email'),
            placeholder: trans('fields.email_placeholder'),
            type: InputType.email,
            validator: rules([Required(), Email()], field: 'email'),
          ),
          WFormInput(
            controller: form['password'],
            label: trans('attributes.password'),
            type: InputType.password,
            validator: rules([Required(), Min(8)], field: 'password'),
          ),
          WButton(
            isLoading: controller.isLoading,
            onTap: _submit,
            className: 'w-full bg-primary p-4 rounded-lg',
            child: WText(trans('auth.login'), className: 'text-white text-center'),
          ),
        ],
      ),
    );
  }
  
  void _submit() {
    final data = form.validated();
    if (data.isNotEmpty) {
      controller.login(data);
    }
  }
}

Form Handling

Views use MagicFormData for centralized form management:

late final form = MagicFormData({
  'name': '',
  'email': '',
  'age': 0,
  'active': true,
}, controller: controller);

// Access values
form.get('name');              // String value
form.value('active');    // Typed value
form['email'];                 // TextEditingController

// Set values
form.setValue('name', 'John');

// Validation
final data = form.validated(); // Returns {} if invalid
form.validate();               // Returns bool

// Cleanup
@override
void onClose() => form.dispose();

Validation Rules

Use the rules() helper within views for client-side validation:

WFormInput(
  controller: form['email'],
  validator: rules([Required(), Email()], field: 'email'),
);

WFormInput(
  controller: form['password_confirmation'],
  validator: rules([
    Required(),
    Same('password', valueGetter: () => form['password'].text),
  ], field: 'password_confirmation'),
);

Rendering Async State

Use controller.renderState() to elegantly handle loading, error, and success states:

@override
Widget build(BuildContext context) {
  return controller.renderState(
    (users) => UserList(users: users),
    onLoading: Center(child: CircularProgressIndicator()),
    onError: (msg) => ErrorWidget(message: msg),
    onEmpty: EmptyState(message: trans('users.empty')),
  );
}

Each callback is optional. Magic provides sensible defaults if omitted.

Responsive Views

For layouts that adapt to screen size, use LayoutBuilder with Wind's responsive helpers:

class DashboardView extends MagicView {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final isDesktop = wScreenIs(context, 'lg');
        
        if (isDesktop) {
          return _buildDesktopLayout();
        }
        return _buildMobileLayout();
      },
    );
  }
}

Or use Wind's responsive prefixes:

WDiv(
  className: '''
    flex flex-col gap-4
    md:flex-row md:gap-6
    lg:gap-8
  ''',
  children: [...],
)

Generating Views

The Magic CLI can generate different types of views:

# Basic stateless view
dart run magic:magic make:view Dashboard

# Stateful view with form support
dart run magic:magic make:view Auth/Login --stateful

# Nested in subfolder
dart run magic:magic make:view Admin/Users/Index

Command Options

Option Description
--stateful Create stateful view with MagicFormData support

Output: Creates lib/resources/views/_view.dart