首要感谢原作者的无私奉献:kaina404/FlutterDouBan,为我学习 Flutter 开发技能的路上亮起了明灯!

 FlutterPractise 这儿是一个依据作者原始项目整理的一个 Flutter 项目小实践,方便 Flutter 初学者增强自己的 Flutter 开发技能熟练度,加深对 Flutter 中各种 Widget 的了解和运用。

 因为豆瓣的束缚,项目中的一切接口现已无法正常恳求数据,即便原始项目中做了一个开关读取本地 JSON 数据,可是并不彻底,所以导致很多作者精心编写的 UI 页面无法展现出来,所以我跟着作者的源代码一步一步整理了原始项目中的每一个页面,使每个页面都读取到本地的 JSON 数据,然后解析数据使页面正常展现出来,这样方便咱们观看页面作用进行学习。

 再次感谢原作者的无私奉献,下面我会以一个 Flutter 初学者的视角顺着 App 的各个页面整理整个项目,而且对相关功用和页面牵涉到的 Flutter 开发根底点进行打开学习。

一、如安在 Flutter 中操控 Widget 的显现与躲藏:Offstage 的运用

Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践

 App 发动首要映入咱们眼帘的是 5 秒倒计时的发动屏,倒计时完毕后呈现出 App 的首页。原始项目中作者用了两个 Offstage 分别显现发动屏页面和 App 内容页面,这儿其实能够牵涉出一个 App 开发中比较常见的场景,那就是咱们操控显现和躲藏一个页面的方式,在 iOS 中咱们比较常用的是 hiddenalpha 特点来操控 UIView 的显现与躲藏,或许调整视图的束缚或 frame,让它脱离当时位置或 size 为 0。不过此刻即就是躲藏状况的 UIView 也并没有从其父视图中移除,因为下一秒咱们或许就需求把它显现出来了。那这儿和发动屏的场景并不匹配,咱们都知道发动屏在 App 的本次生命周期中仅显现一次,发动过后咱们就能够把它销毁了。所以这儿我对发动屏和 App 内容页面进行了改造,封装发动屏在倒计时完毕后回调显现 App 内容页面。这儿我增加了两个文件:root_widget.dart 和 splash_new_widget.dart,其间 RootWidget 依据一个 _showAD 标识判断显现 SplashNewWidget,当 5 秒倒计时完毕回调中,在 setState 中修改 _showAD 的值,把 ContainerPage 显现出来。

