DEV Community

kanta13jp1
kanta13jp1

Posted on

DNS & Domain Manager in Flutter Web: TabController FAB Rebuild + colorScheme Token Pattern

DNS & Domain Manager in Flutter Web: TabController FAB Rebuild + colorScheme Token Pattern

What We Built

A full DNS management UI for 自分株式会社 — competing with Cloudflare, Google Domains, and Amazon Route53:

  • Domains tab: domain list + add dialog (registrar: Cloudflare / Google Domains / Route53)
  • DNS Records tab: A / AAAA / CNAME / MX / TXT / NS / SRV / CAA records per domain
  • SSL tab: certificate expiry monitoring across all domains

TabController FAB Rebuild Pattern

FloatingActionButton needs to change per tab. The key is addListener + indexIsChanging guard:

_tabController.addListener(() {
  if (!_tabController.indexIsChanging) {
    setState(() {});  // trigger FAB rebuild
    if (_tabController.index == 2 && _sslStatus.isEmpty) {
      _fetchSslStatus();  // lazy-load SSL data on first visit
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Without !_tabController.indexIsChanging, the listener fires during the tab animation — causing redundant rebuilds and potentially double-firing the SSL fetch.

In build(), return the correct FAB based on the current tab index:

floatingActionButton: _tabController.index == 0
  ? FloatingActionButton.extended(
      onPressed: _showAddDomainDialog,
      icon: const Icon(Icons.add),
      label: const Text('Add Domain'),
    )
  : _tabController.index == 1
    ? FloatingActionButton.extended(
        onPressed: _selectedDomain != null ? _showAddRecordDialog : null,
        icon: const Icon(Icons.add),
        label: const Text('Add Record'),
      )
    : null,
Enter fullscreen mode Exit fullscreen mode

app_analytics as Generic Storage (No New Table)

DNS data lives in the existing app_analytics table, using source as a discriminator:

// Add domain
await supabase.from("app_analytics").insert({
  user_id: userId,
  source: "dns_domain",
  metadata: { domain, registrar, created_at: new Date().toISOString() },
});

// Add DNS record
await supabase.from("app_analytics").insert({
  user_id: userId,
  source: "dns_record",
  metadata: { domain_id, record_type, name, value, ttl, priority },
});
Enter fullscreen mode Exit fullscreen mode

Query by source:

const { data: domains } = await supabase
  .from("app_analytics")
  .select("*")
  .eq("user_id", userId)
  .eq("source", "dns_domain");
Enter fullscreen mode Exit fullscreen mode

No schema migration needed for a new feature — reuse app_analytics + JSONB metadata.


colorScheme Token Pattern for Dark Mode

Hardcoded colors break dark mode. Replace them with Material 3 semantic tokens:

// WRONG — hardcoded, breaks in dark mode
color: Colors.black54

// CORRECT — semantic tokens, auto-adapts
color: colorScheme.outline
color: colorScheme.onSurfaceVariant
color: colorScheme.surfaceContainerHighest
Enter fullscreen mode Exit fullscreen mode

Lookup the token:

final colorScheme = Theme.of(context).colorScheme;
Enter fullscreen mode Exit fullscreen mode

When the app's ThemeData switches between light and dark, all colorScheme.* references update automatically — including the DNS management page.


DropdownButtonFormField: valueinitialValue

Flutter 3.33+ deprecated the value property:

// DEPRECATED
DropdownButtonFormField<String>(
  value: selectedType,
  ...
)

// CORRECT
DropdownButtonFormField<String>(
  initialValue: selectedType,
  ...
)
Enter fullscreen mode Exit fullscreen mode

flutter analyze catches this as deprecated_member_use. Mechanical rename — no behavioral change.


Edge Function Action Map

// GET ?view=domains         → domain list
// GET ?view=records&domain_id=xxx → DNS records for domain
// GET ?view=ssl_status      → SSL expiry for all domains

// POST {action: "add_domain",    domain, registrar}
// POST {action: "add_record",    domain_id, record_type, name, value, ttl, priority}
// POST {action: "delete_record", record_id}
Enter fullscreen mode Exit fullscreen mode

Flutter calls:

// GET with query params
final response = await _supabase.functions.invoke(
  'dns-domain-manager',
  method: HttpMethod.get,
  queryParameters: {'view': 'domains'},
);

// POST
await _supabase.functions.invoke(
  'dns-domain-manager',
  body: {'action': 'add_domain', 'domain': domain, 'registrar': registrar},
);
Enter fullscreen mode Exit fullscreen mode

Architecture Summary

Layer What it does
app_analytics table Storage (reused, no new migration)
dns-domain-manager EF Auth, CRUD, SSL check aggregation
Flutter 3-tab UI Rendering only
colorScheme tokens Automatic dark/light mode

Try it: 自分株式会社

buildinpublic #Flutter #Supabase #Dart #webdev

Top comments (0)