Iniezione delle dipendenze in Flutter con get_it e injectable
Best practiceIntermedio35 min Flutter 3.x

Iniezione delle dipendenze in Flutter con get_it e injectable

Quando un'app Flutter cresce, gestire manualmente la creazione di servizi, repository e client HTTP diventa rapidamente ingestibile. La Dependency Injection (DI) risolve questo problema disaccoppiando la creazione di un oggetto dal suo utilizzo.

In questo tutorial useremo get_it, un service locator semplice e veloce, insieme a injectable, che genera automaticamente il codice di registrazione tramite annotazioni. Il risultato è un'architettura più pulita, testabile e scalabile.

Vedremo come registrare singleton, factory e dipendenze asincrone, come gestire ambienti diversi (dev/prod) e come iniettare i servizi nei widget.

  1. 1

    Aggiungere le dipendenze al progetto

    Per prima cosa aggiungiamo i pacchetti necessari. get_it è il service locator a runtime, mentre injectable fornisce le annotazioni. Servono anche injectable_generator e build_runner come dipendenze di sviluppo per generare il codice.

    Aggiungi le seguenti voci al file pubspec.yaml oppure usa i comandi flutter pub add.

    dependencies:
      get_it: ^7.7.0
      injectable: ^2.4.4
    
    dev_dependencies:
      build_runner: ^2.4.13
      injectable_generator: ^2.6.2

    Risultato atteso

    Dopo `flutter pub get` i pacchetti sono installati e pronti all'uso.

  2. 2

    Configurare il container get_it

    Creiamo un file injection.dart che espone l'istanza globale di GetIt e la funzione di inizializzazione. L'annotazione @InjectableInit indica a injectable dove generare il codice di configurazione.

    Il file generato si chiamerà injection.config.dart e conterrà l'estensione init che registra tutte le dipendenze annotate.

    import 'package:get_it/get_it.dart';
    import 'package:injectable/injectable.dart';
    
    import 'injection.config.dart';
    
    final getIt = GetIt.instance;
    
    @InjectableInit(
      initializerName: 'init',
      preferRelativeImports: true,
      asExtension: true,
    )
    void configureDependencies() => getIt.init();

    Risultato atteso

    Il file compilerà con un errore su 'injection.config.dart' finché non lanceremo il generatore: è normale.

  3. 3

    Annotare i servizi da iniettare

    Ora creiamo i servizi. Usiamo @lazySingleton per un'istanza unica creata alla prima richiesta, ideale per client di rete e repository. Con @injectable (factory) invece si ottiene una nuova istanza ad ogni get.

    Nota come UserRepository dipende da ApiClient: injectable risolve automaticamente le dipendenze nel costruttore, purché anch'esse siano registrate.

    import 'package:injectable/injectable.dart';
    
    @lazySingleton
    class ApiClient {
      Future<String> fetchName() async {
        await Future.delayed(const Duration(milliseconds: 300));
        return 'Mario Rossi';
      }
    }
    
    @lazySingleton
    class UserRepository {
      final ApiClient _api;
    
      UserRepository(this._api);
    
      Future<String> getUserName() => _api.fetchName();
    }

    Risultato atteso

    I servizi sono annotati e verranno riconosciuti dal generatore al passo successivo.

  4. 4

    Generare il codice con build_runner

    Con le annotazioni in posizione, lanciamo il generatore di codice. Il comando analizza il progetto e crea injection.config.dart con tutte le registrazioni.

    Usa il flag --delete-conflicting-outputs per sovrascrivere eventuali file generati in precedenza senza errori.

    flutter pub run build_runner build --delete-conflicting-outputs

    Risultato atteso

    Viene creato il file `injection.config.dart` e l'errore di import nel passo 2 scompare.

  5. 5

    Inizializzare la DI nel main

    Prima di avviare l'app dobbiamo popolare il container. Chiamiamo configureDependencies() nel main, dopo WidgetsFlutterBinding.ensureInitialized() (necessario se registri dipendenze asincrone o usi plugin).

    Da questo momento qualsiasi punto dell'app potrà recuperare i servizi con getIt<Tipo>().

    import 'package:flutter/material.dart';
    import 'injection.dart';
    
    void main() {
      WidgetsFlutterBinding.ensureInitialized();
      configureDependencies();
      runApp(const MyApp());
    }

    Risultato atteso

    L'app si avvia e tutte le dipendenze sono registrate nel container get_it.

  6. 6

    Utilizzare i servizi in un widget

    Recuperiamo UserRepository dal service locator e usiamolo in un FutureBuilder. Evita di chiamare getIt direttamente nel build: recuperalo una sola volta in initState o in un campo final, così eviti lookup ripetuti ad ogni rebuild.

    Questo approccio mantiene i widget disaccoppiati dalla logica di creazione delle dipendenze.

    class HomePage extends StatefulWidget {
      const HomePage({super.key});
    
      @override
      State<HomePage> createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      final _repo = getIt<UserRepository>();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('DI con get_it')),
          body: Center(
            child: FutureBuilder<String>(
              future: _repo.getUserName(),
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return const CircularProgressIndicator();
                }
                return Text('Utente: ${snapshot.data}');
              },
            ),
          ),
        );
      }
    }

    Risultato atteso

    Dopo un breve caricamento la schermata mostra 'Utente: Mario Rossi'.

  7. 7

    Gestire ambienti diversi con @Environment

    Un vantaggio della DI è poter fornire implementazioni diverse in base all'ambiente. Definiamo un'interfaccia e due implementazioni annotate con @Environment: una per lo sviluppo (dati finti) e una per la produzione.

    Al momento dell'inizializzazione passiamo l'environment desiderato a getIt.init(environment: ...), aggiornando la firma di configureDependencies. Questo è utilissimo anche per iniettare mock nei test.

    import 'package:injectable/injectable.dart';
    
    abstract class ConfigService {
      String get baseUrl;
    }
    
    @Environment('dev')
    @LazySingleton(as: ConfigService)
    class DevConfigService implements ConfigService {
      @override
      String get baseUrl => 'https://dev.api.example.com';
    }
    
    @Environment('prod')
    @LazySingleton(as: ConfigService)
    class ProdConfigService implements ConfigService {
      @override
      String get baseUrl => 'https://api.example.com';
    }
    
    // injection.dart aggiornato
    void configureDependencies(String env) => getIt.init(environment: env);
    
    // main.dart
    // configureDependencies('dev');

    Risultato atteso

    In base all'environment passato, get_it registra l'implementazione corretta di ConfigService. Ricorda di rilanciare build_runner dopo le modifiche.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!