State Management and Global Variables

872

The provider model (or more generally, the subscriber-listener model) does not break encapsulation. For encapsulation to be broken, changes in one object directly result in the mutation of another object. For example, if you had these two classes:

class A {
  int x;
  B b;
}

class B {
  String s;
  A a;
}

So here we have a co-dependency between A and B. Now say there was a method in A to mutate its state:

void changeState(int i) {
  this.x = i;
  b.s = i.toString();
}

This breaks encapsulation because A is directly changing the state of B, which could result in some broken functionality as B attempts to operate with an externally mutated state.

Now say that there is no explicitly defined co-dependency, and A and B instead communicate through an event bus:

// A
void changeState(int i) {
  this.x = i;
  fireEvent('A-changed', this);
}

// B
listenToEvent<A>('A-changed', handleEvent);

...

void handleEvent(A source) {
  this.s = source.x.toString();
}

Now encapsulation is maintained because A and B are communicating their state changes, and each one is responsible only for maintaining its own state.

This is exactly what happens in provider with ChangeNotifier. When an object updates its state and then calls notifyListeners, Flutter uses an internal event bus to notify any widget that is listening to that object, either explicitly or via provider's Provider.of or use of a Consumer. The object is not directly causing the widget to rebuild, but is instead communicating through the event bus and informing the widget that it should rebuild itself. This preserves encapsulation as every object involved is only ever responsible for its own state.


As for how provider is different from global variables, that's because provider utilizes a pattern called "dependency injection" (or DI for short). With DI, you can take a non-global object and "inject" it into the widgets that "depend" on it. This is most commonly done through a constructor, i.e.:

class SomeService {
  Database db;

  SomeService(this.db);
}

In that example, the SomeService class needs to communicate with a database, but instead of calling some global database service, it has the Database object passed to it upon its creation. This gives SomeService a database to communicate with without relying on a global object. (This also allows you to mock the Database object for testing purposes.)

With provider, it implements DI using a slightly different approach. Instead of using constructors, provider embeds the resources into the widget tree. Widgets from that point in the tree down will then be able to retrieve that resource dynamically, but widgets that are above that point or in a different section of the tree will not have access to it. This is how provider implements DI, and is what separates it from global variables.

Share:
872
Muthu N
Author by

Muthu N

Updated on December 20, 2022

Comments

  • Muthu N
    Muthu N over 1 year

    I'm teaching myself Flutter, having previously studied traditional object oriented languages. I'm still a beginner, but it's clear state management is a key issue in Flutter, so I'm learning about it (mostly with Providers).

    Much of what I'm learning seems like using global variables, set and called from other classes (with notifyListener() calls in the latter case). But when I learned about OOP I was taught this was a "bad" thing. One object may inadvertently change a variable's value, breaking another object. In other words, encapsulation is good, global variables are bad--they violate the encapsulation idea.

    What am I missing?

    • GrahamD
      GrahamD about 4 years
      Don't think you are missing anything. Flutter state management is a bit of a dark art at the moment imho, hope it crystalises at some point. I use scoped_model given my app was first developed 12 months ago but I am moving to Provider since scoped_model is being deprecated. I use global variables as well but typically they are constants or set only once when starting the app (haven't got my head around singletons).
    • Abion47
      Abion47 about 4 years
      Global variables are just that - global. When you use provider, you are using dependency injection to give widgets a reference to an object that may or may not be global (and shouldn't be, since that defeats the purpose of provider). In terms of "breaking encapsulation", that is not the case because the source is notifying the target that its state has changed, which in turn causes the target object to update its own state if necessary. This is not the same as one object directly mutating the properties of another object because each object is still responsible for its own state.
    • Abion47
      Abion47 about 4 years
      @GrahamD A non-constant global value that is set once at start-up is exactly what a singleton is.
    • GrahamD
      GrahamD about 4 years
      @Abion47 thanks for the confirmation of what it thought it was. Appreciated. I think I am struggling more with the singleton syntax / terminology rather than the concept. Will get there, hopefully.
    • Abion47
      Abion47 about 4 years
      @GrahamD There is no single syntax or implementation for singletons (though there is the convention that it is implemented as a class with a static instance property). In the strictest terms, a singleton is merely a global variable that is assigned once and then referenced many times over the course of a program's lifetime. Anything beyond that is just implementation detail.
    • GrahamD
      GrahamD about 4 years
      Ok, cool. So my implementation is probably perfectly valid. It was the class implementation of it that lost me. Thanks again.
    • Muthu N
      Muthu N about 4 years
      ",,,value that is set once at start-up..." Isn't that what the final prefix before a variable declaration does?
    • Abion47
      Abion47 about 4 years
      @AlC Yes, but that's a different topic. final is, in essence, a way to mark a class field as read-only, so that it is only ever initialized once and only read from and never written to from that point on. This can be true for objects created at start-up but also for objects created at any other point in the program's lifetime. Singletons in Dart are frequently marked as final (or, more specifically, static final), but a final field is not implicitly a singleton.