import 'package:flutter/material.dart';
import 'package:flutter_official_project/pages/container_page.dart';
import 'package:flutter_official_project/pages/splash/splash_new_widget.dart';
class RootWidget extends StatefulWidget {
  const RootWidget({super.key});
  @override
  State<RootWidget> createState() => _RootWidgetState();
}
class _RootWidgetState extends State<RootWidget> {
  bool _showAD = true;
  var container = const ContainerPage();
  @override
  Widget build(BuildContext context) {
    return _showAD ? SplashNewWidget(onCountDownFinishCallBack: (bool value) {
    if (value) {
      setState(() {
        _showAD = false;
      });
    }
  }) : container;
  }
}

 下面咱们拓宽一下 Offstage 和 Visibility 相关的知识点。

 咱们能够先看一下这篇文章来了解: flutter学习之widget的显现和躲藏。

 这儿是 Offstage 的官方文档:Offstage class,比照 iOS 中 UIView 的 hidden 特点,Offstage 的最大差异在于当 Offstage 的 offstage 特点值为 true 时,它的 child 是不显现的,且它不再占用 parent Widget 的任何空间,就像它彻底不存在相同,可是 child 依然是坚持 active 的依然坚持着 child 的 state 的(依然占用着内存和资源),它能 receive focus 或许键盘输入。虽然它在屏幕上消失了咱们不能点击到它,可是例如它包括一个输入框而且在消失前现已获得焦点,那么咱们依然能够点击键盘向其间输入内容。

 学习 Offstage 时咱们其实能够比照 Visibility 来学习,Visibility 能对躲藏态的 child 做更详尽的操控,它有如下特点,咱们简略过一下:

  • final Widget child;: 要显现或躲藏的 widget,由 visible 操控。
  • final bool visible;: 在显现 child 或躲藏它之间切换。无论 visible 特点的状况如何,maintain flags 都应设置为相同的值,不然它们将无法正常运转(详细而言,每当任何 maintain flags 产生更改时,无论 maintainState 的状况如何,state 都将丢掉,因为这样做将导致 subtree shape 更改)。除非设置了 maintainState,不然 child 将在躲藏时被开释(从树中删去)。这一段的信息有点多,咱们需求理解一下,首要 Visibility 有一组 bool 类型的 maintain flags 的特点:maintainState(坚持状况)、maintainAnimation(坚持动画)、maintainSize(坚持巨细)、maintainSemantics(坚持语义)、maintainInteractivity(坚持交互),它们的值应该一致。另外,假如 maintainState 的值为 false,那么当 visible 变化为 true 时,child 会躲藏,此刻不单单是躲藏,child 会被开释,即它的 dispose 函数会被调用,child 的一切状况也都会丢掉。
  • final bool maintainState;: 是否在 child 子树不行见时坚持其 State 目标。坚持 child 的 state 或许价值昂扬(因为这意味着一切目标仍在内存中,它们的资源不会开释)。仅当无法按需从头创立它时,才应保护它。何时保护 state 的一个示例是 child 是否包括 Navigator,因为该 widget 保护无法动态从头创立的杂乱 state。假如此特点为 false,则 maintainAnimation 也有必要为 false。动态更改此值或许会导致 child 的当时状况丢掉(假如 visible 为 true,则会当即创立具有新 State 目标的子树的新实例)。假如此特点为 true,则运用 Offstage widget 躲藏 child,而不是将其替换为 replacement。
  • final Widget replacement;: 当 child 不行见时运用的 widget,假设未设置任何 maintain flags(特别是 maintainState,即它们的值都为 false)。此特点的默认值行为是将 widget 替换为 zero box(this.replacement = const SizedBox.shrink())。
  • final bool maintainAnimation;: 是否在 child 不行见时坚持动画。要设置此项,还有必要设置 maintainState。当 child 不行见时坚持动画处于活动状况比仅保护状况更贵重。这或许有用的一个示例是,假如子树运用 AnimationController 及时对其布局进行动画处理,而且该布局的效果用于影响其他一些逻辑。假如此标志为 false,则当可见标志为 false 时,child 中托管的任何 AnimationController 都将 muted。假如此特点为 true,则不运用 TickerMode widget。假如此特点为 false,则 maintainSize 也有必要为 false。动态更改此值或许会导致 child 的当时状况丢掉(假如 visible 为 true,则会当即创立具有新 State 目标的 child 的新实例)。
  • final bool maintainSize;: 当 child 不行见时是否为 child 原本存在的位置保存空间。要设置此项,还有必要设置 maintainAnimation 和 maintainState。在 child 不行见时坚持巨细并不比仅坚持动画运转而不坚持巨细贵重得多,而且在某些情况下,假如子树很简略而且经常切换可见特点,则在某些情况下或许会略微便宜一些,因为它防止在切换可见特点时触发布局更改。假如子树不是微不足道的,那么甚至不保存状况会便宜得多。假如此特点为 true,则可运用 Opacity widget 代替而不是 Offstage widget。(Opacity 的 child 躲藏时依然保存 child 所占有的布局空间)假如此特点为 false,则 maintainSemantics 和 maintainInteractivity 也有必要为 false。动态更改此值或许会导致 child 的当时状况丢掉(假如可见为 true,则会当即创立具有新 State 目标的 child 的新实例)。
  • final bool maintainSemantics;: 是否在躲藏 child 时保护其语义(例如,为了 accessibility)。要设置此值,还有必要设置 maintainSize。默认情况下,将 maintainSemantics 设置为 false 时,当 child 对用户躲藏时,accessibility tools 不行见。假如此标志设置为 true,则 accessibility tools 将陈述 widget,就好像它存在相同。动态更改此值或许会导致 child 的当时状况丢掉(假如可见为 true,则会当即创立具有新 State 目标的 child 的新实例)。
  • final bool maintainInteractivity;: 是否允许 child 在躲藏时具有交互性。要设置此设置,还有必要设置 maintainSize。默认情况下,将 maintainInteractivity 设置为 false 时,接触事情在对用户躲藏时无法到达子项。假如此标志设置为 true,则仍会传递接触事情。动态更改此值或许会导致 child 的当时状况丢掉(假如可见为 true,则会当即创立具有新 State 目标的 child 的新实例)。

 至此 Visibility 的特点就看完了,因为它牵涉的内容比较多关于 Widget 的躲藏与显现时的布局空间占有、交互的呼应、动画的坚持、State 的坚持、热重载时 child 的重建等等,所以咱们花的时间有点多。

 咱们简略过一下 ContainerPage 中的内容,咱们能够把它比照为 iOS 中的咱们自己封装的 TabBarController,在里边安排屏幕底部的一排 TabBarItem 以及选中其间某个时显现它们对应的页面,这儿运用了 5 个 Offstage,一个 TabBarItem 对应一个 Offstage,当选中某个 TabBarItem 时就使指定的 Offstage 显现它的 child。运用 Offstage 保证当 child 躲藏时 state 还能坚持,widget 也不会被销毁。

 下面咱们进入 HomePage 的学习,进入之前呢咱们要弥补一下 Flutter 中翻滚视图相关的知识点,它太重要了!

