State Management Fundamentals

MultiProvider & ProxyProvider

50 min Lesson 10 of 14

Managing Multiple Providers

Real-world Flutter applications rarely depend on a single provider. You will typically have authentication state, user profile data, API services, theme preferences, and many other pieces of state that need to be available throughout your widget tree. Provider offers two powerful tools for managing this complexity: MultiProvider for organizing multiple providers, and ProxyProvider for creating providers that depend on other providers.

MultiProvider: Clean Organization

Without MultiProvider, nesting multiple providers leads to deeply indented, hard-to-read code. MultiProvider flattens this nesting into a clean, readable list.

Without MultiProvider (Nested)

// Deeply nested — hard to read and maintain
Widget build(BuildContext context) {
  return ChangeNotifierProvider<AuthModel>(
    create: (_) => AuthModel(),
    child: ChangeNotifierProvider<ThemeModel>(
      create: (_) => ThemeModel(),
      child: ChangeNotifierProvider<CartModel>(
        create: (_) => CartModel(),
        child: Provider<ApiService>(
          create: (_) => ApiService(),
          child: const MyApp(),
        ),
      ),
    ),
  );
}

With MultiProvider (Flat)

// Clean and readable — same result, better structure
Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
      ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel()),
      ChangeNotifierProvider<CartModel>(create: (_) => CartModel()),
      Provider<ApiService>(create: (_) => ApiService()),
    ],
    child: const MyApp(),
  );
}
Important: The order of providers in the MultiProvider list matters. Providers are created top-to-bottom, and they can only access providers declared above them in the list. If CartModel depends on AuthModel, AuthModel must come first.

ProxyProvider: Dependent Providers

A ProxyProvider creates a provider whose value depends on another provider. When the dependency changes, the proxy provider automatically updates its value. This is essential for building service layers where one service needs data from another.

Basic ProxyProvider

class AuthModel extends ChangeNotifier {
  String? _token;
  String? get token => _token;
  bool get isLoggedIn => _token != null;

  void login(String token) {
    _token = token;
    notifyListeners();
  }

  void logout() {
    _token = null;
    notifyListeners();
  }
}

class ApiService {
  final String? authToken;

  ApiService({this.authToken});

  Map<String, String> get headers => {
    'Content-Type': 'application/json',
    if (authToken != null) 'Authorization': 'Bearer \$authToken',
  };

  Future<Map<String, dynamic>> fetchData(String endpoint) async {
    // Uses authToken in headers automatically
    final response = await http.get(
      Uri.parse('https://api.example.com/\$endpoint'),
      headers: headers,
    );
    return jsonDecode(response.body);
  }
}

// Setup: ApiService depends on AuthModel
Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider<AuthModel>(
        create: (_) => AuthModel(),
      ),
      // ProxyProvider rebuilds ApiService when AuthModel changes
      ProxyProvider<AuthModel, ApiService>(
        update: (context, auth, previousApi) => ApiService(
          authToken: auth.token,
        ),
      ),
    ],
    child: const MyApp(),
  );
}

In this example, every time AuthModel notifies its listeners (e.g., after login or logout), the ProxyProvider creates a new ApiService instance with the updated auth token. Any widget watching ApiService will automatically receive the new instance.

ProxyProvider2 and ProxyProvider3

When a provider depends on two or three other providers, use ProxyProvider2 or ProxyProvider3. These variants accept multiple type parameters and provide all dependencies in the update callback.

ProxyProvider2 — Two Dependencies

class UserRepository {
  final ApiService api;
  final CacheService cache;

  UserRepository({required this.api, required this.cache});

  Future<User> getUser(String id) async {
    // Check cache first
    final cached = cache.get('user_\$id');
    if (cached != null) return User.fromJson(cached);

    // Fetch from API if not cached
    final data = await api.fetchData('users/\$id');
    cache.set('user_\$id', data);
    return User.fromJson(data);
  }
}

// Setup with ProxyProvider2
MultiProvider(
  providers: [
    ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
    Provider<CacheService>(create: (_) => CacheService()),
    ProxyProvider<AuthModel, ApiService>(
      update: (_, auth, __) => ApiService(authToken: auth.token),
    ),
    // UserRepository depends on BOTH ApiService and CacheService
    ProxyProvider2<ApiService, CacheService, UserRepository>(
      update: (_, api, cache, __) => UserRepository(
        api: api,
        cache: cache,
      ),
    ),
  ],
  child: const MyApp(),
)

