flutter_filament

Framework admin panel untuk Flutter yang terinspirasi Filament 5. Bangun panel admin lengkap dengan Panel, Resource, Form schema, Table schema, dan Dashboard widget — semua schema-driven, tanpa perlu menulis UI CRUD manual.

  • Panel sebagai pondasi (mirip Filament Panel)
  • Resource<T> otomatis men-generate 4 halaman: list, create, edit, view
  • FormSchema builder dengan komponen reaktif (TextInput, Select, Toggle, DatePicker, dst.)
  • TableSchema dengan search/sort/filter/pagination, row actions, header actions, bulk actions
  • DataSource<T> abstrak — backend bebas (Firestore, REST, GraphQL, in-memory)
  • Mason bricks untuk scaffolding model/resource/page/widget dari CLI

Daftar isi

  1. Instalasi
  2. Quick start
  3. Peta konsep Filament → flutter_filament
  4. Panel
  5. Resource
  6. FormSchema + komponen
  7. TableSchema + kolom + action
  8. Dashboard widget
  9. FilamentPage (halaman kustom)
  10. DataSource
  11. Theme
  12. Mason bricks (CLI scaffolding)
  13. Publish ke pub.flutter-io.cn

Instalasi

Tambahkan di pubspec.yaml:

dependencies:
  flutter_filament: ^0.1.0

atau pakai versi lokal (monorepo):

dependencies:
  flutter_filament:
    path: packages/flutter_filament

Lalu:

flutter pub get

Quick start

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_filament/flutter_filament.dart';

final adminPanel = Panel(
  id: 'admin',
  path: '/admin',
  brandName: 'My App',
  theme: const FilamentTheme(colors: FilamentColors.amber),
  resources: [ProdukResource()],
  widgets: const [TotalPenjualanWidget()],
);

final router = GoRouter(routes: adminPanel.buildRoutes());

void main() => runApp(MaterialApp.router(routerConfig: router));

Jalankan aplikasi dan buka /admin. Sidebar otomatis terbentuk dari daftar resources dan widgets.


Peta konsep

Filament 5 (PHP) flutter_filament (Dart)
Panel::make('admin') Panel(id: 'admin', ...)
Resource::class class FooResource extends Resource<Foo>
Form::schema([...]) FormSchema(components: [...])
Table::columns([...]) TableSchema<T>(columns: [...])
TextInput::make('name')->required() TextInput(name: 'name', required: true)
Select::make('status')->options([...]) Select(name: 'status', options: [...])
Toggle, Checkbox, DatePicker, Textarea nama sama
Section::make('Info')->schema([...]) Section(title: 'Info', children: [...])
Grid::make(2) Grid(columns: 2, children: [...])
TextColumn::make('name')->searchable() TextColumn(name: 'name', searchable: true)
BadgeColumn, IconColumn, BooleanColumn nama sama
Action, BulkAction, HeaderAction RowAction, BulkAction, HeaderAction
StatsOverviewWidget StatWidget(stats: [...])
ChartWidget, TableWidget ChartWidget, TableDashboardWidget
ListRecords, CreateRecord, EditRecord, ViewRecord otomatis — dibuat oleh Resource.buildRoute()
Page::class (standalone) class FooPage extends FilamentPage

Panel

Panel adalah top-level container. Satu aplikasi bisa punya banyak panel (misal admin dan app), masing-masing dengan path, theme, dan resource sendiri.

final adminPanel = Panel(
  id: 'admin',                      // unik per panel
  path: '/admin',                   // mount path di router
  brandName: 'My App',
  brandLogo: Image.asset('logo.png', width: 32),  // opsional
  theme: const FilamentTheme(
    colors: FilamentColors.amber,   // atau .blue / .emerald / .rose / .indigo / .slate
    brightness: Brightness.light,
    borderRadius: 8,
  ),
  dashboardTitle: 'Dashboard',
  dashboardIcon: Icons.dashboard_outlined,
  resources: [
    ProdukResource(),
    UserResource(),
  ],
  pages: const [
    PengaturanPage(),
  ],
  widgets: const [
    TotalPenjualanWidget(),
    GrafikMingguanWidget(),
  ],
  // override layout sidebar jika perlu; null = auto dari resources+pages
  navigationOverride: null,
  // widget di bawah sidebar (misal tombol logout)
  sidebarFooter: const SidebarFooter(),
);

// Daftarkan route Panel ke GoRouter:
final router = GoRouter(routes: [
  ...adminPanel.buildRoutes(),
  // ...route lain
]);

