Để khỏi mất thời gian giới thiệu. Chúng ta có luôn 2 từ khoá, cũng là 2 thư viện được sử dụng để thiết kế khung code cho Flutter. Do Flutter là UI framework dạng widget/component tương tự với React, ta cũng có thể dùng Redux nhưng khối lượng code sẽ dày lên không cần thiết. Và qua một thời gian nghiên cứu, thì Provider architecture có vẻ phù hợp hơn cả:

Tại thời điểm viết bài, 2 lib cần thiết có version tương ứng là:

provider: 4.0.2
provider_architecture: 1.0.5

Chúng ta sẽ xây dựng một app đơn giản: 2 màn hình:

– Ở màn hình (1): Có 1 nút mà khi ở trạng thái Unauthenticated (tạm gọi là trạng thái A) thì sẽ điều hướng sang màn hình thứ (2).
– Ở màn hình thứ (2): Có 1 nút mà khi click vào, chúng ta sẽ giả lập thao tác login: Click vào, sau 2 giây thì trạng thái Unauthenticated sẽ chuyển sang Authenticated. Khi quay lại màn hình (1) thì cái nút ở màn (1) bấm vào chỉ còn print ra console chữ gì đó tuỳ.

Bài viết mặc định hiểu là bạn đã từng làm việc với React / Redux nên sẽ có một số khái niệm đưa ra mang tính ánh xạ sang Redux.

Provider ở đây không khác gì Provider component trong React Redux, và cũng hoạt động theo dạng bên cung (provider) và bên cầu (consumer). Do Dart là ngôn ngữ optional static typing nên tác giả của lib Provider có chia làm một số loại cho dễ dùng:

Provider

Nhận một giá trị vào nhưng giá trị đó không được update cho bên consumer. Nhưng nó không giúp bạn update UI khi giá trị mà nó nhận vào thay đổi một cách tự động.

Ví dụ, lấy một ViewModel như sau:

class MyModel { 
  String someValue = 'Hello';  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
  }
}
SampleViewModel

Sau đó bọc cái widget root bằng widget Provider, rồi tham chiếu đến ViewModel đã tạo bằng widget Consumer.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<MyModel>( //                                <--- Provider
      create: (context) => MyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                padding: const EdgeInsets.all(20),
                color: Colors.green[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return RaisedButton(
                      child: Text('Do something'),
                      onPressed: (){
                        // We have access to the model.
                        myModel.doSomething();
                      },
                    );
                  },
                )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
  }
}

class MyModel { //                                               <--- MyModel
  String someValue = 'Hello';
  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
  }
}
P

Kết quả ta được:

prov

– Phần UI được build với chữ “Hello” lấy từ ViewModel.
– Nhấn nút Do something sẽ kích hoạt hàm doSomething() trong ViewModel, nhưng UI không được update vì Provider vốn chẳng lắng nghe sự kiện nào cả.

ChangeNotifierProvider

Cái này dùng là tiện nhất, nó sẽ tự động bỏ lắng nghe khi cần thiết (VD: Khi widget không còn hiện). Nó cũng tự lắng nghe và update data, khiến cho widget được build lại. Tuy nhiên cần phải viết ViewModel cho nó, và cũng là phần chính trong bài viết này.

Trong phần code trên, đổi Provider sang ChangeNotifierProvider. ViewModel class cần phải sử dụng ChangeNotifier mixin hoặc (bằng từ khoá with hoặc extends) để có thể dùng được hàm notifyListeners() và mỗi khi dùng nó, ChangeNotifierProvider sẽ được thông báo, và Consumer sẽ rebuild widget.

Code đầy đủ:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<MyModel>( //      <--- ChangeNotifierProvider
      create: (context) => MyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                  padding: const EdgeInsets.all(20),
                  color: Colors.green[200],
                  child: Consumer<MyModel>( //                  <--- Consumer
                    builder: (context, myModel, child) {
                      return RaisedButton(
                        child: Text('Do something'),
                        onPressed: (){
                          myModel.doSomething();
                        },
                      );
                    },
                  )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
  }
}

class MyModel with ChangeNotifier { //                          <--- MyModel
  String someValue = 'Hello';

  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
    notifyListeners();
  }
}
CNP

– Trong hầu hết các trường hợp, các ViewModel sẽ được đặt trong file rời khác nhau (giống reducer của Redux) và dùng flutter/foundation.dart để sử dụng ChangeNotifier.
Consumer sẽ rebuild lại toàn bộ widget con của nó mỗi khi notifyListeners() được gọi. Cái nút trong UI không cần được update, nên thay vì dùng Consumer, bạn cũng có thể dùng Provider.of và set thuộc tính listen = false. Đây cũng là practice được khuyến cáo của thư viện provider.

StreamProvider

Về cơ bản thì đây là wrapper cho StreamBuilder. Lắng nghe sự thay đổi data và notify mỗi khi nó thay đổi. Stream Controller gắn liền với Provider này sẽ bắn ra giá trị mới nhất mà nó phát hiện được.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamProvider<MyModel>( //                       <--- StreamProvider
      initialData: MyModel(someValue: 'default value'),
      create: (context) => getStreamOfMyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                padding: const EdgeInsets.all(20),
                color: Colors.green[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return RaisedButton(
                      child: Text('Do something'),
                      onPressed: (){
                        myModel.doSomething();
                      },
                    );
                  },
                )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
    
  }
}

Stream<MyModel> getStreamOfMyModel() { //                        <--- Stream
  return Stream<MyModel>.periodic(Duration(seconds: 1),
          (x) => MyModel(someValue: '$x'))
      .take(10);
}

class MyModel { //                                               <--- MyModel
  MyModel({this.someValue});
  String someValue = 'Hello';
  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
  }
}
StreamProvider

