Flutter Provider狀態管理框架

xiangzhihong發表於2022-03-30

一、簡介

在Flutter開發中,或多或少的都會設計到頁面的多狀態管理,如果大家對Flutter技術比較熟悉的話,那麼應該知道下面的一些狀態管理框架,像Bloc,Getx我都用過,整體來說再狀態管理不是很複雜的情況下還是可以的。

在這裡插入圖片描述
接下來,我們來看一下Flutter官方推薦的狀態管理框架Provider是如何使用的。Flutter 針對不同型別物件提供了多種不同的 Provider;Provider 也是藉助了 InheritWidget,將共享狀態放到頂層 MaterialApp 之上;

  • setState能重新整理widget子樹,重新整理範圍太大,並且需要把資料物件傳遞到子類。
  • InheritedWidget不用傳遞資料物件,通過context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();獲取父類的資料。但是重新整理範圍大,只能由上而下傳遞。
  • Provider可以實現區域性重新整理。只要資料物件改變,UI能自動變化,實現響應式程式設計。遮蔽重新整理邏輯,實現響應式資料與UI的繫結。無論是子類或父類改變資料都能重新整理繫結的UI。

在這裡插入圖片描述

二、基本使用

使用之前,需要先在pubspec.yaml新增provider

dependencies:
  flutter:
    sdk: flutter

  provider: ^6.0.2

2.1 Provider基本使用

Provider是一款基於資料流的觀察者模式,使用的第一步就是新建一個繼承自ChangeNotifier的資料管理類。下面,我們來看一下官方的例子使用Provider方式如何實現。

import 'package:flutter/cupertino.dart';

class CountProviderModel extends ChangeNotifier {
  
  int _count = 0;
  int get count => _count;

  void increment() {
    print("increment");
    _count++;
    notifyListeners();
  }
}

在CountProviderModel類中,我們定義了有一個資料增加的方法,最後還呼叫notifyListeners傳送通知。接著,我們新建一個測試頁面,該頁面的根部使用ChangeNotifierProvider元件進行包裹,需要重新整理的地方使用Consumer元件進行包裹,用於消費

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

import 'count_provider.dart';

class ProviderPage extends StatelessWidget {

  const ProviderPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CountProviderModel>(
      create: (_) => CountProviderModel(),
      builder: (context, child) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("ProviderPage"),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text('You have pushed the button this many times:'),
                Consumer<CountProviderModel>(
                  builder: (context, notifier, child) {
                    return Text("${notifier.count}",style: const TextStyle(
                      fontSize: 24,
                    ), );
                  },
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => context.read<CountProviderModel>().increment(),
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      },
    );
  }
}

最後,我們修改一下Flutter專案的入口main檔案。

// 改寫 main.dart
import 'package:flutter/material.dart';
import 'package:stateresearch/pages/ProviderPage.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter狀態管理',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ProviderPage(),
    );
  }
}

重新執行專案,點選右下角的按鈕時數字就會自動加。

2.2 跨頁面狀態共享

作為一個全域性的狀態管理框架,跨頁面的狀態共享是必須的,為了方便說明,我們再新建兩個頁面ProviderPageTwo和ProviderPageThree,對應的程式碼如下:
ProviderPageTwo.dart

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

import 'count_provider.dart';

class ProviderPageTwo extends StatelessWidget {

  const ProviderPageTwo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ProviderPageTwo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Consumer<CountProviderModel>(
              builder: (context, notifier, child) {
                return Text("${notifier.count}",style: const TextStyle(
                  fontSize: 24,
                ));
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<CountProviderModel>().increment();
          // 2秒後跳轉至新的頁面
          Future.delayed(const Duration(seconds: 2), () {
            Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
              return ProviderPageThree();
            }));
          });
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

ProviderPageThree.dart

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

import 'count_provider.dart';

class ProviderPageThree extends StatelessWidget {
  const ProviderPageThree({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ProviderPageThree"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Consumer<CountProviderModel>(
              builder: (context, notifier, child) {
                return Text("${notifier.count}",style: const TextStyle(
                  fontSize: 24,
                ));
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Provider.of<CountProviderModel>(context, listen: false).increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

接著,我們在App頂層進行全域性監聽,因此,其他頁面無需 ChangeNotifierProvider也可以獲取 Model。

void main() {
  runApp(ChangeNotifierProvider(
    create: (_) => CountProviderModel(),
    child: MyApp(),
  ),);
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home:  ProviderPageTwo(),
    );
  }
}

2.3 多Model全域性共享

除了前面的兩種使用場景,Provider還支援多Model的狀態共享。首先,我們再新建一個ProviderModel類。

import 'package:flutter/material.dart';

class ListProviderModel extends ChangeNotifier {

  final List<String> _list = [];
  List<String> get list => _list;

  void push(String value) {
    _list.add(value);
    notifyListeners();
  }
}

然後,我們修改ProviderPageTwo和ProviderPageThree兩個頁面,對應的程式碼如下:
ProviderPageTwo.dart

import 'dart:math';

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

import 'count_provider.dart';
import 'list_provider.dart';

class ProviderPageTwo extends StatelessWidget {

  const ProviderPageTwo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ProviderPageTwo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Consumer<CountProviderModel>(
              builder: (context, notifier, child) {
                return Text("${notifier.count}");
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<CountProviderModel>().increment();
          context.read<ListProviderModel>().push("List-${Random().nextInt(10)}");
          Future.delayed(const Duration(seconds: 2), () {
            Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
              return const ProviderPageThree();
            }));
          });
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

ProviderPageThree.dart

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

import 'count_provider.dart';
import 'list_provider.dart';

class ProviderPageThree extends StatelessWidget {
  const ProviderPageThree({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("ProviderPageThree"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Consumer<CountProviderModel>(
              builder: (context, notifier, child) {
                return Text("${notifier.count}");
              },
            ),
            Consumer<ListProviderModel>(
              builder: (context, notifier, child) {
                return Text("${notifier.list}");
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Provider.of<CountProviderModel>(context, listen: false).increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

最後,我們修改main入口檔案的程式碼,使用MultiProvider 包裹多個Model,如下。

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => CountProviderModel()),
        ChangeNotifierProvider(create: (_) => ListProviderModel()),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home:  ProviderPageTwo(),
    );
  }
}

三、總結

通過前面的例子可以看到,使用Provider時我們需要先新建一個Model物件,它繼承自ChangeNotifier,是一個被觀察的物件,當Model物件改變時需要呼叫notifyListeners通知觀察者重新整理。下面是ChangeNotifier的原始碼:

class ChangeNotifier implements Listenable {
  ObserverList<VoidCallback>? _listeners = ObserverList<VoidCallback>();

  @protected
  bool get hasListeners {
    return _listeners!.isNotEmpty;
  }

  @override
  void addListener(VoidCallback listener) {
    _listeners!.add(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
    _listeners!.remove(listener);
  }

  @mustCallSuper
  void dispose() {
    _listeners = null;
  }

  @protected
  @visibleForTesting
  void notifyListeners() {
    if (_listeners != null) {
      final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners!);
      for (final VoidCallback listener in localListeners) {
        try {
          if (_listeners!.contains(listener))
            listener();
        } catch (exception, stack) {
          ......
        }
      }
    }
  }
}

ChangeNotifier把方法新增到陣列中,然後呼叫notifyListeners時會通知觀察者實現,整個工作流程示意圖如下。
在這裡插入圖片描述

相關文章