美文网首页Android补给站FlutterFlutter圈子
从零开始的Flutter之旅: StatefulWidget

从零开始的Flutter之旅: StatefulWidget

作者: 午后一小憩 | 来源:发表于2020-03-16 22:20 被阅读0次

    往期回顾

    从零开始的Flutter之旅: StatelessWidget

    在之前的文章中,我们介绍了StatelessWidget的特性与它在Flutter中的呈现原理。

    这次我们接着来聊聊它的兄弟StatefulWidget,俗称有状态小部件。

    特性

    如果你看了我之前的文章,你可能已经非常熟悉无状态小部件StatelessWidget。它们是由一个蓝图与不可变的element配置来实现的,实际安装到屏幕上的是各个StatelessElement。

    不可变的东西我是非常喜欢的,就像写代码一样,一旦定义了一个不可变的变量,我就不用再关心它之后的所有事情,因为它不可变的性质,致使它不会发生不可预期的问题,只需直接使用它即可。

    但一个程序只有不可变的配置是不行的,我们不可能编写一个只绘制一次后就停止的应用。因为一旦数据改变,不可变的配置是不可能帮助我们刷新ui,达到我们预期的效果;而有状态小部件StatefulWidget却可以轻松解决这些事情。

    StatefulWidget提供不可变的配置信息以及可以随着时间变化而触发的状态对象;通过监听状态的变化来达到ui的更新。

    简单点,我们从flutter_github挑选一个实例。

    当我们点击其中一个未读通知信息时,我们需要将其ui状态变成已读的样式。根据状态来改变ui,StatefulWidget能够很好的实现这种场景。来看一下其实现

    class NotificationTabPage extends BasePage<_NotificationPageState> {
      const NotificationTabPage();
     
      @override
      _NotificationPageState createBaseState() => _NotificationPageState();
    }
     
    class _NotificationPageState
        extends BaseState<NotificationVM, NotificationTabPage> {
      @override
      NotificationVM createVM() => NotificationVM(context);
     
      @override
      Widget createContentWidget() {
        return RefreshIndicator(
          onRefresh: vm.handlerRefresh,
          child: Scrollbar(
            child: ListView.builder(
              itemCount: vm.notifications?.length ?? 0,
              itemBuilder: (BuildContext context, int index) {
                final NotificationModel item = vm.notifications[index];
                return GestureDetector(
                  onTap: () {
                    vm.contentTap(index);
                  },
                  child: Container(
                    color: item.unread ? Colors.white : Color.fromARGB(13, 0, 0, 0),
                    padding: EdgeInsets.only(left: 15.0, top: 10.0, right: 15.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          item.repository.fullName,
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 16.0,
                            color: item.unread
                                ? Colors.black87
                                : Color.fromARGB(255, 102, 102, 102),
                          ),
                        ),
                        Row(
                          children: <Widget>[
                            Padding(
                              padding: EdgeInsets.only(top: 5.0),
                              child: Image.asset(
                                vm.getTypeFlagSrc(item.subject.type),
                                width: 18.0,
                                height: 18.0,
                              ),
                            ),
                            Expanded(
                              child: Padding(
                                padding: EdgeInsets.only(top: 5.0, left: 10.0),
                                child: Text(
                                  item.subject.title,
                                  overflow: TextOverflow.ellipsis,
                                  maxLines: 1,
                                  style: TextStyle(
                                    fontSize: 14.0,
                                    color: item.unread
                                        ? Color.fromARGB(255, 17, 17, 17)
                                        : Color.fromARGB(255, 102, 102, 102),
                                  ),
                                ),
                              ),
                            ),
                          ],
                        ),
                        Padding(
                          padding: EdgeInsets.only(top: 10.0),
                          child: Divider(
                            height: 1.0,
                            endIndent: 0.0,
                            color: Color.fromARGB(255, 207, 216, 220),
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        );
      }
    }
    

    这里的BasePage是MSVM架构中的基类,它继承于StatefulWidget;_NotificationPageState也是一样,它继承于State

    abstract class BasePage<S extends BaseState>
        extends StatefulWidget {
        ...
    }
     
    abstract class BaseState<VM extends BaseVM, T extends StatefulWidget>
        extends State implements VMSContract {
        ...
    }
    

    关于MSVM后续会专门开文章介绍,想了解的可以期待一下

    我们来看createContentWidget方法中的布局,找到上述情况关联的ui,在ListView的item中。

    item布局的状态是根据item.unread来判断的,未读状态为ture。

    当用户onTap点击时,将会向服务器发送thread阅读请求,当请求成功之后,再将相应位置的item.unread值改为false。

    但就这样改变你会发现ui是不会刷新的,因为在StatefulWidget,如果你想改变某个值,同时要同步更新ui,需要使用setState方法。

      _markThreadRead(int index) async {
        try {
          Response response =
              await dio.patch('/notifications/threads/${_notifications[index].id}');
          if (response != null &&
              response.statusCode >= 200 &&
              response.statusCode < 300) {
            _notifications[index].unread = false;
            notifyStateChanged();
          }
        } on DioError catch (e) {
          Toast.show('makThreadRead error: ${e.message}', context);
        }
      }
    

    这里将setState方法封装到notifyStateChanged方法中。所以现在再回过去看ui,会发现ui已经刷新了。

    以上是使用StatefulWidget来达到ui的动态改变。再对比于之前的StatelessWidget,它们之间的区别显而易见了。

    呈现原理

    与StatelessWidget一样,接下来看下StatefulWidget的呈现原理。

    StatefulWidget也是继承于Widget,所以它的内部也是存在createElement方法。本质也是通过createElement来创建对应的Element Tree,只不过创建的是StatefulElement;然后再调用对应的Widget Tree中的build方法来获取相应的蓝图。

    但与StatelessWidget所不同的是,它还有另外一个方法

      @protected
      State createState();
    

    通过createState来创建对应的State。StatefulWidget保留了StatelessWidget的特性,即保证final数据的不变性,而对于非final可变数据,将通过Stete进行管理。

    上面是之前StatelessWidget呈现原理图,下面来对照看下StatefulWidget的。

    除了Widget Tree与Element Tree,还有对应的State,它管理着可变的数据,例如item.unread。

    一旦item.unread改变了,且通知到State,State将会再下一帧重新要求Widget Tree进行刷新。重新构建一个Container

    由于是同一种类型Container,将会直接被替换,同时使用更新后的item.unread,所以对应的Container的color也将发生改变。最终呈现的是布局的刷新。

    值得一提的是,State依附于Element Tree中,所以它的生命周期非常长,即使Widget Tree中的NotificationTabPage被移除重建,只要保证重建的类型是一致的,同时Widget Tree 与Element Tree的对应位置是没有变化的,那么Widget可以避免重建,只是会将其标记为脏状态,然后它的子widget将会通过build方法进行重建,替换State中的变化的值。

    如果你要监听Widget的变化,可以重写didUpdateWidget

      @override
      void didUpdateWidget(StatefulWidget oldWidget) {
        // TODO: implement didUpdateWidget
        super.didUpdateWidget(oldWidget);
      }
    

    综上所述,StatefulWidget使你可以随时跟踪数据的变化并更新应用的ui。但你深入Flutter之后,你会发现自己写的更多的是StatelessWidget,因为需要用到的StatefulWidget基本上已经实现了,我们更多的是对StatelessWidget的封装,是不是很有意思呢,期待你的加入。

    文中的代码都是来自于flutter_github,这是一个基于Flutter的Github客户端同时支持Android与IOS,支持账户密码与认证登陆。使用dart语言进行开发,项目架构是基于Model/State/ViewModel的MSVM;使用Navigator进行页面的跳转;网络框架使用了dio。项目正在持续更新中,感兴趣的可以关注一下。

    当然如果你想了解Android原生,相信flutter_github的纯Android版本AwesomeGithub是一个不错的选择。

    下期预告

    从零开始的Flutter之旅: InheritWidget

    如果你喜欢我的文章模式,或者对我接下来的文章感兴趣,可以点击一下我的头像进行关注,当然您也可以关注我微信公众号:【Android补给站】

    或者扫描下方二维码,与我建立有效的沟通,同时更快更准的收到我的更新推送。

    相关文章

      网友评论

        本文标题:从零开始的Flutter之旅: StatefulWidget

        本文链接:https://www.haomeiwen.com/subject/rgfaehtx.html