SP

– StreamProvider sẽ báo cho Consumer biết để rebuild khi có stream event mới.
– Dùng Hot Restart (Shift + R) để trở về giá trị ban đầu.
– Để ý rằng khi nhấn Do something thì sẽ chẳng có gì xảy ra. Nếu cần thì bạn nên dùng cái ChangeNotifierProvider chứ không phải StreamProvider.
– Có thể dùng StreamProvider để làm BLoC.

FutureProvider

Về cơ bản, đây chỉ là một wrapper quanh FutureBuilder. Ta cung cấp cho nó một giá trị ban đầu để hiện UI, sau khi xong Future thì báo cho các Consumer biết để update. Ví dụ:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureProvider<MyModel>( //                      <--- FutureProvider
      initialData: MyModel(someValue: 'default value'),
      create: (context) => someAsyncFunctionToGetMyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                padding: const EdgeInsets.all(20),
                color: Colors.green[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return RaisedButton(
                      child: Text('Do something'),
                      onPressed: (){
                        myModel.doSomething();
                      },
                    );
                  },
                )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( //                    <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
    
  }
}

Future<MyModel> someAsyncFunctionToGetMyModel() async { //  <--- async function
  await Future.delayed(Duration(seconds: 3));
  return MyModel(someValue: 'new data');
}

class MyModel { //                                               <--- MyModel
  MyModel({this.someValue});
  String someValue = 'Hello';
  Future<void> doSomething() async {
    await Future.delayed(Duration(seconds: 2));
    someValue = 'Goodbye';
    print(someValue);
  }
}
FutureProvider

fp

FutureProvider sẽ báo cho Consumer biết để rebuild lại widget khi Future đã xử lý xong.
– Dùng Hot Restart để rebuild app với các giá trị ban đầu.
– Để ý rằng khi nhấn Do something, UI không được update lại (kể cả sau khi Future đã hoàn thành) vì đơn giản là nó không có nhiệm vụ đó). Nếu cần chức năng đó thì bạn nên dùng ChangeNotifierProvider
FutureProvider có thể dùng để đọc / ghi data từ file, hoặc call API. Nhưng ta cũng có thể làm việc đó với FutureBuilder mà không cần đến package Provider này. Nói chung cái Widget này không mấy hữu ích.

ValueListenableProvider

Cái này gần giống ChangeNotifierProvider nhưng phức tạp hơn mộ chút.

Nếu bạn có một class ViewModelv ới ValueNotifier như sau:

class MyModel {  ValueNotifier<String> someValue = ValueNotifier('Hello');  void doSomething() {
    someValue.value = 'Goodbye';
  }
}

thì sử dụng ValueListenableProvider, bạn sẽ có thể lắng nghe thay đổi. Tuy nhiên, nếu muốn dùng method trên model từ UI, thì cũng phải cung cấp model. Đoạn code sau đây mô tả việc Provider cung cấp MyModel cho Consumer, để đưa ValueNotifier cho ValueListenableProvider.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<MyModel>(//                              <--- Provider
      create: (context) => MyModel(),
      child: Consumer<MyModel>( //                           <--- MyModel Consumer
          builder: (context, myModel, child) {
            return ValueListenableProvider<String>.value( // <--- ValueListenableProvider
              value: myModel.someValue,
              child: MaterialApp(
                home: Scaffold(
                  appBar: AppBar(title: Text('My App')),
                  body: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[

                      Container(
                          padding: const EdgeInsets.all(20),
                          color: Colors.green[200],
                          child: Consumer<MyModel>( //       <--- Consumer
                            builder: (context, myModel, child) {
                              return RaisedButton(
                                child: Text('Do something'),
                                onPressed: (){
                                  myModel.doSomething();
                                },
                              );
                            },
                          )
                      ),

                      Container(
                        padding: const EdgeInsets.all(35),
                        color: Colors.blue[200],
                        child: Consumer<String>(//           <--- String Consumer
                          builder: (context, myValue, child) {
                            return Text(myValue);
                          },
                        ),
                      ),

                    ],
                  ),
                ),
              ),
            );
          }),
    );
  }
}

class MyModel { //                                             <--- MyModel
  ValueNotifier<String> someValue = ValueNotifier('Hello'); // <--- ValueNotifier
  void doSomething() {
    someValue.value = 'Goodbye';
    print(someValue.value);
  }
}
VLP

Kết quả tả được:

vlp

– Nhấn nút sẽ khiến chữ “Hello” thành “Goodbye” nhờ ValueListenableProvider.
– Nên sử dụng Provider.of(context, listen: false) để tránh update liên tục.
– Provider cung cấp myModel cho cả ValueListenableProvider và closure của button “Do something”.
– Consumer cho widget Text biết lấy value từ ValueListenableProvider vì kiểu T trong generic đều khớp (đều là String).

ValueListenableProvider

Lấy luôn ví dụ cho dễ hiểu: Ta có thể kết hợp nhiều Provider thành dạng mảng, như này:

Provider<Something>(
  create: (_) => Something(),
  child: Provider<SomethingElse>(
    create: (_) => SomethingElse(),
    child: Provider<AnotherThing>(
      create: (_) => AnotherThing(),
      child: someWidget,
    ),
  ),
),
Nested

sẽ thành:

MultiProvider(
  providers: [
    Provider<Something>(create: (_) => Something()),
    Provider<SomethingElse>(create: (_) => SomethingElse()),
    Provider<AnotherThing>(create: (_) => AnotherThing()),
  ],
  child: someWidget,
)
Multi

ở phần 2, chúng ta sẽ thực hiện đưa thư viện provider_architecture vào sử dụng theo dạng MVVM cho ứng dụng.