buildRoutes() meng-generate otomatis:

  • /admin → Dashboard (widgets)
  • /admin/<resource.slug> → List
  • /admin/<resource.slug>/create → Create
  • /admin/<resource.slug>/:id → View
  • /admin/<resource.slug>/:id/edit → Edit
  • /admin/<page.slug> → FilamentPage kustom

Resource

Resource<T> mengikat sebuah model ke Panel: menyediakan data source, schema form, schema table, dan 4 halaman default.

import 'package:flutter/material.dart';
import 'package:flutter_filament/flutter_filament.dart';
import 'models/produk.dart';

class ProdukResource extends Resource<Produk> {
  @override String get slug => 'produk';
  @override String get label => 'Produk';
  @override String get pluralLabel => 'Produk';
  @override IconData get icon => Icons.inventory_2;
  @override String? get navigationGroup => 'Master Data';
  @override int get navigationSort => 10;

  @override
  DataSource<Produk> get dataSource => ProdukServices.dataSource;

  @override
  String recordId(Produk r) => r.id;

  @override
  String recordTitle(Produk r) => r.nama;

  @override
  Map<String, dynamic> toFormData(Produk r) => r.toJson();

  @override
  FormSchema form(ResourceContext<Produk> ctx) => FormSchema(
    columns: 2,
    components: [
      Section(
        title: 'Informasi Produk',
        icon: Icons.info_outline,
        columns: 2,
        children: [
          TextInput(name: 'nama', label: 'Nama', required: true, columnSpan: 2),
          NumberInput(name: 'harga', label: 'Harga', prefix: 'Rp', required: true),
          Select<String>(
            name: 'kategori',
            label: 'Kategori',
            options: const [
              SelectOption('makanan', 'Makanan'),
              SelectOption('minuman', 'Minuman'),
            ],
          ),
          Toggle(name: 'aktif', label: 'Aktif', defaultValue: true),
          Textarea(name: 'deskripsi', label: 'Deskripsi', rows: 4, columnSpan: 2),
        ],
      ),
    ],
  );

  @override
  TableSchema<Produk> table() => TableSchema<Produk>(
    defaultSort: 'nama',
    columns: [
      TextColumn<Produk>(
        name: 'nama', label: 'Nama',
        accessor: (r) => r.nama,
        searchable: true, sortable: true, bold: true,
      ),
      TextColumn<Produk>(
        name: 'harga', label: 'Harga',
        accessor: (r) => r.harga,
        formatter: (r) => 'Rp ${r.harga}',
        sortable: true,
      ),
      BadgeColumn<Produk>(
        name: 'kategori', label: 'Kategori',
        accessor: (r) => r.kategori,
        color: (r) => r.kategori == 'makanan' ? Colors.orange : Colors.blue,
      ),
      BooleanColumn<Produk>(
        name: 'aktif', label: 'Aktif',
        accessor: (r) => r.aktif,
      ),
    ],
    filters: [
      TableFilter(
        name: 'kategori',
        label: 'Kategori',
        options: const [
          TableFilterOption('makanan', 'Makanan'),
          TableFilterOption('minuman', 'Minuman'),
        ],
      ),
    ],
    rowActions: [
      RowAction.view<Produk>((ctx, row) async { /* handled by list page */ }),
      RowAction.edit<Produk>((ctx, row) async { /* handled by list page */ }),
      RowAction.delete<Produk>((ctx, row) async {
        await ProdukServices.dataSource.delete(row.id);
      }),
    ],
    headerActions: [
      HeaderAction(
        name: 'export',
        label: 'Ekspor CSV',
        icon: Icons.download,
        onPressed: (ctx) async { /* ... */ },
      ),
    ],
  );
}

ResourceContext<T> berisi operation (create/edit/view) dan record saat ini, sehingga schema bisa bercabang:

FormSchema form(ResourceContext<Produk> ctx) => FormSchema(
  components: [
    TextInput(name: 'nama', label: 'Nama', required: true),
    if (ctx.isEdit)
      TextInput(name: 'id', label: 'ID', disabled: true),
  ],
);

Override halaman default kalau perlu:

@override
List<ResourcePageDef> pages() => [
  ResourcePageDef.list(),
  ResourcePageDef.create(),
  ResourcePageDef.edit(),
  // view dihilangkan
  ResourcePageDef.custom(
    path: 'laporan',
    name: 'laporan',
    builder: (ctx, state) => const ProdukLaporanPage(),
  ),
];

FormSchema + komponen

Daftar komponen built-in:

