flutterriverpodfreezedflutter-freezedriverpod-generator

Riverpod 2 - Flutter - Single State Class for multiple widgets (instances)


This might be a simple solution for others, but I have honestly been struggling (for almost 2 months now) trying to get this Riverpod 2 Code Strategy to work. I've looked at other questions here on SO (like -> one, two, and three) but I just can't seem to wrap my head around this code solution for some reason :(.

Here is the code strategy:

1. I have a freezed model (TruZapBtnMDL) class that defines all the properties for the custom button (TruZapBtn) widget class.

2. I have a Business Logic Code (TruZapBtnBLC) class that manages all the business logic and state for the TruZapBtn widget [intrinsically].

3. Finally -> I want to implement this logic where I create and maintain 3 distinct TruZapBtn widget objects using only the "key" property [argument] passed during the class (widget) initialization.


Please see my code logic (attempts) below ...

1. MODEL Class ...

@freezed
class TruZapBtnMDL with _$TruZapBtnMDL {   //* <- Defines the 'Tru Zap Button' Freezed Model - MDL.
  factory TruZapBtnMDL({
    required Key key,
    required bool isEnabled,
    required bool isPressed,
    required bool isLockDown,
    required bool isInitBuild,
    required Color colorLocked,
    required Color colorPressed,
  }) = _TruZapBtnMDL;
}

2a. STATE Class ...

@riverpod
class TruZapBtnBLC extends _$TruZapBtnBLC {   //* <- Defines the 'Tru Zap Button' Business Logic Code - BLC.

  @override
  TruZapBtnMDL build() {
    return TruZapBtnMDL(
      isEnabled: true,
      isPressed: false,
      isLockDown: false,
      isInitBuild: true,
      key: const Key('TZB_BLC'),
      colorLocked: Colors.amber,
      colorPressed: Colors.blueAccent,
    );
  }

}

2b. BUTTON (Widget) Class ...

class TruZapBtn extends ConsumerWidget {   //* <- Defines the actual 'Tru Zap Button' Widget (GUI).

  //* Class Properties ...
  @override final Key key;
  final Color colorPressed, colorLocked;
  final bool isEnabled, isPressed, isLockedDown, isInitBuild;

  //* Class Constructor ...
  const TruZapBtn({
    this.isEnabled=true,
    this.isPressed=false,
    this.isInitBuild=true,
    this.isLockedDown=false,
    this.colorLocked=Colors.amber,
    this.key=const Key('TruZapBtnGUI'),
    this.colorPressed=Colors.blueAccent,
  }) : super(key: key);

  //* Instantiate the 'TruZapBtnBLC' State Provider ...
  static TruZapBtnBLC _blcTZB = TruZapBtnBLC();

  @override
  Widget build(BuildContext context, WidgetRef ref) {

    //* 1. Link Provider to ensure realtime GUI updates ...
    ref.watch(truZapBtnBLCProvider);

    //* 2. Read NOTIFIER - only once !!!
    if(_blcTZB.isInitBuild) {
      _blcTZB = ref.read(truZapBtnBLCProvider.notifier);
      _blcTZB.isInitBuild = false;
    }

    //* 3. Build modular TruZapBtn Widget [with intrinsic BUSINESS CODE LOGIC] !!!
    return InkWell(
      onTap: () {},
      onTapUp: (TapUpDetails tuds) {},
      onTapDown: (TapDownDetails tdds) {},
      child: Container(
        color: _blcTZB.state.isLockDown
                ? colorLocked
                : colorPressed,
      ),
    );
  }
}

3. IMPLEMENTATION Code ...

TruZapBtn tzbBtn01 = TruZapBtn(key: Key('TZB01'));

TruZapBtn tzbBtn02 = TruZapBtn(key: Key('TZB02'));

TruZapBtn tzbBtn03 = TruZapBtn(key: Key('TZB03'));

Right now the code implementation above does not create 3 unique state trackers/providers for each of the 3 distinct buttons (as intended), but simply creates a single state tracker/provider that changes the state in all 3 buttons simultaneously when any one of the 3 buttons are pressed.

So how do I ensure that only the state of the 2nd button is updated when tzbBtn02 is pressed - and likewise for the other buttons?

I trust the code explains my problem better than my actual word explanation.


Solution

  • Your issue lies in how you're handling state with Riverpod. In your current implementation, all buttons share the same instance of TruZapBtnBLC. If you want each button to have its own state, you need to create separate providers for each button or use an identifier to distinguish each instance.

    Here's a solution using a Family provider with Riverpod:

    Modify your provider to use ProviderFamily:

    final truZapBtnBLCProvider = Provider.family<TruZapBtnBLC, Key>((ref, key) => TruZapBtnBLC(key: key));
    

    Update the TruZapBtnBLC class to accept a key:

    class TruZapBtnBLC extends StateNotifier<TruZapBtnMDL> {
      final Key key;
    
      TruZapBtnBLC({required this.key})
          : super(TruZapBtnMDL(
              key: key, // => Add this !
              isEnabled: true,
              isPressed: false,
              isLockDown: false,
              isInitBuild: true,
              colorLocked: Colors.amber,
              colorPressed: Colors.blueAccent,
            ));
    
      // For example, a method to toggle the button press state could look like this:
      void togglePressed() {
        state = state.copyWith(isPressed: !state.isPressed);
      }
    }
    

    In your widget TruZapBtn, use this provider with the key:

    class TruZapBtn extends ConsumerWidget {
      final Color colorPressed, colorLocked;
      final bool isEnabled, isPressed, isLockedDown;
    
      const TruZapBtn({
        required Key key,
        this.isEnabled = true,
        this.isPressed = false,
        this.isLockedDown = false,
        this.colorLocked = Colors.amber,
        this.colorPressed = Colors.blueAccent,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // Link to the provider using the key to get the specific state for this button
        final blc = ref.watch(truZapBtnBLCProvider(key));
    
        // Now, use the state from the BLC directly
        return InkWell(
          onTap: () {
            blc.togglePressed(); // For example
          },
          child: Container(
            color: blc.state.isLockedDown ? colorLocked : colorPressed,
          ),
        );
      }
    }
    

    You no longer need the isInitBuild logic and the instantiation of TruZapBtnBLC in your widget. You can remove those parts.

    You can now create each button with a unique key, and each button will have its own state managed by the provider:

    TruZapBtn tzbBtn01 = TruZapBtn(key: Key('TZB01'));
    TruZapBtn tzbBtn02 = TruZapBtn(key: Key('TZB02'));
    TruZapBtn tzbBtn03 = TruZapBtn(key: Key('TZB03'));
    

    Using a ProviderFamily allows you to create a separate instance of your BLC for each unique key you provide. Thus, each button will have its own state, independent of the other buttons.