ericltw / notes

0 stars 1 forks source link

Flutter State Management #19

Open ericltw opened 6 years ago

ericltw commented 6 years ago

Stateful Widget

Pros
Cons

InheritedWidget

Rather than having the StatefulWidget pass state directly down to its children, the StatefulWidget will actually pass data to an inherited widget and then the responsibility of that widget is to pass that information down to all of its ancestors in the widget tree.

Pros
Cons
Testing in inheritedWidget

The Dream

test('counter increaments', () {
    final state = new MyWidgetState();
    state.increamentCounter();
    expect(state.counter, 1);
});

The Reality

This is a state class and it's kind of tied to Flutter, you actually have to use the Flutter testing utilities to test this type of logic. We actually have to do is we have to pump a widget. In this case, we'll pump the entire app. Then we have to find the button on screen that actually does the tapping and tap that. You wait for Flutter to re-draw the widget by saying, let's pump the tester, and then finally you would basically look at the widget tree that Flutter is rendering in the test environment and say, is there a widget that's displaying this text?

testWidgets('counter increaments', (tester) async {
    await tester.pumpWidget(new MyApp());
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    expect(find.text('1'), findsOneWidget);
});

Pros

Cons

App Layers

Data Layer

API function

Future<int> _fetchIncreament(http.Client client) async {
    final response = client.get(...);
    final json = JSON.decode(response.body);

    return json["counter"];
}

The easier thing we can actually do is just that function out into its own file, or something like that. And then what we'll do this, rather than creating the HTTP client internally, we'll actually inject the HTTP client. And the reason it's so powerful is because if you're in a browser context, you can provide the browser client. If you're in Flutter itself, you can provide the I/O client. And if you want to actually test these functions, you can provide a mock client. And so this gives you full control over how and the environment that you're executing these things in, and it's really powerful for testing and for sharing code. This function could actually be used cross-platform as well. That's just a natural sort of thing that happens when you extract it. The rest of the function is basically the same.

Domain Layer: Lost of options

Redux

State

what sort of data does my application need. Basically it's just a plain old Dart object, and it should be an immutable Dart object. And so you can use just the immutable annotation to create these sort of immutable state objects.

@immutable
class AppState {
    final int counter;

    AppState(this.counter);
}
Actions

Change state in Redux by dispatching actions. In Redux, these are just simple wither enums, or maybe classes. Classes are necessary if you want to deliver some payload information, such as an ID, along with the action.

enum Actions { increament }
Reducers

In order to update the state, we'll have a function that's called a reducer. All this function does is take in the previous app state and current action that's been dispatched, and it will return a new app state.

AppState reducer(AppState prev, action) {
    if (action == Actions.increament) {
        return new AppState(prev.counter + 1);
    }

    return prev;
}
Store

And then we tie all this together with the store. So the store itself just takes in one required parameter, which is the reducer, and you can optionally provide an initial state. And so what we can do with this is then we can immediately create the store and start using it.

final store = new Store(
    reducer,
    initialState: new AppState(0)
);

print(store.state.counter);
Dispatch

We'll start dispatching actions.

store.dispatch(Actions.increament);

print(store.state.counter); //prints 1
Testing

Because we're just dealing with a simple function, we can actually write a really easy test for this reducer. So you've actually gained a lot of testability here, because all of this is just pure Dart. It's all extracted out of this state class. You can actually really easily test this, and you can also share this type of code cross-platform.

test('Reducer handles increament', () {
   final newState = reducer(
       newAppState(0),
       Actions.increament
   );

   expect(newState.counter, 1);
});
In UI Layer

We'll actually use a library called Flutter_Redux, which sort of melds Redux with Flutter.

But if we tap on this thing, you might have noticed, where's the statefulWidget? So how do we actually tell flutter to redraw? So it will actually dispatch this action. The store will be updated. But what about state changes for Flutter? So this is where we'll introduce another concept from Flutter_Redux, which is called the store connector.

This widget is actually hopefully pretty simple. But what it has two required parameters. The first parameter is what's called the converter. So this will actually be given the full store. And you can convert that into some type of view model. So in this case, our widget only cares about the counter. So we'll just extract the counter out of the state. The next required parameter is a builder method, or builder function. This will actually have a builder method method or function that takes two parameters. And the fist is of course, the build context which you always need. And the second is actually the view model that you've gotten from the converter. So in this case we're just extracting the counter, so I'll name the view model counter. And then you can render whatever widgets you want from this.

new StoreConnector(
    converter: (store) => store.state.counter,
    builder: (context, counter) => new Text("$counter")
);

So if we try that tapping action one more time, we can go ahead and swap out the store provider of context, and grabbing the counter directly for our store connector widget. Now when we tap on this, it will update the store. And the store will actually emit changes, and that's what the store connector is listening to. It's listening to changes to the store. And then it'll say, OK, now that I'm connected, I know that I need to redraw. And so it will go ahead and just redraw that one widget that you provide.

Pros
Bonus

Reference