Komponen Kegunaan Filament setara
TextInput Text satu baris TextInput::make()
Textarea Text multi-baris Textarea::make()
NumberInput Angka (int/double) dengan min/max/prefix/suffix TextInput::make()->numeric()
Select<T> Dropdown single-select Select::make()
Toggle Switch on/off Toggle::make()
CheckboxInput Checkbox Checkbox::make()
DatePickerInput Tanggal / jam / tanggal+jam DatePicker::make() / TimePicker
Section Card bertitle + children (bisa collapsible) Section::make()
Grid Layout N kolom untuk anak-anaknya Grid::make(n)

Validasi & reaktivitas:

TextInput(
  name: 'email',
  label: 'Email',
  required: true,
  rules: [
    (value, state) {
      if (value is String && !value.contains('@')) return 'Email tidak valid';
      return null;
    },
  ],
  visibleWhen: (state) => state.get<bool>('subscribe') == true,
)

visibleWhen dan rules menerima FormStateController jadi kamu bisa akses nilai field lain — ini mirip Filament $get('field').

Render form manual (di luar resource):

class MyForm extends StatefulWidget {
  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _state = FormStateController();
  final _schema = FormSchema(components: [
    TextInput(name: 'nama', label: 'Nama', required: true),
    NumberInput(name: 'umur', label: 'Umur'),
  ]);

  void _submit() {
    if (!_schema.validate(_state)) {
      setState(() {}); // tampilkan error
      return;
    }
    final values = _schema.extractValues(_state);
    print(values); // {nama: '...', umur: 25}
  }

  @override
  Widget build(BuildContext context) => Column(children: [
    FormBuilderWidget(schema: _schema, state: _state),
    FilledButton(onPressed: _submit, child: const Text('Simpan')),
  ]);
}

TableSchema + kolom + action

Daftar kolom:

Kolom Kegunaan
TextColumn<T> Text biasa, bisa formatter
BadgeColumn<T> Badge berwarna + icon opsional
DateColumn<T> Tanggal dengan pattern dd MMM yyyy
IconColumn<T> Icon driven by row
BooleanColumn<T> ✓/✗ otomatis

Setiap kolom bisa searchable, sortable, align (start/center/end), width, dan menyediakan accessor: (row) => value untuk mengekstrak data.

Action types:

// Row (per baris)
RowAction<Produk>(
  name: 'duplicate',
  label: 'Duplikat',
  icon: Icons.copy,
  color: ActionColor.info,
  requiresConfirmation: true,
  visibleWhen: (row) => row.aktif,
  onPressed: (ctx, row) async { /* ... */ },
)

// Header (atas tabel — biasanya "Create", "Export")
HeaderAction(
  name: 'create',
  label: 'Tambah Produk',
  icon: Icons.add,
  onPressed: (ctx) async { ctx.goNamed('produk.create'); },
)

// Bulk (aksi pada beberapa row terpilih)
BulkAction<Produk>(
  name: 'delete',
  label: 'Hapus',
  color: ActionColor.danger,
  requiresConfirmation: true,
  onPressed: (ctx, rows) async { /* ... */ },
)

Factory cepat untuk action standar:

RowAction.edit<Produk>((ctx, row) async { /* ... */ })
RowAction.view<Produk>((ctx, row) async { /* ... */ })
RowAction.delete<Produk>((ctx, row) async { /* ... */ })  // sudah include confirmation

Render tabel manual (di luar resource):

TableBuilderWidget<Produk>(
  schema: produkTableSchema,
  dataSource: ProdukServices.dataSource,
  idOf: (row) => row.id,
  onRowTap: (row) => /* ... */,
)

Dashboard widget

StatWidget — baris statistik:

class TotalPenjualanWidget extends DashboardWidget {
  const TotalPenjualanWidget();

  @override int get columnSpan => 12;

  @override
  Widget build(BuildContext context) => const StatWidget(
    stats: [
      Stat(label: 'Penjualan hari ini', value: 'Rp 1,2jt',
           icon: Icons.payments, delta: 12.4),
      Stat(label: 'Order baru', value: '48',
           icon: Icons.shopping_cart, delta: -3.1),
      Stat(label: 'Pelanggan', value: '312',
           icon: Icons.people, description: '+12 minggu ini'),
    ],
  );
}

ChartWidget — bar chart sederhana built-in:

const ChartWidget(
  title: 'Penjualan mingguan',
  subtitle: '7 hari terakhir',
  data: [
    ChartPoint('Sen', 12), ChartPoint('Sel', 18),
    ChartPoint('Rab', 7),  ChartPoint('Kam', 24),
    ChartPoint('Jum', 16),
  ],
)

TableDashboardWidget — embed tabel di dashboard:

TableDashboardWidget<Order>(
  title: 'Order terbaru',
  schema: recentOrdersSchema,
  dataSource: OrderServices.dataSource,
  idOf: (r) => r.id,
)

