iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 22
1
Mobile Development

iOS Developer Learning Flutter系列 第 22

iOS Developer Learning Flutter. Lesson21 Provider

昨天我們提到了狀態管理的基本功:InheritedWidget
今天談談進階版的InheritedWidget:Prodiver
Provider是一個套件
不只是Flutter Favorite而已

更是Flutter 12125 個套件裡面
最多人喜歡的(唯一超過兩千個like)
接下來我們就趕快來看看Prodiver該如何使用吧

Today Preview

先說明本日例湯如下~

  1. 有一個TabBarController
  2. 有三組NavigationContrller包兩個ViewController
  3. 只有第一組有抽屜選單,可以登出登入
  4. 第一個VC要登入才可push到第二個VC
  5. 若登出了第二個VC要pop回第一個VC
    調味料胡椒:push到2ndVC時TabBar要還在
    調味料鹽巴:push到2ndVC時點TabBarItem要popToRootVC

食譜在這

通常我們以前可能會用NotificationCenter來完成上述情境☘️☘️☘️
換成Provider會像這樣子

  1. [通知者]
    先把我們關心的對象定義成ChangeNotifier(ChangeNotifier還是Flutter提供的類別)
    在狀態改變的function中callnotifyListeners()去通知大家
class LoginChangeNotifier with ChangeNotifier { 

  //Flutter沒有private修飾子, 用下底線開頭表示私有
  bool _isLogin = false;
  bool get isLogin => _isLogin;
  
  //因為Provider好像沒有提供onChange之類的callback, 所以自行定義
  VoidCallback onLogin;
  VoidCallback onLogout;

  loginToggle() {
    _isLogin = !_isLogin;
    notifyListeners();
    if (isLogin) {
      onLogin();
    } else {
      onLogout();
    }
  }
}
  1. [提供者]
    跟InheritedWidget一樣
    把它放到最高的位置
    而Provider很貼心的考慮到可能有多組狀態需要管理
    所以提供了MultiProvider類別
    讓我們可以把需要的Provider們都放進去
return MultiProvider(
  child: tabScaffold,
  providers: [
    ChangeNotifierProvider.value(
      value: LoginChangeNotifier()
    )
  ],
);
  1. [發動者]
    在特定的時機利用
    Provider.of取得我們關心的通知對象, 發動特定事件
    關於listen參數
    源碼說明如下:

/// If [listen] is true (default), later value changes will trigger a new
/// [State.build] to widgets, and [State.didChangeDependencies] for
/// [StatefulWidget].

我的理解是當你發動時
狀態已經自己管理了
就不需要接受通知
故為false

Switch(
  value: isLogin,
  onChanged: (isOn) {

    setState(() {
      isLogin = isOn;
    });

    Provider.of<LoginChangeNotifier>(context, listen: false).loginToggle();
  },
)
  1. [接受者]
    有兩種監聽方式

A. 一樣使用Provider.of(這時就要listen)

    Provider.of<LoginChangeNotifier>(context).onLogout = (){
      Navigator.pop(context);
    };

B. 使用Consumer類別

Consumer<LoginChangeNotifier>( 
  child: Text("如果登出就會踢回前一頁"),
  builder: (context, LoginChangeNotifier loginModel, widget){
    loginModel.onLogout = () {
      Navigator.pop(context);
    };
    //這邊return出去的才是最後顯示的widget
    return Row(children: [
      widget, //這個widget就是上面的child
      Text(" :)")
    ], mainAxisAlignment: MainAxisAlignment.center);
  },
)

有什麼差別呢?
根據等等文末推薦的資料

首先这证明了 Provider.of(context) 会导致调用的 context 页面范围的刷新。
那么第二个页面刷新没有呢? 刷新了,但是只刷新了 Consumer 的部分,甚至连浮动按钮中的 Icon 的不刷新我们都给控制了。
你可以在 Consumer 的 builder 方法中验证,这里不再啰嗦
假如你在你的应用的 页面级别 的 Widget 中,使用了 Provider.of(context)。会导致什么后果已经显而易见了,每当其状态改变的时候,你都会重新刷新整个页面。
虽然你有 Flutter 的自动优化算法给你撑腰,但你肯定无法获得最好的性能。
所以在这里我建议各位尽量使用 Consumer 而不是 Provider.of(context) 获取顶层数据。

簡言之
就是Consumer只刷新局部Provider.of刷新整頁

附上完整程式碼

還記得前面提到的胡椒跟鹽巴嗎?
在code裡面
我就不特別解說了
(那邊也是花了點時間研究...)

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

class LessonPageProvider extends StatefulWidget {
  @override
  _LessonPageProviderState createState() => _LessonPageProviderState();
}

class _LessonPageProviderState extends State<LessonPageProvider> {

  bool isLogin = false;
  int currentIndex = 0;

  final pages = [
    PushNextPage(Colors.orangeAccent, showAppBar: true),
    PushNextPage(Colors.black12),
    PushNextPage(Colors.brown),
  ];