二、如安在 Flutter 中构建翻滚视图:NestedScrollView 的运用

 HomePage 是一个十分经典的 App 页面,顶部是一个查找框,下面是一个翻滚列表,包括两种款式的 cell,一种是并排三张图片,一种是一个视频播映器。关于这个滑动列表咱们用到了一个比较杂乱的 Widget: NestedScrollView。

 NestedScrollView class 官方文档。

 NestedScrollView 是一个翻滚视图,其间能够嵌套其他翻滚视图,其翻滚位置本质上是具有 intrinsically linked(内在联系)。

 此 widget 最常见的用例是具有灵敏的 SliverAppBar 的可翻滚视图,该视图在 header 中包括 TabBar(由 headerSliverBuilder 构建,并在 body 中具有 TabBarView,因此可翻滚视图的内容会依据可见的 Tab 而有所不同。(即下面第一个示例中的翻滚页面)

 在一般的 ScrollView 中,有一组 slivers(翻滚视图的 components)。假如其间一个 sliver 托管了一个以相反方向翻滚的 TabBarView(例如,允许用户在 tabs 表示的页面之间水平滑动切换,而列表笔直滑动),则该 TabBarView 内的任何列表都不会与外部 ScrollView 交互。例如,滑动内部列表翻滚到顶部后并不会导致外部 ScrollView 中折叠的 SliverAppBar 打开。(这儿的意思即比方咱们在 iOS 中屏幕上下半部分各放一个能够上下滑动的 UIScrollView,当下面的 UIScrollView 滑动到顶部时它自身会有一个果冻作用,而不是促使上半部分的 UIScrollView 跟着一同向下滑动,而 Flutter 能够通过一些办法,让两个滑动视图进行联动)

 NestedScrollView 通过为外部 ScrollView 和内部 ScrollViews(TabBarView 内部的那些,将它们 hooking 在一同,以便它们向用户显现为一个连贯的翻滚视图)提供自界说 ScrollController 来解决此问题。

 下面咱们看一个实例:此示例显现一个 NestedScrollView,其 header 是 SliverAppBar 中的 TabBar 的组合,其 body 是 TabBarView。它运用 SliverOverlapAbsorber/SliverOverlapInjector 对,使内部列表正确对齐,并运用 SafeArea 来防止任何水平搅扰(例如,iOS 顶部的刘海底部的下巴安全区。咱们运用 SafeArea widget 并给它的 bottom 特点置为 true,那么内部的翻滚列表就会把屏幕底部的安全区留白,无法延伸到屏幕底部,置为 false 便能够延伸到屏幕最底部,这个大约就是 iOS 开发中的翻滚列表适配手机下巴,想必 iOS 开发者是比较了解的。)。此外,PageStorageKeys 用于记住每个 tab’s list 的翻滚位置。

 这儿是运转截图,这个大约在 android 开发或许很多 iOS App 都特别常见到的页面布局。在 Flutter 大约是这样的:首要顶部的 TabBar 包括一组水平排布的 Tab 标签,它们能够横向左右滑动,然后是每个 Tab 标签和下面的 TabBarView 的一个 child 对应绑定的,TabBar 中 Tab 的个数与 TabBarView 的 children 的个数彻底一致且一一对应,咱们点击哪个 Tab 标签就左右切换到 TabBarView 的哪个 child,然后咱们也能够在屏幕的下半部分左右滑动切换 TabBarView 的 child,然后顶部的 Tab 标签也会跟着一同左右滑动切换。(这一套布局在 iOS 下的话要开发者彻底自己手动开发,上下搭配自己做滑动回调处理,iOS 大约只提供了转头,需求开发者自己手动垒墙…)

Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践

 下面是代码完结,初看咱们或许有点懵,可是就这 60 行代码便完结了一个在 iOS 原生开发中比较 “杂乱” 的翻滚视图,开发功率还是比较高的。这段代码咱们大约需求 “全文背诵”,这个是 Flutter 中最根本的翻滚列表完结。

class MyStatelessWidget extends StatelessWidget {
  const MyStatelessWidget({super.key});
  @override
  Widget build(BuildContext context) {
    // 顶部 Tab 的标题字符串
    final List<String> tabs = <String>[
      'Tab 1',
      'Tab 2',
      'Tab 3',
      'Tab 4',
      'Tab 5',
      'Tab 6',
      'Tab 7',
      'Tab 8',
      'Tab 9'
    ];
    // DefaultTabController 是一个 inherited widget,用于与 TabBar 或 TabBarView 同享 TabController。
    // 当同享显式创立的 TabController 不方便时运用它,因为 tab bar widgets 是由无状况 parent widgth 或不同的 parent widget 创立的。
    return DefaultTabController(
      // 一般大于 1,且有必要与 [TabBar.tabs] 和 [TabBarView.children] 的长度相等。
      length: tabs.length,
      child: Scaffold(
        // Scaffold body,这儿就是一个完好的 Neste dScrollView 嵌套翻滚视图 
        body: NestedScrollView(
          // 用于构建 NestedScrollView 的 header 的 builder。一般,这用于创立带有 [TabBar] 的 [SliverAppBar]。
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            debugPrint(" DefaultTabController NestedScrollView 中的:$context");
            return <Widget>[
              // SliverOverlapAbsorber 与 body 中的 SliverOverlapInjector 对应
              SliverOverlapAbsorber(
                handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), // 记录 absorbed overlap(吸收堆叠)的目标
                // 在 [CustomScrollView] 中运用的 app bar
                sliver: SliverAppBar(
                  title: const Text('Books'),
                  // 是否把这个 app bar 固定在 scrollview 的顶部,不然 scrollview 向上滑动时这个 app bar 跟着向上滑动
                  pinned: true,
                  expandedHeight: 150.0,
                  forceElevated: innerBoxIsScrolled,
                  // 底部就是一个 TabBar 内部是一组 Tab
                  bottom: TabBar(
                    isScrollable: true,
                    tabs: tabs.map((String name) => Tab(text: name)).toList(),
                  ),
                ),
              ),
            ];
          },
          // NestedScrollView 的 body:这儿是一个 TabBarView,然后它的每一个 child 是一个上下翻滚的列表
          body: TabBarView(
            // TabBarView 的 children 就是一组与上面 Tab 一一对应的一组 Widget
            children: tabs.map((String name) {
              // SafeArea 安全最大化的运用当时一些异形屏的手机屏幕空间
              return SafeArea(
                top: false,
                bottom: false,
                // 为了运用 context,在整个 NestedScrollView 中需求上下翻滚视图联动,所以运用 Builder 来构建 Widget,
                // 这个有点相似 SwiftUI 中的 ViewBuilder,所以咱们能够比照学习
                child: Builder(
                  builder: (BuildContext context) {
                    debugPrint(" TabBarView SafeArea 中的:$context");
                    // 即 TabBarView 的每个 child 是一个 CustomScrollView
                    return CustomScrollView(
                      key: PageStorageKey<String>(name),
                      slivers: <Widget>[
                        SliverOverlapInjector(
                          handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                        ),
                        // SliverPadding 仅仅是为了增加 padding,当不需求增加 padding 时也可直接运用 SliverFixedExtentList  
                        SliverPadding(
                          padding: const EdgeInsets.all(0.0),
                          sliver: SliverFixedExtentList(
                            itemExtent: 48.0,
                            delegate: SliverChildBuilderDelegate(
                              (BuildContext context, int index) {
                                return Container(
                                  color: Color.fromARGB(255, 174, 168, 174),
                                  child: ListTile(
                                    title: Text('Item $index'),
                                  ),
                                );
                              },
                              childCount: 30,
                            ),
                          ),
                        ),
                      ],
                    );
                  },
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}

 NestedScrollView 就暂时看到这儿,因为不是本文中心,就不过多关注了,当然它真的贼重要,里边的细节每一个 Flutter 开发者都应该彻底掌握。

 看完 NestedScrollView 的相关内容,那么咱们看 HomePage 中的布局就极其简略了,整体结构和上面的示例代码彻底相同,不同之处仅是翻滚列表咱们运用了自界说的列表项,顶部用了自己封装的 SearchTextFieldWidget widget 查找框。然后咱们下面把目光聚集在 HomePage 列表中的视频播映上和查找页面的跳转上。

三、如安在 Flutter 中播映视频:video_player 的运用

 在 HomePage 中咱们看到有一个视频播映的 cell,这儿运用了 video_player 进行视频播映。

Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践

 video_player 运用起来也超简略:

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() => runApp(const VideoApp());
/// Stateful widget to fetch and then display video content.
class VideoApp extends StatefulWidget {
  const VideoApp({Key? key}) : super(key: key);
  @override
  _VideoAppState createState() => _VideoAppState();
}
class _VideoAppState extends State<VideoApp> {
  late VideoPlayerController _controller;
  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4')
      ..initialize().then((_) {
        // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
        setState(() {});
      });
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Demo',
      home: Scaffold(
        body: Center(
          child: _controller.value.isInitialized
              ? AspectRatio(
                  aspectRatio: _controller.value.aspectRatio,
                  child: VideoPlayer(_controller),
                )
              : Container(),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              _controller.value.isPlaying
                  ? _controller.pause()
                  : _controller.play();
            });
          },
          child: Icon(
            _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
          ),
        ),
      ),
    );
  }
  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }
}

 在原始项目中这儿有一个问题,就是当视频播映 widget 滑出屏幕或许页面产生跳转,比方点击顶部查找框跳转到查找页面后,咱们需求暂停视频的播映,这儿咱们运用了另外一个 package:visibility_detector 当视频播映的 widget 不行见时咱们履行视频的暂停播映。 visibility_detector 是一个特别有用的 package,例如咱们也能检测滑动视图中的 widget 滑出屏幕的机遇。

...
    final List<Widget> children = <Widget>[
      GestureDetector(
        // 这儿咱们运用 VisibilityDetector widget,把原始的 VideoPlayer(_controller) 作为其 child 运用
        // child: VideoPlayer(_controller),
        child: VisibilityDetector(
            key: Key(widget.url),
            child: VideoPlayer(_controller),
            onVisibilityChanged: (visibilityInfo) {
              var visiblePercentage = visibilityInfo.visibleFraction * 100;
              debugPrint(' VisibilityDetector 打印: Widget ${visibilityInfo.key} is $visiblePercentage% visible');
              if (visiblePercentage < 100 && _controller.value.isPlaying) {
                _controller.pause();
              }
            }),
        onTap: () {
          // 点击躲藏或许显现视频暂停播映按钮以及视频进度条
          setState(() {
            _showSeekBar = !_showSeekBar;
          });
        },
      ),
      // 视频播映操控按钮等 Widget
      getPlayController(),
    ];
...

 咱们看一下 visibility_detector 的简介:

 VisibilityDetector widget 包装现有的 Flutter widget,并在 widget 的可见性改变时触发回调。(它实际上陈述 VisibilityDetector 自身的可见性何时更改,而且其可见性应与其 child 的可见性相同。

 回调不会在可见性更改时当即触发。相反,回调被推迟和合并,以便每个 VisibilityDetector 的回调将在每个 VisibilityDetectorController.updateInterval 调用时调用一次(除非通过 VisibilityDetectorController.notifyNow() 强制调用)。一切 VisibilityDetector widget 的回调在帧之间同步触发。VisibilityDetectorController.notifyNow() 可用于强制触发挂起的可见性回调,例如切换视图或退出应用程序时,这或许是可取的。

 visibility_detector 的一些束缚:

  • VisibilityDetector 仅考虑 widget 的鸿沟框。它不考虑 widget 的 opacity。
  • 可见性回调中的 visibleFraction(可见分数)或许无法解释堆叠的 widget,这些 widget 会掩盖 VisbilityDetector。

四、如安在 Flutter 中操控页面跳转:Navigator 的运用

 在 HomePage 中点击顶部的查找框,页面产生了跳转,在点击事情中履行了:

MyRouter.push(context, MyRouter.searchPage, '我是传递到查找页面中的参数');
class MyRouter {
...
  MyRouter.push(BuildContext context, String url, dynamic params) {
    Navigator.push(context, MaterialPageRoute(builder: (context) {
      // 这儿依据 params 构建一个指定的页面 widget
      return _getPage(url, params);
    }));
  }
}

 MyRouter 是界说的一个便于做页面跳转的 class,它的内部很简略,首要依据传入的跳转 url 然后跳转到指定的页面。其间最重要的 push 函数,用到了 Navigator,它是 Flutter 中做页面跳转的核心 widget。Navigator 运用 stack 规矩办理一组 child widget 的 widget。许多应用程序在其 widget 层次结构的顶部邻近都有一个 navigator,以便运用 Overlay 显现其逻辑历史记录,其间最近拜访的页面直观地坐落旧页面的顶部。运用此模式,navigator 能够通过在 overlay 中移动 widget 来直观地从一个页面过渡到另一个页面。同样,navigator 可用于通过将 dialog widget 定位在当时页面上方来显现 dialog。

 然后是 Navigatior 的 push 函数,将给定 route(路由)push 到最严密地围住给定 context 的 navigator 上(Navigator.of(context))。新 route 和以前的 route(假如有)会收到告诉(Route.didPush 和 Route.didChangeNext)。假如 Navigator 有任何 Navigator.observers,他们也会收到告诉(NavigatorObserver.didPush)。push 新 route 时,当时 route 中的 ongoing gestures(继续手势)将被撤销。T 类型参数是 route 回来值的类型。回来一个 Future,该 Future 完结到从 navigator 弹出 push 的 route 时传递给 pop 的效果值。

...
  @optionalTypeArgs
  static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
  }
...

 push 的典型用法:

void _openMyPage() {
  Navigator.push<void>(
    context,
    MaterialPageRoute<void>(
      builder: (BuildContext context) => const MyPage(),
    ),
  );
}

 然后下面是一系列的:书影音、小组中的嵌套翻滚视图布局,和首页翻滚视图的运用相似,这儿便不再深入。

Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践

五、如安在 Flutter 中加载网页:flutter_webview_plugin 的运用

 在 ShopPageWidget 中展现的是网页的加载,这儿运用一个 package:flutter_webview_plugin。

Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践

 对应的网页加载代码:

class WebViewPage extends StatelessWidget {
  final String url;
  final dynamic params;
  // ignore: constant_identifier_names
  static const String TITLE = 'title';
  const WebViewPage(this.url, this.params, {super.key});
  @override
  Widget build(BuildContext context) {
    return WebviewScaffold(
      url: url,
      appBar: AppBar(
        title: Text(params[TITLE]),
        backgroundColor: Colors.green,
      ),
    );
  }
}

 或许运用 FlutterWebviewPlugin 用于变换加载不同网页的 URL。

// 提供链接到一个仅有 WebView 的单例实例,以便你能够从应用程序的任何位置操控 webview
final _webviewReference = FlutterWebviewPlugin();
...
  @override
  Widget build(BuildContext context) {
    debugPrint('build widget.url=${widget.url}');
    return _WebviewPlaceholder(
      onRectChanged: (value) {
        if (_rect == null || _closed) {
          if (_rect != value) {
            _rect = value;
          }
          RenderBox? renderBox = context.findRenderObject() as RenderBox;
          double left = 0;
          double top = renderBox.localToGlobal(Offset.zero).dy;
          double width = renderBox.size.width;
          // 这儿 34 是针对苹果刘海屏系列,底部安全区高度是 34
          double height = ScreenUtils.screenH(context) - top - kBottomNavigationBarHeight - 34;
          MediaQueryData mq = MediaQuery.of(context);
          double safeBottom = mq.padding.bottom;
          debugPrint(' _webviewReference.launch ${renderBox.size} ${renderBox.localToGlobal(Offset.zero)} ${ScreenUtils.screenH(context)} $kBottomNavigationBarHeight $height $safeBottom');
          Rect rect = Rect.fromLTWH(left, top, width, height);
          _webviewReference.launch(widget.url, withJavascript: true, withLocalStorage: true, scrollBar: true, rect: rect);
        } else {
          if (_rect != value) {
            _rect = value;
          }
          _webviewReference.reloadUrl(widget.url);
        }
      },
      child: const Center(
        // 环形菊花加载器
        child: CircularProgressIndicator(),
      ),
    );
  }
...

六、如安在 Flutter 中不同的页面切换状况栏主题:SystemUiOverlayStyle 的运用

 在书影音、小组的列表中点击任何一个电影都会跳转到一个电影概况页面 DetailPage,这儿的电影概况数据都运用了本地的 json 数据。

 这儿咱们注意到在前一个页面是黑色的状况栏,然后在 DetailPage 页面运用了白色的状况栏,这儿进程也比较简略,首要咱们用一个 _lastStyle 变量在 initState 中记录上一个页面的状况栏色彩,然后咱们便能够随意调用 SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light); 设置当时页面的状况栏色彩。然后咱们在 dispose 中调用:SystemChrome.setSystemUIOverlayStyle(_lastStyle!); 康复前一个页面的状况栏色彩。

class _DetailPageState extends State<DetailPage> {
...
  // 记录上一个页面的状况栏色彩
  SystemUiOverlayStyle? _lastStyle;
  @override
  void initState() {
    // 获取上一个页面的状况栏色彩
    _lastStyle = SystemChrome.latestStyle;
    // 设置当时页面状况栏色彩为白色
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
    super.initState();
  }
  @override
  void dispose() {
    // 将状况栏设置为之前的色彩
    if (_lastStyle != null) {
      SystemChrome.setSystemUIOverlayStyle(_lastStyle!);
    }
    super.dispose();
  }

 然后是 DetailPage 中的其他布局,在库房代码中都有详细注释,这儿就不在打开了。

Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践
Flutter 学习过程中不容错过的项目实践

 作为 Flutter 学习的一个阶段性学习效果就先到这儿了,库房有完好可直接运转的项目供咱们参阅。

参阅链接

参阅链接:

  • kaina404/FlutterDouBan
  • chm994483868/FlutterPractise