Custom widget — subclass DashboardWidget langsung untuk apapun (misal chart dengan fl_chart, map, dll).

columnSpan adalah grid 12-kolom — 6 = setengah lebar di desktop, 12 = full; mobile di-collapse jadi full.


FilamentPage (halaman kustom)

Untuk halaman non-CRUD yang tetap ikut layout Panel (sidebar + topbar):

class PengaturanPage extends FilamentPage {
  const PengaturanPage();

  @override String get slug => 'pengaturan';
  @override String get title => 'Pengaturan Sistem';
  @override IconData get icon => Icons.settings;
  @override String? get navigationGroup => 'Sistem';

  @override
  List<Widget> buildHeaderActions(BuildContext context) => [
    OutlinedButton(onPressed: () {}, child: const Text('Reset')),
    FilledButton(onPressed: () {}, child: const Text('Simpan')),
  ];

  @override
  Widget buildBody(BuildContext context) => const Text('Isi halaman...');
}

Daftar ke Panel di pages: [const PengaturanPage()]. Route otomatis jadi /<panel.path>/pengaturan.


DataSource

DataSource<T> adalah interface backend. Implementasikan sesuai stack kamu:

abstract class DataSource<T> {
  Future<PaginatedResult<T>> list(ListQuery query);
  Future<T?> get(String id);
  Future<T> create(Map<String, dynamic> data);
  Future<T> update(String id, Map<String, dynamic> data);
  Future<void> delete(String id);
  Stream<List<T>>? watch(ListQuery query) => null;  // opsional (live)
}

MemoryDataSource<T> — built-in untuk prototyping/test:

final ds = MemoryDataSource<Produk>(
  idOf: (p) => p.id,
  toMap: (p) => p.toJson(),
  fromMap: (m) => Produk.fromJson(m),
  seed: [
    Produk(id: '1', nama: 'Kopi', harga: 15000, aktif: true),
  ],
);

Contoh impl Firestore:

class FirestoreProdukDataSource extends DataSource<Produk> {
  final _col = FirebaseFirestore.instance.collection('produk');

  @override
  Future<PaginatedResult<Produk>> list(ListQuery query) async {
    Query<Map<String, dynamic>> q = _col;
    if (query.sortBy != null) {
      q = q.orderBy(query.sortBy!, descending: query.sortDesc);
    }
    final start = (query.page - 1) * query.perPage;
    final snap = await q.limit(query.perPage + start).get();
    final rows = snap.docs
        .skip(start)
        .map((d) => Produk.fromJson({...d.data(), 'id': d.id}))
        .toList();
    return PaginatedResult(
      data: rows, total: snap.size, page: query.page, perPage: query.perPage,
    );
  }

  @override
  Future<Produk?> get(String id) async {
    final s = await _col.doc(id).get();
    return s.exists ? Produk.fromJson({...s.data()!, 'id': s.id}) : null;
  }

  @override
  Future<Produk> create(Map<String, dynamic> data) async {
    final doc = await _col.add(data);
    return Produk.fromJson({...data, 'id': doc.id});
  }

  @override
  Future<Produk> update(String id, Map<String, dynamic> data) async {
    await _col.doc(id).update(data);
    return (await get(id))!;
  }

  @override
  Future<void> delete(String id) => _col.doc(id).delete();
}

Theme

const FilamentTheme(
  colors: FilamentColors.amber,        // atau .blue, .emerald, .rose, .indigo, .slate
  brightness: Brightness.light,         // atau .dark
  borderRadius: 8,
  fontFamily: 'Inter',                  // opsional
)

Kustom palette penuh:

FilamentTheme(
  colors: FilamentColors(
    primary: Color(0xFF6366F1),
    success: Color(0xFF10B981),
    warning: Color(0xFFF59E0B),
    danger:  Color(0xFFEF4444),
    info:    Color(0xFF3B82F6),
    gray:    Color(0xFF6B7280),
  ),
)

Akses di widget kamu sendiri lewat FilamentThemeScope.of(context):

final theme = FilamentThemeScope.of(context);
Container(color: theme.surface, ...);

Mason bricks (CLI scaffolding)

Paket ini datang dengan 4 Mason brick untuk men-generate boilerplate:

# satu kali di root project
mason get

# generate model class
mason make model --name Produk --fields "nama:String, harga:int, aktif:bool, createdAt:DateTime"

# generate Resource lengkap + 4 halaman, auto-register ke panel_config.dart
mason make resource \
  --name Produk --label "Produk" --pluralLabel "Produk" \
  --icon inventory_2 --group "Master Data" \
  --fields "nama:String, harga:int, aktif:bool"