  final items = [
    BottomNavigationBarItem(icon: Icon(IcoFontIcons.brandMacOs), label: "甲"),
    BottomNavigationBarItem(icon: Icon(IcoFontIcons.brandMacOs), label: "乙"),
    BottomNavigationBarItem(icon: Icon(IcoFontIcons.brandMacOs), label: "丙"),
  ];

  final keys = [
    GlobalKey<NavigatorState>(),
    GlobalKey<NavigatorState>(),
    GlobalKey<NavigatorState>()
  ];

  @override
  Widget build(BuildContext context) {

    final tabScaffold = CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: items,
        onTap: (idx){
          if (idx == currentIndex) {
            keys[currentIndex].currentState.popUntil((route) => route.isFirst);
          }
          currentIndex = idx;
        },
      ),
      tabBuilder: (ctx, idx){

        return CupertinoTabView(
          navigatorKey: keys[idx],
          builder: (BuildContext context) =>
            CupertinoPageScaffold(
              child: pages[idx],
              navigationBar: idx != 1 ? null : CupertinoNavigationBar(
                middle: Text("庫比蒂諾"),
              ),
            ),
        );
      },
    );

    return MultiProvider(
      child: tabScaffold,
      providers: [
        ChangeNotifierProvider.value(
          value: LoginChangeNotifier()
        )
      ],
    );
  }
}

//-

class PushNextPage extends StatefulWidget {

  Color backgroundColor = Colors.white;
  bool showAppBar;// = false; //這邊給預設值沒用...要寫在建構子

  PushNextPage(this.backgroundColor, {this.showAppBar = false});

  @override
  _PushNextPageState createState() => _PushNextPageState();
}

class _PushNextPageState extends State<PushNextPage> {

  bool isLogin = false;
  final scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {

    Widget nextPage = PopPreviousPage();

    final center = Container(
      color: widget.backgroundColor,
      alignment: Alignment.center,
      child: CupertinoButton(
        child: Icon(Icons.next_week, size: 30),
        onPressed: (){

          if (Provider.of<LoginChangeNotifier>(context).isLogin) {
            Navigator.push(context,
                MaterialPageRoute(builder: (context) => nextPage)
            );
          } else {
            scaffoldKey.currentState.showSnackBar(
              SnackBar(
                content: Text("只有登入後才可進入下一頁喔 :)")
              )
            );
          }

        },
      )
    );

    final drawer = Drawer(
      child: Column(
        children: [
          Container(
            padding: EdgeInsets.only(top: 30, bottom: 16),
            child: Icon(IcoFontIcons.waiterAlt, size: 100),
          ),

          ListTile(
            title: Text(isLogin ? "登出" : "登入",
              textAlign: TextAlign.center,
              style: TextStyle(
                  fontSize: 20
              ),
            ),

            trailing: Switch(
              value: isLogin,
              onChanged: (isOn) {

                //這句放在setState前面就會壞掉, 也太奇怪了吧....
                //Provider.of<LoginChangeNotifier>(context, listen: false).loginToggle();

                setState(() {
                  isLogin = isOn;
                });

                Provider.of<LoginChangeNotifier>(context, listen: false).loginToggle();
              },
            )
          )
        ],
      )
    );

    return Scaffold(
      key: scaffoldKey,
      appBar: widget.showAppBar ? AppBar(title: Text("瑪提利尤")) : null,
      drawer: widget.showAppBar ? SizedBox(width: 200, child: drawer) : null,
      body: center,
    );
  }
}

//-

class PopPreviousPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        child: Consumer<LoginChangeNotifier>( //用Consumer只刷新這邊
          child: Text("如果登出就會踢回前一頁"),
          builder: (context, LoginChangeNotifier loginModel, widget){
            loginModel.onLogout = () {
              Navigator.pop(context);
            };
            //這邊return出去的才是最後顯示的widget
            return Row(children: [
              widget, //這個widget就是上面的child, child可以保持不重刷
              Text(" :)")
            ], mainAxisAlignment: MainAxisAlignment.center);
          },
        )
      )
    );
  }
}

//-

class LoginChangeNotifier with ChangeNotifier {

  bool _isLogin = false;
  bool get isLogin => _isLogin;
  VoidCallback onLogin;
  VoidCallback onLogout;

  loginToggle() {
    _isLogin = !_isLogin;
    notifyListeners();
    if (isLogin) {
      onLogin();
    } else {
      onLogout();
    }
  }
}

最後推薦一個超詳細的Provider大全:Flutter | 状态管理指南篇——Provider
講得很仔細
內容又超多(目錄都快比我今天的文章還多了 不誇張)
推薦給想深入Provider的朋友

參考連結


下集預告:Notification


上一篇
iOS Developer Learning Flutter. Lesson20 InheritedWidget
下一篇
iOS Developer Learning Flutter. Lesson22 Notification
系列文
iOS Developer Learning Flutter30

尚未有邦友留言

立即登入留言