Flutter:从ValueListenableBuilder到Provider(三)|技术点评

483 阅读4分钟

上一节简单回顾

上一节里面,我们主要了解了单个页面,由多个变量控制Widget,其中核心的组件就是使用MultiProvider进行变量注册,然后通过Selector与Consumer组件对Widget进行创建与控制。并简单分析了Selector与Consumer组件的使用方法和区别。

那么这一节,我们就来聊的就是全局状态管理。这一节的代码可能不会太多,主要聊应用场景。

往期文章:

Flutter:从ValueListenableBuilder到Provider(一)|技术点评

Flutter:从ValueListenableBuilder到Provider(二)|技术点评

全局管理

我们先讲几个App开发的时候的应用场景,由于我是iOS端开发,Android端技术积累不了解,所以以iOS端的实现为主。再来就Flutter中,这些场景应该如何处理。

场景1:A页面,跳转到B页面,在B页面输入了一个姓名,点击确定后,回传A页面展示。

  • iOS端: 这是典型的相邻页面逆传值,B→A,推荐使用callback或delegate进行实现。当然使用Notification也是可以的。

  • Flutter端: 这当然是可以用callback。 不过系统给的pop函数配合协程不香么?下面是官方的代码注释:

void _accept() {
 Navigator.pop(context, true); // dialog returns true
}

根本轮不到Provider出场,Flutter系统自带方法的解决战斗

场景2:A页面,由B1,B2两个子组件构成,B1可以操作,使得A页面状态变化,B2也可以操作,使得A页面状态也变化。

  • iOS端: 这是一个页面,由几个封装好的独立组件构成,需要在B1,B2组件中暴露callback或者delegate,然后在A页面进行统一的实现与逻辑处理。

  • Flutter端: 这当然也可以用callback。 不过考虑到组件B1,B2都在A页面之前,在A构建的时候,在A外层包裹一层Provider组件并注册相应的变量,然后在B1,B2页面使用,下面是代码实现:

class Counter extends ChangeNotifier {
  int _count = 1;

  int get count => _count;

  set count(int value) {
    _count = value;
    notifyListeners();
  }
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: PartManager(),
    );
  }
}

class PartManager extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: _contentView(context),
    );
  }

  Widget _contentView(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Part Manager"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(bottom: 8.0),
              child: Text(
                'count值变化监听',
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(bottom: 8.0),
              child: Consumer<Counter>(
                builder: (context, value, child) {
                  return Text(
                    'count:${value.count}',
                  );
                },
              ),
            ),
            AddButton(),
            SubtractButton(),
            FlatButton(
              color: Colors.green,
              child: Text("Next Page"),
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) {
                    return NextPage();
                  }),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Next Page"),
      ),
      body: Container(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      color: Colors.blue,
      child: Text('count++'),
      onPressed: () {
        Provider.of<Counter>(context, listen: false).count++;
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      color: Colors.blue,
      child: Text('count--'),
      onPressed: () {
        Provider.of<Counter>(context, listen: false).count--;
      },
    );
  }
}

这个例子,看似有点多此一举,不过所以的页面都是由StatelessWidget构成,唯一精细化控制的是Consumer<Counter>

而且试想一下,随着业务量的增加,比如B1组件可以改变B2页面的状态,B2组件可以改变B1页面的状态等等,Provider针对的单一页面下的组件与页面的通信优势就越明显,回想一下,在上一节MultiProvider的例子,也算是这种场景的特例化,只是没有把组件单独拿抽出来封装而已。

image.png

另外,需要注意的是,本例子中有一个NextPage按钮,点击后会push到NextPage页面,通过DevTools可以看到,虽然是通过PartManager的操作push过来,但是在tree上面,两者没有任何关系,也不能在此页面调用Provider.of<Counter>(context, listen: false)这类方法,切记切记!

场景3:TabBarController持有A,B,C, D四个页面,D页面做了一个操作,希望A页面的状态进行更改。

  • iOS端: 这是典型的跨页面数据传递,由于A页和D页面没有关联,无法使用callback或者delegate,使用Notification进行通知的发与收,来进行实现。

  • Flutter端: 这个可以使用EventBus来实现,实现原理与iOS的Notification方式类似,通过总线进行数据的发与收,达到改变数据,进而改变页面。 我们也可以考虑在顶层的MaterialApp之前包裹Provider组件并注册相应的变量组件并注册相应的变量,以达到任何页面都可以获取并改变该变量的机会,达到全局管理的目的。

class ExampleWithGlobalManager extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: HomePage(),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  HomePage({Key key}) : super(key: key);

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

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 4,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Global Manager"),
          bottom: TabBar(
            tabs: [Text("A"), Text("B"), Text("C"), Text("D")],
          ),
        ),
        body: TabBarView(
          children: [
            PageA(),
            PageB(),
            PageC(),
            PageD(),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Consumer<Counter>(
          builder: (context, value, child) => Text(
            'count:${value.count}',
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text("B"),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Text("C"),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          AddButton(),
          SubtractButton(),
        ],
      ),
    );
  }
}


/// 下面是模型与组件代码
class Counter extends ChangeNotifier {
  int _count = 1;

  int get count => _count;

  set count(int value) {
    _count = value;
    notifyListeners();
  }
}

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

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      color: Colors.blue,
      child: Text('count++'),
      onPressed: () {
        Provider.of<Counter>(context, listen: false).count++;
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      color: Colors.blue,
      child: Text('count--'),
      onPressed: () {
        Provider.of<Counter>(context, listen: false).count--;
      },
    );
  }
}

RPReplay_Final1615434940.2021-03-11 12_04_35.gif

由于gif帧率问题,实际操作中,count++与count--按钮都是按了很多下的。

全局管理,说明ChangeNotifierProvider立于所有Widget之上,基本上在整个tree的顶部,我们从DevTools就可以看出:

image.png

试想一下,如何全局更换主题颜色,App主语言?很多Flutter的Demo也有方案。典型的就是在MaterialApp组件上包裹MultiProvider,注册需要全局控制的变量即可,很多Flutter的Demo中也使用了这类方案。

总结

到此,从ValueListenableBuilder的单页面管理,到使用Provider进行单页面管理,最后展开到Provider的全局管理,在我理解范围内基本上已经完成。

在我刚入门Provider的时候,我觉得非常神秘,甚至是非常可怕,因为这货并不是App端开发的思维模式,更多的是前端类似Flux、Redux、Vuex这种思维的传承。

传统的App端开发,我们更多是通过callback或者发消息的机制进行跨页面、多页面的数据更新与操作,而在这个App端向大前端进化过程中(iOS中的SwiftUI、Android的ComposeUI、跨平台方案Flutter),这些框架不约而同的使用了声明式、响应式编程,可谓是盼星星盼月亮,也是大势所趋吧。

总之,如果可以减轻开发复杂度,在写代码的时候少掉些头发,何乐而不为呢?

本文正在参与「掘金 2021 春招闯关活动」, 点击查看活动详情