# generate FilamentPage standalone
mason make page --name Pengaturan --title "Pengaturan Sistem" --icon settings

# generate DashboardWidget (type: stat/chart/custom)
mason make widget --name TotalProduk --type stat --columnSpan 4

Setiap brick resource, page, widget punya post_gen hook yang menambahkan import + registrasi ke lib/core/filament/panel_config.dart di antara marker:

// filament:imports
// filament:resources-begin ... // filament:resources-end
// filament:pages-begin     ... // filament:pages-end
// filament:widgets-begin   ... // filament:widgets-end

Jangan hapus marker — hook mengandalkannya untuk auto-register. Setelah generate, edit file resource-nya untuk menyesuaikan form/table sesuai domain.

Lihat bricks/*/README.md untuk detail tiap brick.


Publish ke pub.flutter-io.cn

Kalau mau rilis paket ini (atau fork-nya) ke pub.flutter-io.cn:

1. Siapkan metadata

Edit pubspec.yaml, hapus baris publish_to: 'none' lalu lengkapi:

name: flutter_filament
description: Filament 5-inspired admin panel framework for Flutter. Schema-driven forms, tables, resources with auto-generated CRUD pages.
version: 0.1.0
homepage: https://github.com/<user>/flutter_filament
repository: https://github.com/<user>/flutter_filament
issue_tracker: https://github.com/<user>/flutter_filament/issues
documentation: https://pub.flutter-io.cn/documentation/flutter_filament/latest/
topics:
  - admin
  - panel
  - crud
  - filament
  - scaffold

pub.flutter-io.cn pakai description untuk pencarian — 60–180 karakter, imbangi keyword & kejelasan.

2. File wajib

  • README.md — sudah ada
  • CHANGELOG.md — catatan versi (wajib, pub.flutter-io.cn akan render)
  • LICENSE — lisensi open-source (MIT/Apache-2.0/BSD umum)

Template CHANGELOG.md:

## 0.1.0

- Initial release.
- Panel + Resource + 4 default pages (list/create/edit/view).
- FormSchema with TextInput, Textarea, Select, Toggle, Checkbox, DatePicker,
  NumberInput, Section, Grid.
- TableSchema with TextColumn, BadgeColumn, DateColumn, IconColumn,
  BooleanColumn + search/sort/filter/pagination.
- Dashboard widgets: StatWidget, ChartWidget, TableDashboardWidget.
- DataSource abstraction + MemoryDataSource impl.
- Mason bricks: model, resource, page, widget.

3. Example app

pub.flutter-io.cn memberi poin ekstra kalau ada example/:

packages/flutter_filament/
├── example/
│   ├── pubspec.yaml
│   ├── lib/
│   │   └── main.dart        # minimal app pakai flutter_filament
│   └── README.md

4. Validasi sebelum publish

cd packages/flutter_filament

# analyze harus 0 issue
flutter analyze

# format
dart format --set-exit-if-changed .

# dry-run: pub.flutter-io.cn akan cek pubspec, LICENSE, README, dan kalkulasi pub score
dart pub publish --dry-run

Outputnya menampilkan daftar file yang akan di-upload + warning. Perbaiki sampai bersih.

5. Login & publish

# pertama kali: login pakai akun Google
dart pub login

# publish (akan minta konfirmasi y/N)
dart pub publish

pub.flutter-io.cn akan meng-upload paket, men-generate dokumentasi dari docstring, dan menghitung pub score:

  • ✓ follows Dart file conventions (pubspec, README, CHANGELOG, LICENSE)
  • ✓ provides documentation (dartdoc ///)
  • ✓ platform support (Android/iOS/web/desktop — Flutter multi-platform otomatis)
  • ✓ passes static analysis
  • ✓ supports up-to-date dependencies

6. Versioning

Patuh semver:

  • 0.1.0 → 0.1.1 — bugfix
  • 0.1.0 → 0.2.0 — fitur baru, breaking change boleh (pre-1.0)
  • 0.x → 1.0.0 — API stabil, breaking harus di mayor bump setelahnya

Setiap rilis: bump version: di pubspec + tulis catatan di CHANGELOG + dart pub publish.

7. Verified publisher (opsional)

Untuk badge ✓ di pub.flutter-io.cn, daftarkan domain di pub.flutter-io.cn/publishers dan tambahkan publish_to ke pubspec. Butuh domain aktif + DNS TXT record.


Lisensi

MIT — bebas dipakai, dimodifikasi, didistribusikan. Lihat LICENSE.

Libraries

flutter_filament
Flutter Filament - A Filament 5-inspired admin panel framework for Flutter.