Due app native per iOS e Android: due team, due codebase, il doppio dei costi. Se la tua azienda ha bisogno di un'app mobile, lo sai bene. Noi, di Meteora Web, abbiamo visto troppe PMI bloccate tra budget insufficienti e tempi lunghi. Flutter risolve questo problema: un unico codice Dart genera app performanti su entrambi i sistemi. Non è magia, è ingegneria. E funziona in produzione.
Perché Flutter e non React Native, Kotlin o Swift
Flutter compila in codice nativo. Non è un WebView travestito. Ogni pixel viene disegnato dal motore Skia direttamente sulla GPU. Risultato: prestazioni paragonabili a un'app nativa, ma con un'unica base di codice. React Native usa un ponte JavaScript che introduce latenza. Flutter no. Inoltre, il linguaggio Dart offre hot reload in tempo reale: modifichi il codice e vedi il risultato in mezzo secondo. Per noi sviluppatori significa iterare veloce. Per il cliente, meno ore fatturate.
Dart: il linguaggio che sembra semplice ma è potente
Se conosci Java, C# o JavaScript, Dart ti sarà familiare. È tipizzato, compilato sia AOT (release) che JIT (debug). L'installazione è immediata:
# Installa Dart SDK (o Flutter SDK che lo include)
brew install dart
# Verifica
dart --version
Per iniziare un progetto Flutter:
flutter create mia_app
cd mia_app
flutter run
Il hot reload cambia le regole del gioco. Durante lo sviluppo, salvi il file e l'app si aggiorna senza perdere lo stato. Noi lo usiamo ogni giorno per regolare margini, colori e logica al volo, senza ricompilare.
Dart basi: variabili, funzioni, classi
void main() {
print('Hello, Meteora!');
}
int somma(int a, int b) => a + b;
class Utente {
final String nome;
Utente(this.nome);
}
Null safety integrato: String? nome significa che può essere null. Niente più NullPointerException a runtime.
Sponsored Protocol
Widget: tutto è un widget
In Flutter tutto è un widget. Il bottone, il padding, la riga, persino l'app stessa. Non ci sono Activity o ViewController. Si costruisce l'interfaccia componendo widget dentro widget. Questo è il widget tree.
StatelessWidget vs StatefulWidget
- StatelessWidget: non cambia mai. Icone, testi statici, layout fissi.
- StatefulWidget: può cambiare nel tempo. Un contatore, un form, una lista che si aggiorna.
class Contatore extends StatefulWidget {
@override
_ContatoreState createState() => _ContatoreState();
}
class _ContatoreState extends State<Contatore> {
int _conta = 0;
void _incrementa() {
setState(() { _conta++; });
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Conteggio: $_conta'),
ElevatedButton(onPressed: _incrementa, child: Text('+1')),
],
);
}
}
Attenzione: se abusi di setState in widget troppo grandi, l'app rallenta. Qui entra in gioco lo state management.
State management: Provider, Riverpod, Bloc
Un'app reale ha decine di stati: utente loggato, carrello, notifiche. Gestirli con setState è come pagare le tasse con le ricevute in un cassetto. Serve un metodo.
Provider (consigliato per iniziare)
Provider è la soluzione ufficiale, semplice e stabile. Avvolgi un oggetto con ChangeNotifier e lo esponi con ChangeNotifierProvider. I widget ascoltano i cambiamenti con context.watch.
Sponsored Protocol
class Carrello extends ChangeNotifier {
int _quantita = 0;
int get quantita => _quantita;
void aggiungi() { _quantita++; notifyListeners(); }
}
// Nel main.dart
runApp(
ChangeNotifierProvider(create: (context) => Carrello(), child: MyApp()),
);
// In un widget
final carrello = context.watch<Carrello>();
Text('${carrello.quantita} articoli');
Riverpod (più testabile e sicuro)
Riverpod elimina la dipendenza dal context. I provider sono globali, ma il tipo è controllato. Utile per app grandi con test automatizzati.
Bloc (event-driven, per logiche complesse)
Bloc separa eventi e stati con stream. Potente, ma verboso. Lo usiamo per form di login con validazione asincrona o ricerche in tempo reale.
Navigazione e routing
La navigazione tra schermate è gestita con Navigator. Per app semplici, puoi usare Navigator.push. Per app complesse, usa GoRouter (ufficiale) che supporta routing dichiarativo, redirect e deep link.
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (context, state) => HomePage()),
GoRoute(path: '/dettaglio/:id', builder: (context, state) => DettaglioPage(id: state.pathParameters['id']!)),
],
);
I deep link sono essenziali per campagne marketing: un link apre direttamente la scheda prodotto. Con GoRouter li gestisci in modo nativo.
Flutter con Firebase: backend senza server
Firebase offre autenticazione, database Firestore, storage, notifiche push. Noi lo usiamo per app con utenti registrati e dati in tempo reale. L'integrazione è immediata:
Sponsored Protocol
// pubspec.yaml
dependencies:
firebase_core: ^2.24.0
firebase_auth: ^4.16.0
cloud_firestore: ^4.14.0
// main.dart
await Firebase.initializeApp();
// Login con email
UserCredential user = await FirebaseAuth.instance.signInWithEmailAndPassword(
email: 'a@b.com',
password: '123456',
);
Per le push notification, Firebase Cloud Messaging si integra nativamente. Attenzione alla gestione dei permessi su iOS.
UI: Material Design, Cupertino e widget custom
Flutter offre due set di widget: Material (Android) e Cupertino (iOS). Puoi usarli insieme per un look nativo. Ma a noi piace creare widget custom per il brand del cliente. Un esempio: un pulsante con ombra e gradiente.
class MeteoraButton extends StatelessWidget {
final String label;
const MeteoraButton({required this.label});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.blue, Colors.purple]),
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(blurRadius: 4, color: Colors.grey)],
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Text(label, style: TextStyle(color: Colors.white)),
),
);
}
}
HTTP e API con Dio
Le app devono parlare con il server. Dio è il miglior client HTTP per Dart. Supporta interceptors, retry, timeout e gestione errori.
Sponsored Protocol
final dio = Dio(BaseOptions(baseUrl: 'https://api.meteoraweb.com'));
void fetchProdotti() async {
try {
final response = await dio.get('/prodotti');
final list = response.data as List;
// aggiorna stato
} on DioException catch (e) {
// gestisci errore
}
}
Abbiamo un articolo su Bubble per PMI se preferisci un backend no-code.
Testing: unit, widget e integration
Se non testi, non sai se la app si rompe. In Flutter i test sono nativi:
- Unit test: testano singole funzioni e classi.
- Widget test: testano un widget isolato con pumpWidget.
- Integration test: testano l'app completa su dispositivo o emulatore.
test('somma deve funzionare', () {
expect(somma(2, 3), 5);
});
testWidgets('Contatore incrementa', (tester) async {
await tester.pumpWidget(ContatoreApp());
expect(find.text('Conteggio: 0'), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text('Conteggio: 1'), findsOneWidget);
});
Noi scriviamo test per ogni API call critica e per il flusso di checkout. Riduce i bug in produzione del 70%.
Pubblicare su Play Store e App Store
Pubblicare un'app Flutter non è diverso da una nativa, ma ci sono accorgimenti.
Firma dell'app
Per Android, genera un keystore:
keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
Configura android/app/build.gradle con le credenziali. Per iOS, usa Xcode e gestisci i profili di provisioning.
Ottimizzazione per gli store
Riduci la dimensione dell'APK con flutter build appbundle. Usa shrinking e obfuscation (--obfuscate --split-debug-info). Per iOS, attiva bitcode se richiesto.
Sponsored Protocol
Performance: rendere l'app fluida
Un'app che balbetta non viene usata. Flutter è veloce, ma devi evitare trappole.
Profiling con DevTools
Flutter DevTools include frame rendering chart, memory profiler, CPU profiler. Apri con flutter devtools mentre l'app è in esecuzione.
Best practice
- Usa
constwidget dove possibile (riduce ricostruzioni). - Evita composizioni profonde di widget: estrai in metodi o widget separati.
- Ottimizza immagini con
cached_network_imagee compressione. - Per liste lunghe, usa
ListView.builderinvece diListView(children: []).
In sintesi — cosa fare adesso
- Installa Flutter e segui il corso ufficiale.
- Scegli lo state management: inizia con Provider, poi passa a Riverpod per app più grandi.
- Integra Firebase per autenticazione e dati in tempo reale.
- Scrivi test per i flussi critici prima di pubblicare.
- Pubblica su entrambi gli store con firma e dimensione ottimizzata.
- Monitora le performance con DevTools durante lo sviluppo.
Se invece la tua azienda non ha risorse per uno sviluppo nativo o Flutter, valuta soluzioni no-code come Bubble — ne abbiamo parlato nella nostra guida operativa. Ma se vuoi un'app mobile professionale con un unico team, Flutter è la scelta giusta. Noi, di Meteora Web, lo usiamo in produzione per clienti che vendono, gestiscono magazzini e interagiscono con i clienti in tempo reale. E funziona.