fluttercode-duplication

Same provider class in different pages or per page provider class?


In Flutter, is it okay to use same provider class in different pages to avoid provider code duplication or will this approach have adverse consequences unseen to me? Should I instead go for one provider class per page (meaning code duplication)?

As I was expanding the code of my project, I realized that multiple pages in my project use similar code/widgets for different purposes and that I could use same provider class across these pages to avoid duplication of codes, writing one provider class for each of those pages. But, I am unsure if this can or will have any adverse side-effects that are unseen to me. So, I have the question stated above.

Parts of my code:

Relevant code of one of my provider:

ClickableTabBarSelected? clickableTabBarSelected =
    ClickableTabBarSelected.leftTab;

void selectedClickableTabBar(ClickableTabBarSelected ctbs) {
  print("#### User selected tab bar: ${ctbs}");
  clickableTabBarSelected = ctbs;
  notifyListeners();
}

My second provider needs to show result in a custom widget when the result is available. This will be something like:

bool _isResultAvailable = false;

bool get isResultAvailableForUse => _isResultAvailable;

void resetResultAvailableStatusWithoutNotifyListners() {
  // Reset status but don't call notifyListeners method.
  _isResultAvailable = false;
}

void setResultAvailableForUseStatus() {
  _isResultAvailable = true;
  notifyListeners();
}

My third provider, is for checking if Admob ad is loaded, and if it is, display a banner ad. The provider has a function initializeBannerHomePage which sends request to Admob, and sets the provider's boolean variable isBannerHomePageLoaded as per the listener status and calls notifyListeners().


Solution

  • Yes, reusing the same Provider class across multiple pages is a good practice to avoid code duplication, However....

    However, it can lead to unintended consequences depending on how the state is managed.

    Understanding the Issue with Shared Providers

    Consider a scenario where a bool property (isActive) in the Provider determines the color of a button:

    If multiple widgets share the same Provider instance and one widget updates isActive, all other widgets listening to the provider will also reflect the change.

    Example:

    Scenario 1: Shared Provider (State Affects All Widgets)

    => Demo Video for this scenario 1: shared_provider_instance

    In this case, all widgets that depend on isActive will change together.

    1. Reusable Widget (Affected by Provider Changes):
    class ResuableWidget extends StatelessWidget {
      const ResuableWidget({
        super.key,
        required this.lable,
        required this.isActive,
        required this.onStateChange,
        required this.activationLable,
      });
    
      final void Function() onStateChange;
      final String lable;
      final bool isActive;
      final String activationLable;
    
      double get screenWidth => MediaQuery.sizeOf(context).width;
      double get screenHeight => MediaQuery.sizeOf(context).height;
    
      @override
      Widget build(BuildContext context) {
        return Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Column(
              children: [
                SizedBox(
                  width: screenWidth * .6,
                  height: screenHeight * .07,
                  child: MaterialButton(
                    onPressed: onStateChange,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(25),
                    ),
                    color: isActive ? Colors.red : Colors.brown,
                    child: Text(lable),
                  ),
                ),
                if (isActive) ...{
                  Text(
                    activationLable,
                    style:
                        const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                },
              ],
            ),
          ],
        );
      }
    }
    

    2. Provider that will manages isActive bool State:

    
    class ReusableProvider extends ChangeNotifier {
      bool isActive = false;
    
      void get changeButtonState {
        isActive = !isActive;
        log("Current Button State :$isActive");
        notifyListeners();
      }
    }
    

    3. Main Widget with a Shared Provider Instance instance ChangeNotifierProvider:

    
    class TestSharedProvider extends StatelessWidget {
      const TestSharedProvider({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.cyan,
            automaticallyImplyLeading: false,
            title: const Text("UI with Same Provider"),
            centerTitle: true,
          ),
          body: ChangeNotifierProvider(
            create: (context) => ReusableProvider(),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Consumer<ReusableProvider>(
                  builder: (context, test1, _) {
                    return ResuableWidget(
                      lable: "Test 1",
                      isActive: test1.isActive,
                      activationLable: "Provider Instance 1 is Active",
                      onStateChange: () {
                        test1.changeButtonState;
                        log("Button 1 State : ${test1.isActive}");
                      },
                    );
                  },
                ),
                SizedBox(height: 20),
                Consumer<ReusableProvider>(
                  builder: (context, test2, _) {
                    return ResuableWidget(
                      lable: "Test 2",
                      isActive: test2.isActive,
                      activationLable: "Provider Instance 2 is Active",
                      onStateChange: () {
                        test2.changeButtonState;
                        log("Button 2 State : ${test2.isActive}");
                      },
                    );
                  },
                ),
              ],
            ),
          ),
        );
      }
    }
    

    Issue: Since both buttons use the same ReusableProvider instance, clicking one button affects all the widgets that related with isActive.

    => Solution of this Issue

    Scenario 2: Separate Providers (Independent State per Widget)

    => Demo Video for this scenario 2: non_shared_provider

    You have the Main Screen with the reusable widgets but with different Instances of provider ChangenotifierProvider

    
    class TestNonSharedProviderWidget extends StatelessWidget {
      const TestNonSharedProviderWidget({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.cyan,
            automaticallyImplyLeading: false,
            title: const Text("UI with Same Provider"),
            centerTitle: true,
          ),
          body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              ChangeNotifierProvider(
                create: (context) => ReusableProvider(),
                child: Consumer<ReusableProvider>(
                  builder: (context, test1, _) {
                    return ResuableWidget(
                      lable: "Test 1",
                      isActive: test1.isActive,
                      activationLable: "Provider Instance 1 is Active",
                      onStateChange: () {
                        test1.changeButtonState;
                        log("Button 1 State : ${test1.isActive}");
                      },
                    );
                  },
                ),
              ),
              SizedBox(height: 20),,
              ChangeNotifierProvider(
                create: (context) => ReusableProvider(),
                child: Consumer<ReusableProvider>(
                  builder: (context, test2, _) {
                    return ResuableWidget(
                      lable: "Test 2",
                      isActive: test2.isActive,
                      activationLable: "Provider Instance 2 is Active",
                      onStateChange: () {
                        test2.changeButtonState;
                        log("Button 2 State : ${test2.isActive}");
                      },
                    );
                  },
                ),
              ),
            ],
          ),
        );
      }
    }
    

    Finally:

    Really hope this is help you!!