Perché puntare sui widget adattivi

Uno dei punti di forza di Flutter è la possibilità di scrivere una sola base di codice per più piattaforme. Tuttavia, un'app che presenta esattamente la stessa UI Material su iOS può risultare fuori posto agli occhi degli utenti Apple, abituati a controlli, transizioni e comportamenti diversi. I widget adattivi permettono di rispettare le convenzioni della piattaforma senza duplicare l'intera interfaccia.

In questo articolo vediamo come sfruttare i costruttori .adaptive, i widget Cupertino e una strategia di platform detection pulita.

Costruttori .adaptive integrati

Flutter offre alcuni widget che cambiano automaticamente aspetto in base alla piattaforma. Il più noto è Switch.adaptive, ma esistono anche altri.

// Mostra uno switch in stile iOS su Apple, Material altrove
Switch.adaptive(
  value: isEnabled,
  onChanged: (value) => setState(() => isEnabled = value),
);

// Indicatore di caricamento adattivo
const CircularProgressIndicator.adaptive();

// Slider adattivo
Slider.adaptive(
  value: volume,
  onChanged: (v) => setState(() => volume = v),
);

Anche Checkbox.adaptive e Radio.adaptive sono disponibili nelle versioni recenti di Flutter. Questi costruttori sono il modo più rapido per introdurre adattività senza scrivere logica condizionale.

Il pacchetto Cupertino

Quando i costruttori .adaptive non bastano, si può ricorrere direttamente ai widget Cupertino, che replicano fedelmente i componenti iOS.

import 'package:flutter/cupertino.dart';

CupertinoButton(
  color: CupertinoColors.activeBlue,
  onPressed: () {},
  child: const Text('Conferma'),
);

// Un dialog in stile iOS
showCupertinoDialog(
  context: context,
  builder: (context) => CupertinoAlertDialog(
    title: const Text('Attenzione'),
    content: const Text('Vuoi davvero eliminare l\'elemento?'),
    actions: [
      CupertinoDialogAction(
        isDestructiveAction: true,
        onPressed: () => Navigator.pop(context),
        child: const Text('Elimina'),
      ),
      CupertinoDialogAction(
        onPressed: () => Navigator.pop(context),
        child: const Text('Annulla'),
      ),
    ],
  ),
);

Platform detection pulita

Per decidere quale widget mostrare, evita di usare dart:io con Platform.isIOS sparso ovunque: non funziona su web e rende il codice difficile da testare. Meglio centralizzare la logica.

import 'package:flutter/foundation.dart';

bool get isApplePlatform {
  return defaultTargetPlatform == TargetPlatform.iOS ||
      defaultTargetPlatform == TargetPlatform.macOS;
}

Usare defaultTargetPlatform da package:flutter/foundation.dart è preferibile perché è sicuro anche su Flutter Web e può essere sovrascritto nei test tramite debugDefaultTargetPlatformOverride.

Creare un widget adattivo personalizzato

Mettiamo insieme i concetti creando un pulsante che si adatta alla piattaforma.

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

class AdaptiveButton extends StatelessWidget {
  const AdaptiveButton({
    super.key,
    required this.label,
    required this.onPressed,
  });

  final String label;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    final isApple = defaultTargetPlatform == TargetPlatform.iOS ||
        defaultTargetPlatform == TargetPlatform.macOS;

    if (isApple) {
      return CupertinoButton.filled(
        onPressed: onPressed,
        child: Text(label),
      );
    }

    return FilledButton(
      onPressed: onPressed,
      child: Text(label),
    );
  }
}

Adattare la struttura delle pagine

Anche la scaffold di base cambia tra le piattaforme. Su iOS si usa CupertinoPageScaffold con CupertinoNavigationBar, mentre su Android Scaffold con AppBar.

Widget buildScaffold(BuildContext context, Widget body) {
  if (isApplePlatform) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Profilo'),
      ),
      child: SafeArea(child: body),
    );
  }
  return Scaffold(
    appBar: AppBar(title: const Text('Profilo')),
    body: body,
  );
}

Best practice

  • Non esagerare: adatta solo i componenti dove la differenza è percepibile (pulsanti, dialog, switch, navigazione). Un'app coerente col brand può mantenere gran parte dell'UI condivisa.
  • Testa su entrambe le piattaforme: usa debugDefaultTargetPlatformOverride per verificare l'aspetto senza cambiare dispositivo.
  • Considera i pacchetti community: flutter_platform_widgets offre widget già adattivi e riduce il boilerplate, ma valuta il costo in termini di dipendenze.
  • Attenzione al web e al desktop: defaultTargetPlatform restituisce valori sensati anche lì, ma decidi consapevolmente quale stile preferire.

Conclusione

I widget adattivi permettono di offrire un'esperienza che rispetta le aspettative degli utenti su ogni piattaforma senza sacrificare la codebase unica di Flutter. Iniziando dai costruttori .adaptive, passando ai widget Cupertino e centralizzando la platform detection, si ottiene un'app che appare nativa ovunque, mantenendo il codice pulito e testabile.