ProxyProvider3 — Three Dependencies

class AnalyticsService {
  final AuthModel auth;
  final ApiService api;
  final AppConfig config;

  AnalyticsService({
    required this.auth,
    required this.api,
    required this.config,
  });

  void trackEvent(String event) {
    if (!config.analyticsEnabled) return;
    api.fetchData('analytics/track?event=\$event&user=\${auth.token}');
  }
}

// ProxyProvider3 with three dependencies
ProxyProvider3<AuthModel, ApiService, AppConfig, AnalyticsService>(
  update: (_, auth, api, config, __) => AnalyticsService(
    auth: auth,
    api: api,
    config: config,
  ),
)
Tip: If you need more than three dependencies, consider restructuring your architecture. A provider depending on four or more other providers is a sign that the class is doing too much. Break it into smaller, focused services.

ChangeNotifierProxyProvider

When your dependent provider is a ChangeNotifier (not just a plain object), use ChangeNotifierProxyProvider. This variant properly disposes the notifier when it is no longer needed and provides the previous instance for state preservation.

ChangeNotifierProxyProvider

class UserProfileModel extends ChangeNotifier {
  final ApiService _api;
  User? _user;
  bool _isLoading = false;

  UserProfileModel({required ApiService api}) : _api = api;

  User? get user => _user;
  bool get isLoading => _isLoading;

  // Update the API service when auth changes
  void updateApi(ApiService newApi) {
    // Re-fetch user data with new credentials
    if (_user != null) {
      loadUser(_user!.id);
    }
  }

  Future<void> loadUser(String id) async {
    _isLoading = true;
    notifyListeners();

    try {
      final data = await _api.fetchData('users/\$id');
      _user = User.fromJson(data);
    } catch (e) {
      _user = null;
    }

    _isLoading = false;
    notifyListeners();
  }
}

// Setup: UserProfileModel depends on ApiService AND is a ChangeNotifier
MultiProvider(
  providers: [
    ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
    ProxyProvider<AuthModel, ApiService>(
      update: (_, auth, __) => ApiService(authToken: auth.token),
    ),
    ChangeNotifierProxyProvider<ApiService, UserProfileModel>(
      create: (context) => UserProfileModel(
        api: context.read<ApiService>(),
      ),
      update: (context, api, previous) {
        // Preserve existing state, update dependency
        previous!.updateApi(api);
        return previous;
      },
    ),
  ],
  child: const MyApp(),
)
Warning: In the update callback of ChangeNotifierProxyProvider, always return the previous instance when possible. Creating a new instance every time will lose accumulated state (loaded data, flags, etc.) and can cause infinite rebuild loops.

Lazy Loading Providers

By default, providers in Flutter are lazy — they are not created until a widget first accesses them. This is beneficial for performance because providers for features the user has not visited yet will not consume resources.

Controlling Lazy Loading

MultiProvider(
  providers: [
    // Lazy (default) — created when first accessed
    ChangeNotifierProvider<CartModel>(
      create: (_) => CartModel(),
      // lazy: true is the default
    ),

    // Eager — created immediately when MultiProvider builds
    ChangeNotifierProvider<AuthModel>(
      create: (_) => AuthModel()..checkStoredSession(),
      lazy: false, // Created immediately
    ),

    // Eager — useful for providers that need initialization
    Provider<DatabaseService>(
      create: (_) => DatabaseService()..initialize(),
      lazy: false, // Start DB connection right away
      dispose: (_, db) => db.close(), // Clean up on dispose
    ),
  ],
  child: const MyApp(),
)
Tip: Set lazy: false for providers that perform initialization work (like checking stored auth tokens or opening database connections). Keep all other providers lazy to avoid unnecessary resource usage at startup.

Provider Scope and Disposal

Providers are scoped to the widget tree where they are declared. When the widget that created a provider is removed from the tree, the provider is automatically disposed. This is particularly important for ChangeNotifier providers, which need cleanup to prevent memory leaks.

Scoped Providers

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      // App-level providers — live for the entire app lifetime
      providers: [
        ChangeNotifierProvider(create: (_) => AuthModel()),
        ChangeNotifierProvider(create: (_) => ThemeModel()),
      ],
      child: MaterialApp(
        routes: {
          '/': (_) => const HomePage(),
          '/shop': (_) => ChangeNotifierProvider(
            // Page-level provider — disposed when leaving /shop
            create: (_) => CartModel(),
            child: const ShopPage(),
          ),
          '/chat': (_) => ChangeNotifierProvider(
            // Page-level provider — disposed when leaving /chat
            create: (_) => ChatModel(),
            child: const ChatPage(),
          ),
        },
      ),
    );
  }
}

Practical Example: Auth + User Profile Chain

Let’s build a complete, real-world example that chains authentication, API service, user repository, and user profile together:

Complete Provider Chain

// 1. Auth provides the token
class AuthModel extends ChangeNotifier {
  String? _token;
  User? _currentUser;

  String? get token => _token;
  User? get currentUser => _currentUser;
  bool get isLoggedIn => _token != null;

  Future<void> login(String email, String password) async {
    final response = await http.post(
      Uri.parse('https://api.example.com/login'),
      body: jsonEncode({'email': email, 'password': password}),
    );
    final data = jsonDecode(response.body);
    _token = data['token'];
    _currentUser = User.fromJson(data['user']);
    notifyListeners();
  }

  void logout() {
    _token = null;
    _currentUser = null;
    notifyListeners();
  }
}

// 2. ApiService depends on AuthModel for the token
class ApiService {
  final String? authToken;
  ApiService({this.authToken});

  Future<dynamic> get(String path) async {
    final response = await http.get(
      Uri.parse('https://api.example.com/\$path'),
      headers: {
        'Content-Type': 'application/json',
        if (authToken != null) 'Authorization': 'Bearer \$authToken',
      },
    );
    return jsonDecode(response.body);
  }
}

// 3. UserRepository depends on ApiService
class UserRepository {
  final ApiService _api;
  UserRepository({required ApiService api}) : _api = api;

  Future<UserProfile> getProfile() async {
    final data = await _api.get('profile');
    return UserProfile.fromJson(data);
  }

  Future<List<Order>> getOrders() async {
    final data = await _api.get('orders');
    return (data as List).map((e) => Order.fromJson(e)).toList();
  }
}

// 4. Wire everything together
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        // Layer 1: Auth (independent)
        ChangeNotifierProvider<AuthModel>(
          create: (_) => AuthModel(),
          lazy: false, // Check stored session immediately
        ),

        // Layer 2: ApiService (depends on Auth)
        ProxyProvider<AuthModel, ApiService>(
          update: (_, auth, __) => ApiService(
            authToken: auth.token,
          ),
        ),

        // Layer 3: UserRepository (depends on ApiService)
        ProxyProvider<ApiService, UserRepository>(
          update: (_, api, __) => UserRepository(api: api),
        ),
      ],
      child: MaterialApp(
        home: Consumer<AuthModel>(
          builder: (context, auth, _) {
            return auth.isLoggedIn
                ? const DashboardPage()
                : const LoginPage();
          },
        ),
      ),
    );
  }
}

// 5. Usage in widgets
class DashboardPage extends StatelessWidget {
  const DashboardPage({super.key});

  @override
  Widget build(BuildContext context) {
    final user = context.select<AuthModel, User?>(
      (auth) => auth.currentUser,
    );

    return Scaffold(
      appBar: AppBar(
        title: Text('Welcome, \${user?.name ?? "User"}'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () => context.read<AuthModel>().logout(),
          ),
        ],
      ),
      body: FutureBuilder<UserProfile>(
        future: context.read<UserRepository>().getProfile(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('Error: \${snapshot.error}'));
          }
          final profile = snapshot.data!;
          return ProfileCard(profile: profile);
        },
      ),
    );
  }
}
Architecture Note: Notice the layered dependency chain: Auth → ApiService → UserRepository. When the user logs in or out, the auth token changes, which triggers a new ApiService, which triggers a new UserRepository. The entire chain updates automatically through Provider’s reactive system.