├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── flutter_export_environment.sh │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── main.m ├── lib │ ├── detail_page.dart │ ├── home_page.dart │ ├── main.dart │ ├── pageview_in_tabview_page.dart │ ├── pageview_mixin_page.dart │ ├── pageview_wrapper_page.dart │ ├── popup_page.dart │ └── tabview_page.dart ├── pubspec.lock ├── pubspec.yaml ├── test │ └── widget_test.dart └── webp │ └── sample.webp ├── gifs ├── 1PageRoute.gif ├── 2PopupRoute.gif ├── 3PageView.gif ├── 3PageView.gif.zip ├── 4TabView.gif ├── 5PageViewInTabView.gif └── demo.gif ├── lib ├── flutter_page_tracker.dart └── src │ ├── page_load_mixin.dart │ ├── page_load_provider.dart │ ├── page_tracker_aware.dart │ ├── page_view_listener_mixin.dart │ ├── page_view_wrapper.dart │ ├── tracker_dialog_wrapper.dart │ ├── tracker_page_mixin.dart │ ├── tracker_page_widget.dart │ ├── tracker_route_observer.dart │ └── tracker_route_observer_provider.dart ├── publish.sh ├── pubspec.lock ├── pubspec.yaml ├── tabview_event.jpg ├── test └── xm_flutter_tracker_test.dart └── 管理TabView的事件.graffle /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - first publish 2 | ## [1.0.0] - add doc 3 | ## [1.0.1] - 完善文档 4 | ## [1.0.2] - 修正文档中的错误 5 | ## [1.0.3] - 添加图片 6 | ## [1.0.4] - 添加图片 7 | ## [1.1.0] - 支持在TabView中嵌套PageView 8 | ## [1.1.1] - - 修改provider版本到3.2.0 9 | ## [1.2.0] - 支持弹窗埋点 10 | ## [1.2.1] - bugfix: 弹窗埋点可以不传didPageView/didPageExit 11 | ## [1.2.2] - 升级demo和文档 12 | ## [1.2.3] - 更具pub.dev的提示修改文档和代码 13 | ## [2.0.0] - 去除对provider的依赖,并支持最新版本flutter1.9.1 14 | ## [2.0.1] - 修改文档,增加flutter_sliver_tracker的外链 15 | ## [2.0.2] - 修改文档,增加flutter_sliver_tracker的图片 16 | ## [2.1.0] - PageView/TabView推荐直接使用mixin,但是StatelessWidget只能使用Wrapper 17 | ## [2.1.1] - bugfix: 使用PageViewListenerMixin上的of发放 18 | ## [2.1.2] - bugfix: tab曝光埋点使用asBroadcastStream,支持多次事件绑定 19 | ## [2.1.3] - bugfix: 控制数据的范围 20 | ## [2.1.4] - bugfix: 在生产环境不抛出异常 21 | ## [2.1.5] - readme: 添加文档,绑定RouteObserver 22 | ## [2.1.6] - readme: 弹窗埋点demo修改 23 | ## [2.1.7] - bugfix: PageView组件,应该在首次注册回调事件的时候触发首次页面曝光。这样即使PageView组件随着焦点离开被销毁,也能发页面曝光事件 24 | ## [2.1.8] - bugfix: 在dispose中捕获异常 25 | ## [2.2.0] - feature: 统计页面加载时长 26 | ## [2.2.1] - bugfix: rebuildStartTime 27 | ## [2.2.2] - 优化:较少统计页面加载时长对业务代码的入侵 28 | ## [3.0.0] - 优化:分别计算三段加载时间,通过provider提供环境配置 29 | ## [3.0.1] - 优化:较少统计页面加载时长对业务代码的入侵 30 | ## [3.0.2] - 优化:fluttertoast升级 31 | ## [3.0.3] - 优化:页面加载时长统计精度 32 | ## [3.0.4] - 优化:判断当前路由是否是被浏览的路由 33 | ## [3.0.5] - bugfix: toast 34 | ## [3.0.6] - bugfix: 页面加载时长统计 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, DavidTang 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_page_tracker 2 | 3 | ## 简介 4 | FlutterPageTracker 是一个易用的 Flutter 应用页面事件埋点插件。它不仅支持在普通导航事件中监听页面曝光和离开,也支持弹窗的曝光和离开。 5 | 6 | 针对 TabView(PageView)形式的首页,FlutterPageTracker 可以监听每一个Tab的曝光和离开,并且把Tab形式的页面和普通页面衔接起来。 7 | 8 | 针对TabView和PageView组件相互嵌套的情况,FlutterPageTracker 可以对每一级嵌套分别监控埋点事件,大大提升埋点的效率。 9 | 10 | 它具有以下特性: 11 | 12 | - 1.监听普通页面的`露出`和`离开`事件(PageRoute), 13 | - 当前页面入栈会触发当前页面的`曝光事件`和前一个页面的`离开事件`。 14 | - 当前页面出栈会触发当前页面的`离开事件`和前一个页面的`曝光事件`。 15 | - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/1PageRoute.gif) 16 | - 2.监听对话框的`露出`和`离开`(PopupRoute), 17 | - 它和PageRoute的区别是,当前对话框的露出和关闭不会触发前一个页面的`露出`、`离开`事件 18 | - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/2PopupRoute.gif) 19 | - 3.监听PageView、TabView组件的`切换`事件 20 | - 当一个PageView或者TabView`入栈`时,前一个页面会触发页面`离开事件` 21 | - 当一个PageView或者TabView`出栈`时,前一个页面会触发页面`曝光事件` 22 | - 当焦点页面发生变化时,旧的页面触发页面露出,新的页面触发PageView 23 | - PageView组件 24 | - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/3PageView.gif) 25 | - TabView组件 26 | - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/4TabView.gif) 27 | - 4.PageView和TabView嵌套使用 28 | - 我们可以将这两种组件嵌套在一起使用,不限制嵌套的层次 29 | - 发生焦点变化的PageView(或者TabView)以及它的子级都会受到`曝光事件`和`离开事件` 30 | - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/5PageViewInTabView.gif) 31 | - 5.滑动曝光事件 32 | - 如果你对列表的滑动露出事件感兴趣,你可以参考flutter_sliver_tracker插件 33 | - `https://github.com/SBDavid/flutter_sliver_tracker` 34 | - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_sliver_tracker/master/demo.gif) 35 | 36 | ## 运行Demo程序 37 | 38 | - 克隆代码到本地: git clone git@github.com:SBDavid/flutter_page_tracker.git 39 | - 切换工作路径: cd flutter_page_tracker/example/ 40 | - 启动模拟器 41 | - 运行: flutter run 42 | 43 | ## 使用 44 | 45 | ### 1. 安装 46 | ```yaml 47 | dependencies: 48 | flutter_page_tracker: ^1.2.2 49 | ``` 50 | 51 | ### 2. 引入flutter_page_tracker 52 | ```dart 53 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 54 | ``` 55 | 56 | ### 3. 发送普通页面埋点事件 57 | 58 | #### 3.1 添加路由监听 59 | ```dart 60 | void main() => runApp( 61 | TrackerRouteObserverProvider( 62 | child: MyApp(), 63 | ) 64 | ); 65 | ``` 66 | 67 | #### 3.2 添加路由事件监听 68 | ```dart 69 | class MyApp extends StatelessWidget { 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | return MaterialApp( 74 | // 添加路由事件监听 75 | navigatorObservers: [TrackerRouteObserverProvider.of(context)], 76 | home: MyHomePage(title: 'Flutter_Page_tracker Demo'), 77 | ); 78 | } 79 | } 80 | ``` 81 | 82 | #### 3.3 在组件中发送埋点事件 83 | 84 | 必须使用`PageTrackerAware`和`TrackerPageMixin`这两个mixin 85 | 86 | ```dart 87 | class HomePageState extends State with PageTrackerAware, TrackerPageMixin { 88 | @override 89 | Widget build(BuildContext context) { 90 | return Container(); 91 | } 92 | 93 | @override 94 | void didPageView() { 95 | super.didPageView(); 96 | // 发送页面露出事件 97 | } 98 | 99 | @override 100 | void didPageExit() { 101 | super.didPageExit(); 102 | // 发送页面离开事件 103 | } 104 | } 105 | ``` 106 | 107 | #### 3.4 Dialog的埋点 108 | ```dart 109 | class PopupPage extends StatelessWidget { 110 | 111 | @override 112 | Widget build(BuildContext context) { 113 | return TrackerDialogWrapper( 114 | didPageView: () { 115 | print('dialog didPageView'); 116 | }, 117 | didPageExit: () { 118 | print('dialog didPageExit'); 119 | }, 120 | child: SimpleDialog( 121 | children: [ 122 | // body 123 | ], 124 | ), 125 | ); 126 | } 127 | } 128 | ``` 129 | 130 | #### 3.5 PageView发送埋点事件 131 | 132 | 在`StatefulWidget`中,推荐直接使用`PageViewListenerMixin`发送页面事件,如果是`StatelessWidget`组件则可以使用`PageViewListenerWrapper`。 133 | `PageViewListenerWrapper`内部也是使用`PageViewListenerMixin`来发送事件。 134 | 135 | ```dart 136 | 137 | // 嵌入到PageView组件中页面 138 | class Page extends StatefulWidget { 139 | final int index; 140 | 141 | const Page({Key key, this.index}): super(key: key); 142 | 143 | @override 144 | PageState createState() { 145 | return PageState(); 146 | } 147 | } 148 | 149 | class PageState extends State with PageTrackerAware, PageViewListenerMixin { 150 | 151 | int get pageViewIndex => widget.index; 152 | 153 | @override 154 | void didPageView() { 155 | super.didPageView(); 156 | // 页面曝光事件 157 | } 158 | 159 | @override 160 | void didPageExit() { 161 | super.didPageExit(); 162 | // 页面离开事件 163 | } 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | return Container(); 168 | } 169 | } 170 | 171 | // PageView组件 172 | class PageViewMixinPage extends StatefulWidget { 173 | 174 | @override 175 | PageViewMixinPageState createState() => PageViewMixinPageState(); 176 | } 177 | 178 | class PageViewMixinPageState extends State { 179 | 180 | PageController pageController; 181 | 182 | @override 183 | void initState() { 184 | super.initState(); 185 | pageController = PageController(); 186 | } 187 | 188 | @override 189 | Widget build(BuildContext context) { 190 | return PageViewWrapper( 191 | changeDelegate: PageViewChangeDelegate(pageController), 192 | pageAmount: 3, 193 | initialPage: pageController.initialPage, 194 | child: PageView( 195 | controller: pageController, 196 | children: [ 197 | Page(index: 0,), 198 | Page(index: 1,), 199 | Page(index: 3,), 200 | ], 201 | ), 202 | ); 203 | } 204 | } 205 | ``` 206 | 207 | #### 3.6 TabView发送埋点事件 208 | 209 | 在这个例子中我们只用`PageViewListenerWrapper`来发送页面事件,我们也可以向例子3.3中一样使用直接使用`PageViewListenerMixin`。 210 | 在`StatefulWidget`中,荐使用`mixin`更简洁。 211 | 212 | ```dart 213 | class TabViewPage extends StatefulWidget { 214 | TabViewPage({Key key,}) : super(key: key); 215 | 216 | @override 217 | _State createState() => _State(); 218 | } 219 | 220 | class _State extends State with TickerProviderStateMixin { 221 | TabController tabController = TabController(initialIndex: 0, length: 3, vsync: this); 222 | 223 | @override 224 | Widget build(BuildContext context) { 225 | 226 | return Scaffold( 227 | // 添加TabView的包裹层 228 | body: PageViewWrapper( 229 | // Tab页数量 230 | pageAmount: 3, 231 | // 初始Tab下标 232 | initialPage: 0, 233 | // 监听Tab onChange事件 234 | changeDelegate: TabViewChangeDelegate(tabController), 235 | child: TabBarView( 236 | controller: tabController, 237 | children: [ 238 | Builder( 239 | builder: (_) { 240 | // 监听由PageViewWrapper转发的PageView,PageExit事件 241 | return PageViewListenerWrapper( 242 | 0, 243 | onPageView: () { 244 | // 发送页面曝光事件 245 | }, 246 | onPageExit: () { 247 | // 发送页面离开事件 248 | }, 249 | child: Container(), 250 | ); 251 | }, 252 | ), 253 | // 第二个Tab 254 | // 第三个Tab 255 | ], 256 | ), 257 | ), 258 | ); 259 | } 260 | } 261 | ``` 262 | 263 | #### 3.7 TabView中嵌套PageView(PageView也可以嵌套TabView,TabView也可以嵌套TabView) 264 | 265 | 在这个例子中我们只用`PageViewListenerWrapper`来发送页面事件,我们也可以向例子3.5中一样使用直接使用`PageViewListenerMixin`。 266 | 在`StatefulWidget`中,荐使用`mixin`更简洁。 267 | 268 | ```dart 269 | class PageViewInTabViewPage extends StatefulWidget { 270 | 271 | @override 272 | _State createState() => _State(); 273 | } 274 | 275 | class _State extends State with TickerProviderStateMixin { 276 | 277 | TabController tabController; 278 | PageController pageController; 279 | 280 | @override 281 | void initState() { 282 | super.initState(); 283 | tabController = TabController(initialIndex: 0, length: 3, vsync: this); 284 | pageController = PageController(); 285 | } 286 | 287 | @override 288 | Widget build(BuildContext context) { 289 | 290 | return Scaffold( 291 | // 外层TabView 292 | body: PageViewWrapper( 293 | pageAmount: 3, // 子Tab数量 294 | initialPage: 0, // 首个展现的Tab序号 295 | changeDelegate: TabViewChangeDelegate(tabController), 296 | child: TabBarView( 297 | controller: tabController, 298 | children: [ 299 | Builder( 300 | builder: (BuildContext context) { 301 | // 转发上层的事件 302 | return PageViewListenerWrapper( 303 | 0, 304 | // 内层PageView 305 | child: PageViewWrapper( 306 | changeDelegate: PageViewChangeDelegate(pageController), 307 | pageAmount: 3, 308 | initialPage: pageController.initialPage, 309 | child: PageView( 310 | controller: pageController, 311 | children: [ 312 | PageViewListenerWrapper( 313 | 0, 314 | onPageView: () { 315 | // 页面露出事件 316 | }, 317 | onPageExit: () { 318 | // 页面离开事件 319 | }, 320 | child: Container() 321 | ), 322 | // PageView中的第二个页面 323 | // PageView中的第三个页面 324 | ], 325 | ), 326 | ) 327 | ); 328 | }, 329 | ), 330 | // tab2 331 | // tab3 332 | ], 333 | ), 334 | ) 335 | ); 336 | } 337 | } 338 | ``` 339 | 340 | ## 原理篇 341 | ### 1.概述 342 | 343 | 页面的埋点追踪通常处于业务开发的最后一环,留给埋点的开发时间通常并不充裕,但是埋点数据对于后期的产品调整有重要的意义,所以一个稳定高效的埋点框架是非常重要的。 344 | 345 | ### 2. 我们期望埋点框架所具备的功能 346 | 347 | #### 2.1 PageView,PageExit事件 348 | 我们期望当调用`Navigator.of(context).pushNamed("XXX Page");`时,首先对之前的页面发送`PageExit`,然后对当前页面发送`PageView`事件。当调用`Navigator.of(context).pop();`时则,首先发送当前页面的`PageExit`事件,再发送之前页面的`PageView`事件。 349 | 350 | 我们首先想到的是使用[RouteObserver](https://github.com/flutter/flutter/blob/c06bf6503a8b6690b2740a7101852fcc8f133057/packages/flutter/lib/src/widgets/routes.dart#L1426),但是`PageView`和`PageExit`发送的顺序相反。并且[PopupRoute](https://github.com/flutter/flutter/blob/c06bf6503a8b6690b2740a7101852fcc8f133057/packages/flutter/lib/src/widgets/routes.dart#L1510)类型的路由会影响前一个页面的埋点事件发送,例如我们入栈的顺序是 A页面 -> A页面上的弹窗 -> B页面,但是在这个过程中A页面的`PageExit`事件没有发送。 351 | 352 | 所以我们必须自己管理路由栈,这样判断不同路由的类型,并控制事件的顺序。详细实现方案在后面展开。 353 | 354 | #### 2.2 TagView组件于PageView组件 355 | 这两个组件虽然与Flutter的路由无关,但是在产品经理眼中它们任属于页面。并且当Tab发生首次曝光和切换的时候我们都需要发送埋点事件。 356 | 357 | 例如当Tab页A首次曝光时,我们首先发送上一个页面的`PageExit`事件,然后发送TabA的`PageView`事件。当我们从TabA切换到TabB的时候,先发送TabA的`PageExit`事件,然后发送TabB的`PageView`事件。当我们push一个新的路由时,需要发送TabB的`PageExit`事件。 358 | 359 | 这套流程需要Tab页和普通页面之间通过事件机制来交互,如果直接把这套机制搬到业务代码中,那么业务代码中就会包含大量与业务无关并且重复的代码。详细的抽象方案见后文。 360 | 361 | ### 3. 解决这些问题 362 | 363 | #### 3.1 解决PageView,PageExit的顺序问题 364 | [RouteObserver](https://github.com/flutter/flutter/blob/c06bf6503a8b6690b2740a7101852fcc8f133057/packages/flutter/lib/src/widgets/routes.dart#L1426)给了我们一个不错的起点,我们重写其中的`didPop`和`didPush`方法就并调整事件发送的顺序就可以解决这个问题。详见[TrackerStackObserver](https://github.com/SBDavid/flutter_tracker/blob/4fb20ad03fd63300f5b33a92fc38fcd4f7a8fa45/lib/src/tracker_route_observer.dart#L59),在`didpop`方法中我们先触发上一个路由的`PageExit`事件,然后再触发当前路由的`PageView`事件。 365 | 366 | #### 3.2 避免弹窗的干扰(例如Dialog) 367 | 在[RouteObserver.didPop(Route route, Route previousRoute)](https://github.com/flutter/flutter/blob/c06bf6503a8b6690b2740a7101852fcc8f133057/packages/flutter/lib/src/widgets/routes.dart#L1456)中,我们可以通过previousRoute找到上一个路由,并更具它来发送上一个路由的PageView事件。但是如果上一个路由是`Dialog`,就会造成错误,因为我们实际想要的是包含这个`Dialog`的路由。 368 | 369 | 要解决这个问题我们必须自己维护一个路由栈,这样当`didPop`触发时我们就可以找到真正的上一个路由。请参考这一段[代码](https://github.com/SBDavid/flutter_tracker/blob/4fb20ad03fd63300f5b33a92fc38fcd4f7a8fa45/lib/src/tracker_route_observer.dart#L36),这里的`routes`是当前的路由栈。 370 | 371 | #### 3.3 如何上报TabView中的埋点事件,并和其它页面串联起来 372 | 这个问题可以分解为两个小问题: 373 | - 1. 如何把TabView页面和普通的路由进行串联? 374 | - 2. 当Tab发生切换时如何发送埋点事件? 375 | 376 | 为了解决这两个问题,我们需要一个容器来管理tab页面的状态并且承载事件转发的任务。详见下图: 377 | ![管理TabView中的事件](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/tabview_event.jpg)。 378 | 379 | 其中TabsWrapper会监听来自Flutter的路由事件,并转发给当前曝光的Tab,这就可以解决了问题一。 380 | 381 | 同时TabsWrappe也会包含一个`TabController`和上一个被打开的Tab索引,TabsWrappe会监听来自`TabController`的onChange(index)事件,并把事件转发给对应的tab,这就解决了问题二。 382 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.example" 37 | minSdkVersion 16 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 20 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.example; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/xmly/tools/flutter-1.9.1" 4 | export "FLUTTER_APPLICATION_PATH=/Users/xmly/001-workspace/flutter_page_tracker/example" 5 | export "FLUTTER_TARGET=/Users/xmly/001-workspace/flutter_page_tracker/example/lib/main.dart" 6 | export "FLUTTER_BUILD_DIR=build" 7 | export "SYMROOT=${SOURCE_ROOT}/../build/ios" 8 | export "FLUTTER_FRAMEWORK_DIR=/Users/xmly/tools/flutter-1.9.1/bin/cache/artifacts/engine/ios" 9 | export "FLUTTER_BUILD_NAME=1.0.0" 10 | export "FLUTTER_BUILD_NUMBER=1" 11 | export "TRACK_WIDGET_CREATION=true" 12 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | pods_ary = [] 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) { |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | pods_ary.push({:name => podname, :path => podpath}); 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | } 32 | return pods_ary 33 | end 34 | 35 | target 'Runner' do 36 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 37 | # referring to absolute paths on developers' machines. 38 | system('rm -rf .symlinks') 39 | system('mkdir -p .symlinks/plugins') 40 | 41 | # Flutter Pods 42 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 43 | if generated_xcode_build_settings.empty? 44 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first." 45 | end 46 | generated_xcode_build_settings.map { |p| 47 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 48 | symlink = File.join('.symlinks', 'flutter') 49 | File.symlink(File.dirname(p[:path]), symlink) 50 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 51 | end 52 | } 53 | 54 | # Plugin Pods 55 | plugin_pods = parse_KV_file('../.flutter-plugins') 56 | plugin_pods.map { |p| 57 | symlink = File.join('.symlinks', 'plugins', p[:name]) 58 | File.symlink(p[:path], symlink) 59 | pod p[:name], :path => File.join(symlink, 'ios') 60 | } 61 | end 62 | 63 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 64 | install! 'cocoapods', :disable_input_output_paths => true 65 | 66 | post_install do |installer| 67 | installer.pods_project.targets.each do |target| 68 | target.build_configurations.each do |config| 69 | config.build_settings['ENABLE_BITCODE'] = 'NO' 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 13 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 15 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 16 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 17 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 18 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 19 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 20 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 21 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXCopyFilesBuildPhase section */ 25 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 26 | isa = PBXCopyFilesBuildPhase; 27 | buildActionMask = 2147483647; 28 | dstPath = ""; 29 | dstSubfolderSpec = 10; 30 | files = ( 31 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 32 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 33 | ); 34 | name = "Embed Frameworks"; 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXCopyFilesBuildPhase section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 41 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 42 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 43 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 44 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 45 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 46 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 47 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 48 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 49 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 50 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 52 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 53 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 54 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 55 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 64 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | 9740EEB11CF90186004384FC /* Flutter */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 3B80C3931E831B6300D905FE /* App.framework */, 75 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 76 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 77 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 78 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 79 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 80 | ); 81 | name = Flutter; 82 | sourceTree = ""; 83 | }; 84 | 97C146E51CF9000F007C117D = { 85 | isa = PBXGroup; 86 | children = ( 87 | 9740EEB11CF90186004384FC /* Flutter */, 88 | 97C146F01CF9000F007C117D /* Runner */, 89 | 97C146EF1CF9000F007C117D /* Products */, 90 | CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | 97C146EF1CF9000F007C117D /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 97C146EE1CF9000F007C117D /* Runner.app */, 98 | ); 99 | name = Products; 100 | sourceTree = ""; 101 | }; 102 | 97C146F01CF9000F007C117D /* Runner */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 106 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 107 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 108 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 109 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 110 | 97C147021CF9000F007C117D /* Info.plist */, 111 | 97C146F11CF9000F007C117D /* Supporting Files */, 112 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 113 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 114 | ); 115 | path = Runner; 116 | sourceTree = ""; 117 | }; 118 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 97C146F21CF9000F007C117D /* main.m */, 122 | ); 123 | name = "Supporting Files"; 124 | sourceTree = ""; 125 | }; 126 | /* End PBXGroup section */ 127 | 128 | /* Begin PBXNativeTarget section */ 129 | 97C146ED1CF9000F007C117D /* Runner */ = { 130 | isa = PBXNativeTarget; 131 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 132 | buildPhases = ( 133 | 9740EEB61CF901F6004384FC /* Run Script */, 134 | 97C146EA1CF9000F007C117D /* Sources */, 135 | 97C146EB1CF9000F007C117D /* Frameworks */, 136 | 97C146EC1CF9000F007C117D /* Resources */, 137 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 138 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 139 | ); 140 | buildRules = ( 141 | ); 142 | dependencies = ( 143 | ); 144 | name = Runner; 145 | productName = Runner; 146 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 147 | productType = "com.apple.product-type.application"; 148 | }; 149 | /* End PBXNativeTarget section */ 150 | 151 | /* Begin PBXProject section */ 152 | 97C146E61CF9000F007C117D /* Project object */ = { 153 | isa = PBXProject; 154 | attributes = { 155 | LastUpgradeCheck = 1020; 156 | ORGANIZATIONNAME = "The Chromium Authors"; 157 | TargetAttributes = { 158 | 97C146ED1CF9000F007C117D = { 159 | CreatedOnToolsVersion = 7.3.1; 160 | }; 161 | }; 162 | }; 163 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 164 | compatibilityVersion = "Xcode 3.2"; 165 | developmentRegion = en; 166 | hasScannedForEncodings = 0; 167 | knownRegions = ( 168 | en, 169 | Base, 170 | ); 171 | mainGroup = 97C146E51CF9000F007C117D; 172 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 173 | projectDirPath = ""; 174 | projectRoot = ""; 175 | targets = ( 176 | 97C146ED1CF9000F007C117D /* Runner */, 177 | ); 178 | }; 179 | /* End PBXProject section */ 180 | 181 | /* Begin PBXResourcesBuildPhase section */ 182 | 97C146EC1CF9000F007C117D /* Resources */ = { 183 | isa = PBXResourcesBuildPhase; 184 | buildActionMask = 2147483647; 185 | files = ( 186 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 187 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 188 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 189 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 190 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | /* End PBXResourcesBuildPhase section */ 195 | 196 | /* Begin PBXShellScriptBuildPhase section */ 197 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 198 | isa = PBXShellScriptBuildPhase; 199 | buildActionMask = 2147483647; 200 | files = ( 201 | ); 202 | inputPaths = ( 203 | ); 204 | name = "Thin Binary"; 205 | outputPaths = ( 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | shellPath = /bin/sh; 209 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 210 | }; 211 | 9740EEB61CF901F6004384FC /* Run Script */ = { 212 | isa = PBXShellScriptBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | ); 216 | inputPaths = ( 217 | ); 218 | name = "Run Script"; 219 | outputPaths = ( 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | shellPath = /bin/sh; 223 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 224 | }; 225 | /* End PBXShellScriptBuildPhase section */ 226 | 227 | /* Begin PBXSourcesBuildPhase section */ 228 | 97C146EA1CF9000F007C117D /* Sources */ = { 229 | isa = PBXSourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 233 | 97C146F31CF9000F007C117D /* main.m in Sources */, 234 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | /* End PBXSourcesBuildPhase section */ 239 | 240 | /* Begin PBXVariantGroup section */ 241 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 242 | isa = PBXVariantGroup; 243 | children = ( 244 | 97C146FB1CF9000F007C117D /* Base */, 245 | ); 246 | name = Main.storyboard; 247 | sourceTree = ""; 248 | }; 249 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 250 | isa = PBXVariantGroup; 251 | children = ( 252 | 97C147001CF9000F007C117D /* Base */, 253 | ); 254 | name = LaunchScreen.storyboard; 255 | sourceTree = ""; 256 | }; 257 | /* End PBXVariantGroup section */ 258 | 259 | /* Begin XCBuildConfiguration section */ 260 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 261 | isa = XCBuildConfiguration; 262 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | CLANG_ANALYZER_NONNULL = YES; 266 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 267 | CLANG_CXX_LIBRARY = "libc++"; 268 | CLANG_ENABLE_MODULES = YES; 269 | CLANG_ENABLE_OBJC_ARC = YES; 270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 271 | CLANG_WARN_BOOL_CONVERSION = YES; 272 | CLANG_WARN_COMMA = YES; 273 | CLANG_WARN_CONSTANT_CONVERSION = YES; 274 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 276 | CLANG_WARN_EMPTY_BODY = YES; 277 | CLANG_WARN_ENUM_CONVERSION = YES; 278 | CLANG_WARN_INFINITE_RECURSION = YES; 279 | CLANG_WARN_INT_CONVERSION = YES; 280 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 281 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 282 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 283 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 284 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 285 | CLANG_WARN_STRICT_PROTOTYPES = YES; 286 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 287 | CLANG_WARN_UNREACHABLE_CODE = YES; 288 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 289 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 290 | COPY_PHASE_STRIP = NO; 291 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 292 | ENABLE_NS_ASSERTIONS = NO; 293 | ENABLE_STRICT_OBJC_MSGSEND = YES; 294 | GCC_C_LANGUAGE_STANDARD = gnu99; 295 | GCC_NO_COMMON_BLOCKS = YES; 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 303 | MTL_ENABLE_DEBUG_INFO = NO; 304 | SDKROOT = iphoneos; 305 | TARGETED_DEVICE_FAMILY = "1,2"; 306 | VALIDATE_PRODUCT = YES; 307 | }; 308 | name = Profile; 309 | }; 310 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 311 | isa = XCBuildConfiguration; 312 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 313 | buildSettings = { 314 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 315 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 316 | DEVELOPMENT_TEAM = S8QB4VV633; 317 | ENABLE_BITCODE = NO; 318 | FRAMEWORK_SEARCH_PATHS = ( 319 | "$(inherited)", 320 | "$(PROJECT_DIR)/Flutter", 321 | ); 322 | INFOPLIST_FILE = Runner/Info.plist; 323 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 324 | LIBRARY_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "$(PROJECT_DIR)/Flutter", 327 | ); 328 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 329 | PRODUCT_NAME = "$(TARGET_NAME)"; 330 | VERSIONING_SYSTEM = "apple-generic"; 331 | }; 332 | name = Profile; 333 | }; 334 | 97C147031CF9000F007C117D /* Debug */ = { 335 | isa = XCBuildConfiguration; 336 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 337 | buildSettings = { 338 | ALWAYS_SEARCH_USER_PATHS = NO; 339 | CLANG_ANALYZER_NONNULL = YES; 340 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 341 | CLANG_CXX_LIBRARY = "libc++"; 342 | CLANG_ENABLE_MODULES = YES; 343 | CLANG_ENABLE_OBJC_ARC = YES; 344 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 345 | CLANG_WARN_BOOL_CONVERSION = YES; 346 | CLANG_WARN_COMMA = YES; 347 | CLANG_WARN_CONSTANT_CONVERSION = YES; 348 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 349 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 350 | CLANG_WARN_EMPTY_BODY = YES; 351 | CLANG_WARN_ENUM_CONVERSION = YES; 352 | CLANG_WARN_INFINITE_RECURSION = YES; 353 | CLANG_WARN_INT_CONVERSION = YES; 354 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 356 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 357 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 358 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 359 | CLANG_WARN_STRICT_PROTOTYPES = YES; 360 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 361 | CLANG_WARN_UNREACHABLE_CODE = YES; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 364 | COPY_PHASE_STRIP = NO; 365 | DEBUG_INFORMATION_FORMAT = dwarf; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | ENABLE_TESTABILITY = YES; 368 | GCC_C_LANGUAGE_STANDARD = gnu99; 369 | GCC_DYNAMIC_NO_PIC = NO; 370 | GCC_NO_COMMON_BLOCKS = YES; 371 | GCC_OPTIMIZATION_LEVEL = 0; 372 | GCC_PREPROCESSOR_DEFINITIONS = ( 373 | "DEBUG=1", 374 | "$(inherited)", 375 | ); 376 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 377 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 378 | GCC_WARN_UNDECLARED_SELECTOR = YES; 379 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 380 | GCC_WARN_UNUSED_FUNCTION = YES; 381 | GCC_WARN_UNUSED_VARIABLE = YES; 382 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 383 | MTL_ENABLE_DEBUG_INFO = YES; 384 | ONLY_ACTIVE_ARCH = YES; 385 | SDKROOT = iphoneos; 386 | TARGETED_DEVICE_FAMILY = "1,2"; 387 | }; 388 | name = Debug; 389 | }; 390 | 97C147041CF9000F007C117D /* Release */ = { 391 | isa = XCBuildConfiguration; 392 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_ANALYZER_NONNULL = YES; 396 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 397 | CLANG_CXX_LIBRARY = "libc++"; 398 | CLANG_ENABLE_MODULES = YES; 399 | CLANG_ENABLE_OBJC_ARC = YES; 400 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 401 | CLANG_WARN_BOOL_CONVERSION = YES; 402 | CLANG_WARN_COMMA = YES; 403 | CLANG_WARN_CONSTANT_CONVERSION = YES; 404 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 405 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 406 | CLANG_WARN_EMPTY_BODY = YES; 407 | CLANG_WARN_ENUM_CONVERSION = YES; 408 | CLANG_WARN_INFINITE_RECURSION = YES; 409 | CLANG_WARN_INT_CONVERSION = YES; 410 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 411 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 412 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 413 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 414 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 415 | CLANG_WARN_STRICT_PROTOTYPES = YES; 416 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 417 | CLANG_WARN_UNREACHABLE_CODE = YES; 418 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 419 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 420 | COPY_PHASE_STRIP = NO; 421 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 422 | ENABLE_NS_ASSERTIONS = NO; 423 | ENABLE_STRICT_OBJC_MSGSEND = YES; 424 | GCC_C_LANGUAGE_STANDARD = gnu99; 425 | GCC_NO_COMMON_BLOCKS = YES; 426 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 427 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 428 | GCC_WARN_UNDECLARED_SELECTOR = YES; 429 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 430 | GCC_WARN_UNUSED_FUNCTION = YES; 431 | GCC_WARN_UNUSED_VARIABLE = YES; 432 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 433 | MTL_ENABLE_DEBUG_INFO = NO; 434 | SDKROOT = iphoneos; 435 | TARGETED_DEVICE_FAMILY = "1,2"; 436 | VALIDATE_PRODUCT = YES; 437 | }; 438 | name = Release; 439 | }; 440 | 97C147061CF9000F007C117D /* Debug */ = { 441 | isa = XCBuildConfiguration; 442 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 443 | buildSettings = { 444 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 445 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 446 | ENABLE_BITCODE = NO; 447 | FRAMEWORK_SEARCH_PATHS = ( 448 | "$(inherited)", 449 | "$(PROJECT_DIR)/Flutter", 450 | ); 451 | INFOPLIST_FILE = Runner/Info.plist; 452 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 453 | LIBRARY_SEARCH_PATHS = ( 454 | "$(inherited)", 455 | "$(PROJECT_DIR)/Flutter", 456 | ); 457 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | VERSIONING_SYSTEM = "apple-generic"; 460 | }; 461 | name = Debug; 462 | }; 463 | 97C147071CF9000F007C117D /* Release */ = { 464 | isa = XCBuildConfiguration; 465 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 466 | buildSettings = { 467 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 468 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 469 | ENABLE_BITCODE = NO; 470 | FRAMEWORK_SEARCH_PATHS = ( 471 | "$(inherited)", 472 | "$(PROJECT_DIR)/Flutter", 473 | ); 474 | INFOPLIST_FILE = Runner/Info.plist; 475 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 476 | LIBRARY_SEARCH_PATHS = ( 477 | "$(inherited)", 478 | "$(PROJECT_DIR)/Flutter", 479 | ); 480 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 481 | PRODUCT_NAME = "$(TARGET_NAME)"; 482 | VERSIONING_SYSTEM = "apple-generic"; 483 | }; 484 | name = Release; 485 | }; 486 | /* End XCBuildConfiguration section */ 487 | 488 | /* Begin XCConfigurationList section */ 489 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 490 | isa = XCConfigurationList; 491 | buildConfigurations = ( 492 | 97C147031CF9000F007C117D /* Debug */, 493 | 97C147041CF9000F007C117D /* Release */, 494 | 249021D3217E4FDB00AE95B9 /* Profile */, 495 | ); 496 | defaultConfigurationIsVisible = 0; 497 | defaultConfigurationName = Release; 498 | }; 499 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 500 | isa = XCConfigurationList; 501 | buildConfigurations = ( 502 | 97C147061CF9000F007C117D /* Debug */, 503 | 97C147071CF9000F007C117D /* Release */, 504 | 249021D4217E4FDB00AE95B9 /* Profile */, 505 | ); 506 | defaultConfigurationIsVisible = 0; 507 | defaultConfigurationName = Release; 508 | }; 509 | /* End XCConfigurationList section */ 510 | }; 511 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 512 | } 513 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 3 | 4 | class DetailPage extends StatefulWidget { 5 | DetailPage({Key key}) : super(key: key); 6 | 7 | @override 8 | _MyHomePageState createState() => _MyHomePageState(); 9 | } 10 | 11 | class _MyHomePageState extends State with PageTrackerAware, TrackerPageMixin, PageLoadMixin { 12 | 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | 17 | return Scaffold( 18 | appBar: AppBar( 19 | title: Text("PageRoute Demo"), 20 | ), 21 | body: Container( 22 | color: Colors.blue, 23 | padding: EdgeInsets.all(10), 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | mainAxisAlignment: MainAxisAlignment.start, 27 | children: [ 28 | Text( 29 | "When a PageRoute is added to navigation stack, a PageExit event " 30 | "will be trigged on previous route and you can override didPageExit method" 31 | "by using TrackerPageMixin. ", 32 | ), 33 | Container( 34 | height: 10, 35 | ), 36 | Text( 37 | "After PageExit event, a PageView event will be trigged on current route.", 38 | ), 39 | Container( 40 | height: 10, 41 | ), 42 | Text("Vice versa for the pop of PageRoute."), 43 | Container( 44 | height: 50, 45 | ), 46 | Text("You can see 'PageExit: PageRoute' and 'PageView: PageRoute' in the " 47 | "console panel.") 48 | ], 49 | ), 50 | ),// This trailing comma makes auto-formatting nicer for build methods. 51 | ); 52 | } 53 | 54 | @override 55 | void didPageView() { 56 | super.didPageView(); 57 | 58 | print("PageView: PageRoute"); 59 | } 60 | 61 | @override 62 | void didPageExit() { 63 | super.didPageExit(); 64 | 65 | print("PageExit: PageRoute"); 66 | } 67 | 68 | @override 69 | void didPageLoaded(_, __, ___, ____) { 70 | // DoSomething 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/lib/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 5 | import 'package:example/popup_page.dart'; 6 | 7 | class MyHomePage extends StatefulWidget { 8 | MyHomePage({Key key, this.title}) : super(key: key); 9 | 10 | 11 | final String title; 12 | 13 | @override 14 | _MyHomePageState createState() => _MyHomePageState(); 15 | } 16 | 17 | class _MyHomePageState extends State with PageTrackerAware, TrackerPageMixin, PageLoadMixin { 18 | 19 | Widget _button(String text, VoidCallback onTap) { 20 | return GestureDetector( 21 | onTap: onTap, 22 | child: Container( 23 | color: Colors.amber[500 + 100 * Random().nextInt(4)], 24 | height: 50, 25 | child: Center( 26 | child: Text(text), 27 | ), 28 | ) 29 | ); 30 | } 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | 40 | return Scaffold( 41 | appBar: AppBar( 42 | // Here we take the value from the MyHomePage object that was created by 43 | // the App.build method, and use it to set our appbar title. 44 | title: Text(widget.title), 45 | ), 46 | body: SafeArea( 47 | child: CustomScrollView( 48 | slivers: [ 49 | SliverToBoxAdapter( 50 | child: Container( 51 | color: Colors.blue, 52 | padding: EdgeInsets.all(10), 53 | child: Column( 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | children: [ 56 | Text("flutter_page_tracker is a flutter plugin which " 57 | "help tracking route events of PageRoute/PopupRoute/PageView/TabView."), 58 | Container(height: 50,), 59 | Text("1. Route events include PageView and PageExit."), 60 | Container(height: 15,), 61 | Text("2. For PageRoute, when the current route is pushed to the top of the stack, " 62 | "a PageView event will be trigged on the current, and a PageExit event will " 63 | "be trigged on previous route, and vice versa for pop route."), 64 | Container(height: 15,), 65 | Text("3. For PopupRoute, only PageView will be trigged when dialog " 66 | "is pushed, and vice versa for pop route."), 67 | Container(height: 15,), 68 | Text("4. For PageView and TabView, PageView and PageExit will be trigged when you " 69 | "switch between views.") 70 | ], 71 | ), 72 | ), 73 | ), 74 | SliverList( 75 | delegate: SliverChildListDelegate( 76 | [ 77 | _button( 78 | 'PageRoute demo', 79 | () { 80 | Navigator.of(context).pushNamed("detail"); 81 | } 82 | ), 83 | _button( 84 | 'PopupRoute demo', 85 | () { 86 | showDialog( 87 | context: context, 88 | builder: (_) { 89 | return PopupPage(); 90 | } 91 | ); 92 | } 93 | ), 94 | _button( 95 | 'PageView Mixin demo', 96 | () { 97 | Navigator.of(context).pushNamed("pageview_mixin"); 98 | } 99 | ), 100 | _button( 101 | 'PageView Wrapper demo', 102 | () { 103 | Navigator.of(context).pushNamed("pageview"); 104 | } 105 | ), 106 | _button( 107 | 'TapView demo', 108 | () { 109 | Navigator.of(context).pushNamed("tabview"); 110 | } 111 | ), 112 | _button( 113 | 'Pageview Wraped in TabView demo', 114 | () { 115 | Navigator.of(context).pushNamed("pageviewInTabView"); 116 | } 117 | ), 118 | ], 119 | ), 120 | ), 121 | ], 122 | ), 123 | ),// This trailing comma makes auto-formatting nicer for build methods. 124 | ); 125 | } 126 | 127 | @override 128 | void didPageView() { 129 | super.didPageView(); 130 | 131 | print("tracker pageview home"); 132 | } 133 | 134 | @override 135 | void didPageLoaded(_, __, ___, ____) { 136 | // DoSomething 137 | } 138 | 139 | @override 140 | void didPageExit() { 141 | super.didPageExit(); 142 | 143 | print("tracker pageExit home"); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 3 | 4 | // 页面 5 | import 'home_page.dart'; 6 | import 'detail_page.dart'; 7 | import 'pageview_wrapper_page.dart'; 8 | import 'tabview_page.dart'; 9 | import 'pageview_in_tabview_page.dart'; 10 | import 'pageview_mixin_page.dart'; 11 | 12 | void main() => runApp( 13 | TrackerRouteObserverProvider( 14 | child: PageLoadProvider(env: "dev",child: MyApp()), 15 | ) 16 | ); 17 | 18 | class MyApp extends StatelessWidget { 19 | // This widget is the root of your application. 20 | @override 21 | Widget build(BuildContext context) { 22 | 23 | return MaterialApp( 24 | title: 'Flutter Demo', 25 | theme: ThemeData( 26 | primarySwatch: Colors.blue, 27 | textTheme: TextTheme( 28 | button: TextStyle(fontSize: 30), 29 | display1: TextStyle(fontSize: 40), 30 | display2: TextStyle(fontSize: 40), 31 | display3: TextStyle(fontSize: 40), 32 | display4: TextStyle(fontSize: 40), 33 | headline: TextStyle(fontSize: 40), 34 | title: TextStyle(fontSize: 40), 35 | subhead: TextStyle(fontSize: 40), 36 | body1: TextStyle(fontSize: 20, color: Colors.white), 37 | ) 38 | ), 39 | navigatorObservers: [TrackerRouteObserverProvider.of(context)], 40 | home: MyHomePage(title: 'Flutter_Page_tracker Demo'), 41 | routes: { 42 | "home": (_) => MyHomePage(title: 'Flutter_Page_tracker Demo'), 43 | "detail": (_) => DetailPage(), 44 | "pageview_mixin": (_) => PageViewMixinPage(title: 'PageView Mixin Demo'), 45 | "pageview": (_) => PageViewWrapperPage(title: 'PageView Wrapper Demo'), 46 | "tabview": (_) => TabViewPage(title: 'TabView Demo'), 47 | "pageviewInTabView": (_) => PageviewInTabviewPage(title: 'Pageview Wraped in TabView demo'), 48 | }, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/lib/pageview_in_tabview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 3 | 4 | import 'pageview_wrapper_page.dart'; 5 | 6 | class PageviewInTabviewPage extends StatefulWidget { 7 | PageviewInTabviewPage({Key key, this.title}) : super(key: key); 8 | 9 | 10 | final String title; 11 | 12 | @override 13 | _State createState() => _State(); 14 | } 15 | 16 | class _State extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { 17 | 18 | @override 19 | bool get wantKeepAlive => true; 20 | 21 | TabController tabController; 22 | PageController pageController; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | tabController = TabController(initialIndex: 0, length: 3, vsync: this); 28 | pageController = PageController(); 29 | } 30 | 31 | Widget _buildTag(BuildContext _, int index, int color) { 32 | return Builder( 33 | builder: (_) { 34 | 35 | return PageViewListenerWrapper( 36 | index, 37 | onPageView: () { 38 | print("TabView: PageView $index"); 39 | }, 40 | onPageExit: () { 41 | print("TabView: PageExit $index"); 42 | }, 43 | child: Container( 44 | color: Colors.blue[color], 45 | child: Stack( 46 | alignment: Alignment.bottomCenter, 47 | children: [ 48 | Container( 49 | padding: EdgeInsets.all(10), 50 | child: Column( 51 | crossAxisAlignment: CrossAxisAlignment.start, 52 | children: [ 53 | Text("Tab $index: normal tab", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),), 54 | ], 55 | ), 56 | ), 57 | Positioned( 58 | left: 0, 59 | right: 0, 60 | bottom: 45, 61 | child: GestureDetector( 62 | onTap: () { 63 | Navigator.of(context).pushNamed("detail"); 64 | }, 65 | child: Container( 66 | color: Colors.amber, 67 | height: 50, 68 | child: Center( 69 | child: Text("Go to another PageRoute"), 70 | ), 71 | ), 72 | ), 73 | ) 74 | ], 75 | ), 76 | ), 77 | ); 78 | }, 79 | ); 80 | } 81 | 82 | Widget _buildPage(int index, int color) { 83 | return PageViewListenerWrapper( 84 | index, 85 | onPageView: () { 86 | print("pageview $index"); 87 | }, 88 | onPageExit: () { 89 | print("pageexit $index"); 90 | }, 91 | child: SafeArea( 92 | child: Container( 93 | color: Colors.blue[color], 94 | child: Stack( 95 | alignment: Alignment.bottomCenter, 96 | children: [ 97 | Container( 98 | padding: EdgeInsets.all(10), 99 | child: Column( 100 | crossAxisAlignment: CrossAxisAlignment.start, 101 | children: [ 102 | Text("Page $index wrapped in Tab1", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),), 103 | Container(height: 50,), 104 | Text("For PageView and TabView, PageView and PageExit will be trigged when you " 105 | "switch between views."), 106 | Container(height: 15,), 107 | Text("You can see 'PageView $index' and 'PageExit $index' in the console."), 108 | Container(height: 15,), 109 | Text("PageExit event will also be trigged when you push a new PageRoute on current stack. " 110 | "Try it by clicking the buttom show below. "), 111 | Container(height: 15,), 112 | Text("When you pop a PageRoute, the previous " 113 | "focused page will receive a PageView event. "), 114 | Container(height: 50,), 115 | Text("Try slide", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600)) 116 | ], 117 | ), 118 | ), 119 | Positioned( 120 | left: 0, 121 | right: 0, 122 | bottom: 45, 123 | child: GestureDetector( 124 | onTap: () { 125 | Navigator.of(context).pushNamed("detail"); 126 | }, 127 | child: Container( 128 | color: Colors.amber, 129 | height: 50, 130 | child: Center( 131 | child: Text("Go to another PageRoute"), 132 | ), 133 | ), 134 | ), 135 | ) 136 | ], 137 | ), 138 | ), 139 | ), 140 | ); 141 | } 142 | 143 | @override 144 | Widget build(BuildContext context) { 145 | 146 | return Scaffold( 147 | appBar: AppBar( 148 | title: Text(widget.title), 149 | ), 150 | body: SafeArea( 151 | child: Stack( 152 | alignment: Alignment.bottomCenter, 153 | children: [ 154 | PageViewWrapper( 155 | pageAmount: 3, 156 | initialPage: 0, 157 | changeDelegate: TabViewChangeDelegate(tabController), 158 | child: TabBarView( 159 | physics: NeverScrollableScrollPhysics(), 160 | controller: tabController, 161 | children: [ 162 | Builder( 163 | builder: (BuildContext context) { 164 | 165 | return PageViewListenerWrapper( 166 | 0, 167 | onPageView: () { 168 | // print("tabbar pageview 0"); 169 | }, 170 | onPageExit: () { 171 | // print("tabbar pageexit 0"); 172 | }, 173 | child: PageViewWrapper( 174 | changeDelegate: PageViewChangeDelegate(pageController), 175 | pageAmount: 3, 176 | initialPage: pageController.initialPage, 177 | child: PageView( 178 | controller: pageController, 179 | children: [ 180 | _buildPage(0, 100), 181 | _buildPage(1, 300), 182 | _buildPage(2, 500) 183 | ], 184 | ), 185 | ) 186 | ); 187 | }, 188 | ), 189 | _buildTag(context, 1, 600), 190 | _buildTag(context, 2, 900), 191 | ], 192 | ), 193 | ), 194 | Positioned( 195 | bottom: 0, 196 | left: 0, 197 | right: 0, 198 | child: TabBar( 199 | controller: tabController, 200 | tabs: [ 201 | Tab(text: "Tab1",), 202 | Tab(text: "Tab2",), 203 | Tab(text: "Tab3",), 204 | ], 205 | ), 206 | ), 207 | ], 208 | ), 209 | ) 210 | ); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /example/lib/pageview_mixin_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 3 | 4 | class PageViewMixinPage extends StatefulWidget { 5 | PageViewMixinPage({Key key, this.title}) : super(key: key); 6 | 7 | 8 | final String title; 9 | 10 | @override 11 | _MyHomePageState createState() => _MyHomePageState(); 12 | } 13 | 14 | class _MyHomePageState extends State with AutomaticKeepAliveClientMixin { 15 | 16 | @override 17 | bool get wantKeepAlive => true; 18 | 19 | PageController pageController; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | pageController = PageController(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | super.build(context); 30 | return Scaffold( 31 | appBar: AppBar( 32 | title: Text(widget.title), 33 | ), 34 | body: PageViewWrapper( 35 | changeDelegate: PageViewChangeDelegate(pageController), 36 | pageAmount: 3, 37 | initialPage: pageController.initialPage, 38 | child: PageView( 39 | controller: pageController, 40 | children: [ 41 | Page(index: 0, color: 300,), 42 | Page(index: 1, color: 600,), 43 | Page(index: 3, color: 900,), 44 | ], 45 | ), 46 | ) 47 | ); 48 | } 49 | } 50 | 51 | class Page extends StatefulWidget { 52 | final int index; 53 | final int color; 54 | 55 | const Page({Key key, this.index, this.color}): super(key: key); 56 | 57 | @override 58 | PageState createState() { 59 | return PageState(); 60 | } 61 | } 62 | 63 | class PageState extends State with PageTrackerAware, PageViewListenerMixin, PageLoadMixin { 64 | 65 | int get pageViewIndex => widget.index; 66 | 67 | @override 68 | void didPageView() { 69 | super.didPageView(); 70 | print("PageView mixin ${widget.index}"); 71 | } 72 | 73 | @override 74 | void didPageExit() { 75 | super.didPageExit(); 76 | print("PageExit mixin ${widget.index}"); 77 | } 78 | 79 | @override 80 | void didPageLoaded(_, __, ___, ____) { 81 | // DoSomething 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | return SafeArea( 87 | child: Container( 88 | color: Colors.blue[widget.color], 89 | child: Stack( 90 | alignment: Alignment.bottomCenter, 91 | children: [ 92 | Container( 93 | padding: EdgeInsets.all(10), 94 | child: Column( 95 | crossAxisAlignment: CrossAxisAlignment.start, 96 | children: [ 97 | Text("Page $widget.index", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),), 98 | Container(height: 50,), 99 | Text("For PageView and TabView, PageView and PageExit will be trigged when you " 100 | "switch between views."), 101 | Container(height: 15,), 102 | Text("You can see 'PageView $widget.index' and 'PageExit $widget.index' in the console."), 103 | Container(height: 15,), 104 | Text("PageExit event will also be trigged when you push a new PageRoute on current stack. " 105 | "Try it by clicking the buttom show below. "), 106 | Container(height: 15,), 107 | Text("When you pop a PageRoute, the previous " 108 | "focused page will receive a PageView event. "), 109 | Container(height: 50,), 110 | Text("Try slide", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600)) 111 | ], 112 | ), 113 | ), 114 | GestureDetector( 115 | onTap: () { 116 | Navigator.of(context).pushNamed("detail"); 117 | }, 118 | child: Container( 119 | color: Colors.amber, 120 | height: 50, 121 | child: Center( 122 | child: Text("Go to another PageRoute"), 123 | ), 124 | ), 125 | ) 126 | ], 127 | ), 128 | ), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /example/lib/pageview_wrapper_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 3 | 4 | class PageViewWrapperPage extends StatefulWidget { 5 | PageViewWrapperPage({Key key, this.title}) : super(key: key); 6 | 7 | 8 | final String title; 9 | 10 | @override 11 | _MyHomePageState createState() => _MyHomePageState(); 12 | } 13 | 14 | class _MyHomePageState extends State with AutomaticKeepAliveClientMixin { 15 | 16 | @override 17 | bool get wantKeepAlive => true; 18 | 19 | PageController pageController; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | pageController = PageController(); 25 | } 26 | 27 | Widget _buildPage(int index, int color) { 28 | return PageViewListenerWrapper( 29 | index, 30 | onPageView: () { 31 | print("pageview $index"); 32 | }, 33 | onPageExit: () { 34 | print("pageexit $index"); 35 | }, 36 | onPageLoaded: (_, __, ___, ____) { 37 | }, 38 | child: SafeArea( 39 | child: Container( 40 | color: Colors.blue[color], 41 | child: Stack( 42 | alignment: Alignment.bottomCenter, 43 | children: [ 44 | Container( 45 | padding: EdgeInsets.all(10), 46 | child: Column( 47 | crossAxisAlignment: CrossAxisAlignment.start, 48 | children: [ 49 | Text("Page $index", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),), 50 | Container(height: 50,), 51 | Text("For PageView and TabView, PageView and PageExit will be trigged when you " 52 | "switch between views."), 53 | Container(height: 15,), 54 | Text("You can see 'PageView $index' and 'PageExit $index' in the console."), 55 | Container(height: 15,), 56 | Text("PageExit event will also be trigged when you push a new PageRoute on current stack. " 57 | "Try it by clicking the buttom show below. "), 58 | Container(height: 15,), 59 | Text("When you pop a PageRoute, the previous " 60 | "focused page will receive a PageView event. "), 61 | Container(height: 50,), 62 | Text("Try slide", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600)) 63 | ], 64 | ), 65 | ), 66 | GestureDetector( 67 | onTap: () { 68 | Navigator.of(context).pushNamed("detail"); 69 | }, 70 | child: Container( 71 | color: Colors.amber, 72 | height: 50, 73 | child: Center( 74 | child: Text("Go to another PageRoute"), 75 | ), 76 | ), 77 | ) 78 | ], 79 | ), 80 | ), 81 | ), 82 | ); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | super.build(context); 88 | return Scaffold( 89 | appBar: AppBar( 90 | // Here we take the value from the MyHomePage object that was created by 91 | // the App.build method, and use it to set our appbar title. 92 | title: Text(widget.title), 93 | ), 94 | body: PageViewWrapper( 95 | changeDelegate: PageViewChangeDelegate(pageController), 96 | pageAmount: 3, 97 | initialPage: pageController.initialPage, 98 | child: PageView( 99 | controller: pageController, 100 | children: [ 101 | _buildPage(0, 300), 102 | _buildPage(1, 600), 103 | _buildPage(2, 900), 104 | ], 105 | ), 106 | ) 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /example/lib/popup_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 3 | import 'dart:math'; 4 | 5 | class PopupPage extends StatelessWidget { 6 | 7 | const PopupPage({ 8 | Key key, 9 | }): super(key: key); 10 | 11 | Widget _button(String text, VoidCallback onTap) { 12 | return GestureDetector( 13 | onTap: onTap, 14 | child: Container( 15 | color: Colors.amber[500 + 100 * Random().nextInt(4)], 16 | height: 50, 17 | child: Center( 18 | child: Text(text), 19 | ), 20 | ) 21 | ); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return TrackerDialogWrapper( 27 | didPageView: () { 28 | print('dialog didPageView'); 29 | }, 30 | didPageExit: () { 31 | print('dialog didPageExit'); 32 | }, 33 | child: SimpleDialog( 34 | elevation: 0, 35 | backgroundColor: Colors.transparent, 36 | children: [ 37 | ClipRRect( 38 | clipBehavior: Clip.antiAlias, 39 | borderRadius: BorderRadius.all(Radius.circular(15)), 40 | child: Container( 41 | width: 400, 42 | color: Colors.blue, 43 | child: Column( 44 | crossAxisAlignment: CrossAxisAlignment.start, 45 | children: [ 46 | Container( 47 | padding: EdgeInsets.all(10), 48 | child: Text("When you show a dialog, only PageView event will be trigged. "), 49 | ), 50 | Container( 51 | padding: EdgeInsets.all(10), 52 | child: Text("You can see 'dialog didPageView' in the console. "), 53 | ), 54 | Container(height: 10,), 55 | _button("Go to anther PageRoute, will not trigger PageExit on this dialog", () { 56 | Navigator.of(context).pushNamed("detail"); 57 | }), 58 | _button("Close dialog, will not trigger PageView on Previous route", () { 59 | Navigator.of(context).pop(); 60 | }), 61 | ], 62 | ), 63 | ), 64 | ), 65 | ], 66 | ), 67 | ); 68 | } 69 | } -------------------------------------------------------------------------------- /example/lib/tabview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_page_tracker/flutter_page_tracker.dart'; 3 | 4 | class TabViewPage extends StatefulWidget { 5 | TabViewPage({Key key, this.title}) : super(key: key); 6 | 7 | 8 | final String title; 9 | 10 | @override 11 | _State createState() => _State(); 12 | } 13 | 14 | class _State extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { 15 | 16 | @override 17 | bool get wantKeepAlive => true; 18 | 19 | TabController tabController; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | tabController = TabController(initialIndex: 0, length: 3, vsync: this); 25 | } 26 | 27 | Widget _buildTag(BuildContext _, int index, int color) { 28 | return Builder( 29 | builder: (_) { 30 | 31 | return PageViewListenerWrapper( 32 | index, 33 | onPageView: () { 34 | print("TabView: PageView $index"); 35 | }, 36 | onPageExit: () { 37 | print("TabView: PageExit $index"); 38 | }, 39 | child: Container( 40 | color: Colors.blue[color], 41 | child: Stack( 42 | alignment: Alignment.bottomCenter, 43 | children: [ 44 | Container( 45 | padding: EdgeInsets.all(10), 46 | child: Column( 47 | crossAxisAlignment: CrossAxisAlignment.start, 48 | children: [ 49 | Text("Tab $index", style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),), 50 | Container(height: 50,), 51 | Text("For PageView and TabView, PageView and PageExit will be trigged when you " 52 | "switch between views."), 53 | Container(height: 15,), 54 | Text("You can see 'PageView $index' and 'PageExit $index' in the console."), 55 | Container(height: 15,), 56 | Text("PageExit event will also be trigged when you push a new PageRoute on current stack. " 57 | "Try it by clicking the buttom show below."), 58 | Container(height: 15,), 59 | Text("When you pop a PageRoute, the previous " 60 | "focused tab will receive a PageView event. "), 61 | ], 62 | ), 63 | ), 64 | Positioned( 65 | left: 0, 66 | right: 0, 67 | bottom: 45, 68 | child: GestureDetector( 69 | onTap: () { 70 | Navigator.of(context).pushNamed("detail"); 71 | }, 72 | child: Container( 73 | color: Colors.amber, 74 | height: 50, 75 | child: Center( 76 | child: Text("Go to another PageRoute"), 77 | ), 78 | ), 79 | ), 80 | ) 81 | ], 82 | ), 83 | ), 84 | ); 85 | }, 86 | ); 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | super.build(context); 92 | return Scaffold( 93 | appBar: AppBar( 94 | title: Text(widget.title), 95 | ), 96 | body: SafeArea( 97 | child: Container( 98 | color: Colors.blue, 99 | child: Stack( 100 | alignment: Alignment.bottomCenter, 101 | children: [ 102 | PageViewWrapper( 103 | pageAmount: 3, 104 | initialPage: 0, 105 | changeDelegate: TabViewChangeDelegate(tabController), 106 | child: TabBarView( 107 | controller: tabController, 108 | children: [ 109 | _buildTag(context, 0, 300), 110 | _buildTag(context, 1, 600), 111 | _buildTag(context, 2, 900), 112 | ], 113 | ), 114 | ), 115 | Positioned( 116 | bottom: 0, 117 | left: 0, 118 | right: 0, 119 | child: Container( 120 | color: Colors.amber[900], 121 | child: TabBar( 122 | controller: tabController, 123 | tabs: [ 124 | Tab(text: "Tab1",), 125 | Tab(text: "Tab2",), 126 | Tab(text: "Tab3",), 127 | ], 128 | ), 129 | ), 130 | ), 131 | ], 132 | ), 133 | ), 134 | ) 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.2.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.0.4" 18 | charcode: 19 | dependency: transitive 20 | description: 21 | name: charcode 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.1.2" 25 | collection: 26 | dependency: transitive 27 | description: 28 | name: collection 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.14.11" 32 | cupertino_icons: 33 | dependency: "direct main" 34 | description: 35 | name: cupertino_icons 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "0.1.3" 39 | flutter: 40 | dependency: "direct main" 41 | description: flutter 42 | source: sdk 43 | version: "0.0.0" 44 | flutter_page_tracker: 45 | dependency: "direct main" 46 | description: 47 | path: ".." 48 | relative: true 49 | source: path 50 | version: "3.0.0" 51 | flutter_test: 52 | dependency: "direct dev" 53 | description: flutter 54 | source: sdk 55 | version: "0.0.0" 56 | fluttertoast: 57 | dependency: transitive 58 | description: 59 | name: fluttertoast 60 | url: "https://pub.flutter-io.cn" 61 | source: hosted 62 | version: "2.2.11" 63 | matcher: 64 | dependency: transitive 65 | description: 66 | name: matcher 67 | url: "https://pub.flutter-io.cn" 68 | source: hosted 69 | version: "0.12.5" 70 | meta: 71 | dependency: transitive 72 | description: 73 | name: meta 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "1.1.6" 77 | path: 78 | dependency: transitive 79 | description: 80 | name: path 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "1.6.2" 84 | pedantic: 85 | dependency: transitive 86 | description: 87 | name: pedantic 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "1.7.0" 91 | quiver: 92 | dependency: transitive 93 | description: 94 | name: quiver 95 | url: "https://pub.flutter-io.cn" 96 | source: hosted 97 | version: "2.0.3" 98 | sky_engine: 99 | dependency: transitive 100 | description: flutter 101 | source: sdk 102 | version: "0.0.99" 103 | source_span: 104 | dependency: transitive 105 | description: 106 | name: source_span 107 | url: "https://pub.flutter-io.cn" 108 | source: hosted 109 | version: "1.5.5" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.flutter-io.cn" 115 | source: hosted 116 | version: "1.9.3" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "2.0.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "1.0.4" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.1.0" 138 | test_api: 139 | dependency: transitive 140 | description: 141 | name: test_api 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "0.2.5" 145 | typed_data: 146 | dependency: transitive 147 | description: 148 | name: typed_data 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "1.1.6" 152 | vector_math: 153 | dependency: transitive 154 | description: 155 | name: vector_math 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "2.0.8" 159 | sdks: 160 | dart: ">=2.2.2 <3.0.0" 161 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | flutter_page_tracker: 23 | path: ../ 24 | # flutter_page_tracker: ^3.0.0 25 | 26 | # The following adds the Cupertino Icons font to your application. 27 | # Use with the CupertinoIcons class for iOS style icons. 28 | cupertino_icons: ^0.1.2 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | 34 | 35 | # For information on the generic Dart part of this file, see the 36 | # following page: https://dart.dev/tools/pub/pubspec 37 | 38 | # The following section is specific to Flutter. 39 | flutter: 40 | 41 | # The following line ensures that the Material Icons font is 42 | # included with your application, so that you can use the icons in 43 | # the material Icons class. 44 | uses-material-design: true 45 | 46 | # To add assets to your application, add an assets section, like this: 47 | assets: 48 | - webp/sample.webp 49 | # - images/a_dot_ham.jpeg 50 | 51 | # An image asset can refer to one or more resolution-specific "variants", see 52 | # https://flutter.dev/assets-and-images/#resolution-aware. 53 | 54 | # For details regarding adding assets from package dependencies, see 55 | # https://flutter.dev/assets-and-images/#from-packages 56 | 57 | # To add custom fonts to your application, add a fonts section here, 58 | # in this "flutter" section. Each entry in this list should have a 59 | # "family" key with the font family name, and a "fonts" key with a 60 | # list giving the asset and other descriptors for the font. For 61 | # example: 62 | # fonts: 63 | # - family: Schyler 64 | # fonts: 65 | # - asset: fonts/Schyler-Regular.ttf 66 | # - asset: fonts/Schyler-Italic.ttf 67 | # style: italic 68 | # - family: Trajan Pro 69 | # fonts: 70 | # - asset: fonts/TrajanPro.ttf 71 | # - asset: fonts/TrajanPro_Bold.ttf 72 | # weight: 700 73 | # 74 | # For details regarding fonts from package dependencies, 75 | # see https://flutter.dev/custom-fonts/#from-packages 76 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /example/webp/sample.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/example/webp/sample.webp -------------------------------------------------------------------------------- /gifs/1PageRoute.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/gifs/1PageRoute.gif -------------------------------------------------------------------------------- /gifs/2PopupRoute.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/gifs/2PopupRoute.gif -------------------------------------------------------------------------------- /gifs/3PageView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/gifs/3PageView.gif -------------------------------------------------------------------------------- /gifs/3PageView.gif.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/gifs/3PageView.gif.zip -------------------------------------------------------------------------------- /gifs/4TabView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/gifs/4TabView.gif -------------------------------------------------------------------------------- /gifs/5PageViewInTabView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/gifs/5PageViewInTabView.gif -------------------------------------------------------------------------------- /gifs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/gifs/demo.gif -------------------------------------------------------------------------------- /lib/flutter_page_tracker.dart: -------------------------------------------------------------------------------- 1 | library flutter_page_tracker; 2 | 3 | export './src/page_tracker_aware.dart'; 4 | export './src/tracker_route_observer.dart'; 5 | export './src/tracker_route_observer_provider.dart'; 6 | export './src/tracker_page_widget.dart'; 7 | export './src/tracker_page_mixin.dart'; 8 | export './src/page_view_wrapper.dart'; 9 | export './src/tracker_dialog_wrapper.dart'; 10 | export './src/page_view_listener_mixin.dart'; 11 | export './src/page_load_mixin.dart'; 12 | export './src/page_load_provider.dart'; 13 | -------------------------------------------------------------------------------- /lib/src/page_load_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'page_tracker_aware.dart'; 4 | import 'dart:async'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'page_load_provider.dart'; 7 | import 'package:flutter/scheduler.dart'; 8 | 9 | // 监控页面加载时长 10 | mixin PageLoadMixin on State, PageTrackerAware { 11 | // 增加页面加载时间统计 12 | DateTime _firstCreateTime; // 初始化时间 13 | DateTime _firstBuildTIme; // 首次build时间,通常为布局解析时间 14 | DateTime _beginRequestTime; // 发起网络请求 15 | DateTime _endRequestTime; // 网络请求结束 16 | DateTime _rebuildStartTime; // 得到请求结果后二次刷新开始的时间 17 | DateTime _nextFrameTime; // 二次刷新后,完成渲染的时间 18 | 19 | StreamSubscription _httpRequestSS; 20 | 21 | @protected 22 | String get httpRequestKey => null; 23 | 24 | void _didPageloaded() { 25 | 26 | // 总时间 27 | Duration totalTime = _nextFrameTime.difference(_firstCreateTime); 28 | // 页面初始化时间 29 | Duration buildTime = _firstBuildTIme.difference(_firstCreateTime); 30 | // 网络请求时间 31 | Duration requestTime = httpRequestKey == null ? null : _endRequestTime.difference(_beginRequestTime); 32 | // 渲染时间 33 | Duration renderTime = _nextFrameTime.difference(_rebuildStartTime); 34 | 35 | if (PageLoadProvider.of(context) != 'pro') { 36 | Fluttertoast.showToast(msg: "加载时长:${totalTime.inMilliseconds}", fontSize: 16); 37 | } 38 | 39 | didPageLoaded(totalTime, buildTime, requestTime, renderTime); 40 | } 41 | 42 | void beginRequestTime() { 43 | _beginRequestTime ??= DateTime.now(); 44 | } 45 | 46 | void endRequestTime() { 47 | _endRequestTime ??= DateTime.now(); 48 | } 49 | 50 | @override 51 | void setState(fn) { 52 | if (httpRequestKey != null && _endRequestTime != null && _rebuildStartTime == null) { 53 | _rebuildStartTime = DateTime.now(); 54 | Future.delayed(Duration(seconds: 0), () { 55 | _nextFrameTime ??= DateTime.now(); 56 | _didPageloaded(); 57 | }); 58 | } 59 | super.setState(fn); 60 | } 61 | 62 | void _handleHttpRequestEvent(int type) { 63 | if (type == 0) { 64 | beginRequestTime(); 65 | } 66 | 67 | if (type == 1) { 68 | endRequestTime(); 69 | rebuildStartTime(); 70 | } 71 | } 72 | 73 | @override 74 | void initState() { 75 | super.initState(); 76 | _firstCreateTime ??= DateTime.now(); 77 | 78 | // 监听网络开始加载 79 | if (httpRequestKey != null) { 80 | _httpRequestSS = PageLoadHttpRequestObserver.on(httpRequestKey).listen(_handleHttpRequestEvent); 81 | } 82 | 83 | Future.delayed(Duration(seconds: 0), () { 84 | _firstBuildTIme ??= DateTime.now(); 85 | rebuildStartTime(); 86 | }); 87 | } 88 | 89 | void rebuildStartTime() { 90 | if (httpRequestKey == null) { // 没有网络请求 91 | if (_rebuildStartTime == null) { 92 | _rebuildStartTime = DateTime.now(); 93 | _nextFrameTime ??= DateTime.now(); 94 | _didPageloaded(); 95 | } 96 | } else { // 使用网络请求 97 | if (_endRequestTime != null && _rebuildStartTime == null) { 98 | _rebuildStartTime = DateTime.now(); 99 | Future.delayed(Duration(seconds: 0), () { 100 | _nextFrameTime ??= DateTime.now(); 101 | _didPageloaded(); 102 | }); 103 | } 104 | } 105 | } 106 | 107 | @override 108 | void dispose() { 109 | _httpRequestSS?.cancel(); 110 | super.dispose(); 111 | } 112 | } 113 | 114 | // 监控网络请求 115 | class PageLoadHttpRequestObserver { 116 | static StreamController _streamController = StreamController.broadcast(sync: true); 117 | 118 | static void fireHttpRequestStart(String key) { 119 | _streamController.add({ 120 | "key": key, 121 | "type": 0 122 | }); 123 | } 124 | 125 | static void fireHttpRequestComplete(String key) { 126 | _streamController.add({ 127 | "key": key, 128 | "type": 1 129 | }); 130 | } 131 | 132 | static Stream on(String key) { 133 | return _streamController.stream 134 | .where((Map event) => event['key'] == key) 135 | .map((Map event) => event['type']); 136 | } 137 | 138 | static void destroy() { 139 | _streamController.close(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/src/page_load_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class PageLoadProvider extends InheritedWidget { 4 | 5 | final String env; 6 | 7 | PageLoadProvider({ 8 | Key key, 9 | this.env = 'pro', 10 | @required Widget child, 11 | }): super(key: key, child: child); 12 | 13 | @override 14 | bool updateShouldNotify(PageLoadProvider oldWidget) { 15 | return env != oldWidget.env; 16 | } 17 | 18 | static String of(BuildContext context) { 19 | try { 20 | return (context.inheritFromWidgetOfExactType( 21 | PageLoadProvider) as PageLoadProvider) 22 | .env; 23 | } catch (err) { 24 | return "pro"; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /lib/src/page_tracker_aware.dart: -------------------------------------------------------------------------------- 1 | abstract class PageTrackerAware { 2 | 3 | // 判断当前路由是否是被浏览的路由 4 | bool isActive = false; 5 | 6 | void didPageView() { 7 | isActive = true; 8 | } 9 | 10 | void didPageExit() { 11 | isActive = false; 12 | } 13 | 14 | void didPageLoaded(Duration totalTime, Duration buildTime, Duration requestTime, Duration renderTime) { } 15 | 16 | } -------------------------------------------------------------------------------- /lib/src/page_view_listener_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | 4 | import 'page_tracker_aware.dart'; 5 | import 'page_view_wrapper.dart'; 6 | import 'page_load_mixin.dart'; 7 | 8 | // 处理pageview组件的事件传法 9 | mixin PageViewListenerMixin on State, PageTrackerAware { 10 | 11 | StreamSubscription sb; 12 | bool isPageView = false; 13 | // 向列表中的列表转发页面事件 14 | Set subscribers; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | 20 | subscribers = Set(); 21 | } 22 | 23 | @override 24 | void didChangeDependencies() { 25 | super.didChangeDependencies(); 26 | if (sb == null && pageViewIndex!= null) { 27 | 28 | Stream stream = PageViewWrapper.of(context, pageViewIndex); 29 | // 如果外围没有包裹PageViewWrapper,那么stream为null 30 | if (stream != null) { 31 | sb = stream.listen(_onPageTrackerEvent); 32 | // 应该在首次注册回调事件的时候触发首次页面曝光 33 | // 这样即使PageView组件随着焦点离开被销毁,也能发页面曝光事件 34 | _onPageTrackerEvent(PageTrackerEvent.PageView); 35 | } 36 | } 37 | } 38 | 39 | void _onPageTrackerEvent(PageTrackerEvent event) { 40 | if (event == PageTrackerEvent.PageView) { 41 | if (!isPageView) { 42 | didPageView(); 43 | isPageView = true; 44 | } 45 | } else { 46 | if (isPageView) { 47 | didPageExit(); 48 | isPageView = false; 49 | } 50 | } 51 | } 52 | 53 | int get pageViewIndex => null; 54 | 55 | @override 56 | void didPageView() { 57 | try { 58 | super.didPageView(); 59 | subscribers.forEach((subscriber) { 60 | subscriber.didPageView(); 61 | }); 62 | } catch (err) { 63 | assert(() { 64 | throw err; 65 | }()); 66 | } 67 | } 68 | 69 | @override 70 | void didPageExit() { 71 | try { 72 | super.didPageExit(); 73 | subscribers.forEach((subscriber) { 74 | subscriber.didPageExit(); 75 | }); 76 | } catch (err) { 77 | assert(() { 78 | throw err; 79 | }()); 80 | } 81 | } 82 | 83 | // 子列表页面订阅页面事件 84 | void subscribe(PageTrackerAware pageTrackerAware) { 85 | subscribers.add(pageTrackerAware); 86 | } 87 | 88 | void unsubscribe(PageTrackerAware pageTrackerAware) { 89 | subscribers.remove(pageTrackerAware); 90 | } 91 | 92 | @override 93 | void dispose() { 94 | try { 95 | if (isPageView) 96 | didPageExit(); 97 | sb?.cancel(); 98 | } catch (err) { 99 | rethrow; 100 | } finally { 101 | super.dispose(); 102 | } 103 | } 104 | 105 | static PageViewListenerWrapperState of(BuildContext context) { 106 | return context.ancestorStateOfType(TypeMatcher()); 107 | } 108 | } 109 | 110 | typedef onPageLoadedCallback = void Function(Duration, Duration, Duration, Duration); 111 | 112 | // 列表项中还可以再次嵌套列表,所以[PageViewListenerWrapper]需要把 113 | class PageViewListenerWrapper extends StatefulWidget { 114 | 115 | final int index; 116 | final bool hasRequest; 117 | final Widget child; 118 | final VoidCallback onPageView; 119 | final VoidCallback onPageExit; 120 | final onPageLoadedCallback onPageLoaded; 121 | 122 | const PageViewListenerWrapper(this.index, { 123 | Key key, 124 | this.hasRequest = false, 125 | this.child, 126 | this.onPageView, 127 | this.onPageExit, 128 | this.onPageLoaded, 129 | }): super(key: key); 130 | 131 | @override 132 | PageViewListenerWrapperState createState() { 133 | return PageViewListenerWrapperState(); 134 | } 135 | 136 | } 137 | 138 | class PageViewListenerWrapperState extends State with PageTrackerAware, PageViewListenerMixin, PageLoadMixin { 139 | 140 | @override 141 | Widget build(BuildContext context) { 142 | return widget.child; 143 | } 144 | 145 | 146 | @override 147 | int get pageViewIndex => widget.index; 148 | 149 | @override 150 | void didPageView() { 151 | try { 152 | super.didPageView(); 153 | if (widget.onPageView != null) { 154 | widget.onPageView(); 155 | } 156 | } catch (err) { 157 | assert(() { 158 | throw err; 159 | }()); 160 | } 161 | } 162 | 163 | @override 164 | void didPageExit() { 165 | try { 166 | super.didPageExit(); 167 | if (widget.onPageExit != null) { 168 | widget.onPageExit(); 169 | } 170 | } catch (err) { 171 | assert(() { 172 | throw err; 173 | }()); 174 | } 175 | } 176 | 177 | @override 178 | void didPageLoaded(Duration totalTime, Duration buildTime, Duration requestTime, Duration renderTime) { 179 | try { 180 | if (widget.onPageLoaded != null) { 181 | widget.onPageLoaded(totalTime, buildTime, requestTime, renderTime); 182 | } 183 | } catch (err) { 184 | assert(() { 185 | throw err; 186 | }()); 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /lib/src/page_view_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | 4 | import 'tracker_page_mixin.dart'; 5 | import 'page_tracker_aware.dart'; 6 | 7 | enum PageTrackerEvent {PageView, PageExit} 8 | 9 | // 包裹在PageView TabView组件之外,用于监听Tab切换事件 10 | class PageViewWrapper extends StatefulWidget { 11 | 12 | final int pageAmount; 13 | final int initialPage; 14 | final Widget child; 15 | final ChangeDelegate changeDelegate; 16 | 17 | const PageViewWrapper({ 18 | Key key, 19 | this.pageAmount = 0, 20 | this.initialPage = 0, 21 | this.child, 22 | this.changeDelegate, 23 | }): 24 | assert(pageAmount != null), 25 | super(key: key); 26 | 27 | @override 28 | PageViewWrapperState createState() { 29 | 30 | return PageViewWrapperState(); 31 | } 32 | 33 | // 用于子页面事件监听 34 | static Stream of(BuildContext context, int index) { 35 | try { 36 | assert(index >= 0); 37 | List> broadCaseStreams = (context 38 | .ancestorStateOfType( 39 | TypeMatcher()) as PageViewWrapperState) 40 | .broadCaseStreams; 41 | assert(index < broadCaseStreams.length); 42 | 43 | return broadCaseStreams[index]; 44 | } catch (err) { 45 | return null; 46 | } 47 | } 48 | } 49 | 50 | class PageViewWrapperState extends State with PageTrackerAware, TrackerPageMixin { 51 | 52 | List> controllers = []; 53 | List> broadCaseStreams = []; 54 | // 上一次打开的Page 55 | int currPageIndex; 56 | // 监听子页面控制器 57 | StreamSubscription pageChangeSB; 58 | 59 | void _createController(int index) { 60 | controllers[index] = StreamController(sync: true, onCancel: () { 61 | _createController(index); 62 | }); 63 | broadCaseStreams[index] = controllers[index].stream.asBroadcastStream(); 64 | } 65 | 66 | 67 | @override 68 | void initState() { 69 | super.initState(); 70 | 71 | currPageIndex = widget.initialPage; 72 | 73 | // 创建streams 74 | controllers = List(widget.pageAmount); 75 | broadCaseStreams = List(widget.pageAmount); 76 | for(int i=0; i= 0) 136 | controllers[index].add(event); 137 | } 138 | } 139 | 140 | // Tab切换事件接口 141 | abstract class ChangeDelegate { 142 | StreamController streamController; 143 | Stream stream; 144 | 145 | ChangeDelegate() { 146 | streamController = StreamController(sync: true); 147 | stream = streamController.stream.asBroadcastStream(); 148 | } 149 | 150 | void sendPageChange(int index) { 151 | streamController.add(index); 152 | } 153 | 154 | @protected 155 | void listen(); 156 | 157 | @protected 158 | void onChange(); 159 | 160 | @protected 161 | void dispose() { 162 | streamController?.close(); 163 | } 164 | } 165 | 166 | class PageViewChangeDelegate extends ChangeDelegate { 167 | 168 | PageController pageController; 169 | 170 | PageViewChangeDelegate(this.pageController): super(); 171 | 172 | @override 173 | void listen() { 174 | pageController.addListener(onChange); 175 | } 176 | 177 | @override 178 | void onChange() { 179 | if (0 != pageController.page % 1.0) { 180 | return; 181 | } 182 | 183 | sendPageChange(pageController.page.toInt()); 184 | } 185 | 186 | @override 187 | void dispose() { 188 | try { 189 | pageController.removeListener(onChange); 190 | } catch (err) { 191 | rethrow; 192 | } finally { 193 | super.dispose(); 194 | } 195 | } 196 | } 197 | 198 | class TabViewChangeDelegate extends ChangeDelegate { 199 | TabController tabController; 200 | int lastIndex; 201 | 202 | TabViewChangeDelegate(this.tabController): super(); 203 | 204 | @override 205 | void listen() { 206 | tabController.addListener(onChange); 207 | } 208 | 209 | @override 210 | void onChange() { 211 | if (tabController.index == lastIndex) { 212 | return; 213 | } else { 214 | lastIndex = tabController.index; 215 | sendPageChange(tabController.index); 216 | } 217 | } 218 | 219 | @override 220 | void dispose() { 221 | super.dispose(); 222 | } 223 | } -------------------------------------------------------------------------------- /lib/src/tracker_dialog_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'tracker_page_mixin.dart'; 3 | import 'page_tracker_aware.dart'; 4 | 5 | class TrackerDialogWrapper extends StatefulWidget { 6 | 7 | final Widget child; 8 | final VoidCallback didPageView; 9 | final VoidCallback didPageExit; 10 | 11 | const TrackerDialogWrapper({ 12 | Key key, 13 | this.child, 14 | this.didPageView, 15 | this.didPageExit, 16 | }): 17 | assert(child != null), 18 | super(key: key); 19 | 20 | @override 21 | _State createState() { 22 | return _State(); 23 | } 24 | } 25 | 26 | class _State extends State with PageTrackerAware, TrackerPageMixin { 27 | @override 28 | Widget build(BuildContext context) { 29 | return widget.child; 30 | } 31 | 32 | @override 33 | void didPageView() { 34 | super.didPageView(); 35 | 36 | if (widget.didPageView != null) { 37 | widget.didPageView(); 38 | } 39 | } 40 | 41 | @override 42 | void didPageExit() { 43 | super.didPageExit(); 44 | 45 | if (widget.didPageExit != null) { 46 | widget.didPageExit(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /lib/src/tracker_page_mixin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'page_tracker_aware.dart'; 4 | import 'tracker_route_observer.dart'; 5 | import 'page_view_listener_mixin.dart'; 6 | import 'tracker_route_observer_provider.dart'; 7 | 8 | mixin TrackerPageMixin on State, PageTrackerAware { 9 | TrackerStackObserver _routeObserver; 10 | PageViewListenerWrapperState _pageViewListenerWrapperState; 11 | 12 | @override 13 | void didChangeDependencies() { 14 | super.didChangeDependencies(); 15 | 16 | if (_pageViewListenerWrapperState != null) { 17 | return; 18 | } 19 | 20 | // 订阅PageView组件中的事件 21 | _pageViewListenerWrapperState = PageViewListenerMixin.of(context); 22 | if (_pageViewListenerWrapperState != null) { 23 | _pageViewListenerWrapperState.subscribe(this); 24 | return; 25 | } 26 | 27 | // 订阅本页面的事件 28 | if (_routeObserver != null) { 29 | return; 30 | } 31 | _routeObserver = TrackerRouteObserverProvider.of(context) 32 | ..subscribe( 33 | this, 34 | ModalRoute.of(context), 35 | ); 36 | } 37 | 38 | @override 39 | void dispose() { 40 | try { 41 | _routeObserver?.unsubscribe(this); 42 | _pageViewListenerWrapperState?.unsubscribe(this); 43 | } catch (err) { 44 | rethrow; 45 | } finally { 46 | super.dispose(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/tracker_page_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'tracker_page_mixin.dart'; 3 | import 'page_tracker_aware.dart'; 4 | 5 | // 废弃 6 | class TrackerPageWidget extends StatefulWidget { 7 | 8 | final Widget child; 9 | 10 | const TrackerPageWidget({Key key, this.child}): 11 | assert(child != null), 12 | super(key: key); 13 | 14 | @override 15 | _State createState() { 16 | return _State(); 17 | } 18 | } 19 | 20 | class _State extends State with PageTrackerAware, TrackerPageMixin { 21 | @override 22 | Widget build(BuildContext context) { 23 | return widget.child; 24 | } 25 | 26 | @override 27 | void didPageView() { 28 | super.didPageView(); 29 | 30 | print("pageview"); 31 | } 32 | 33 | @override 34 | void didPageExit() { 35 | super.didPageExit(); 36 | print("didPageExit"); 37 | } 38 | } -------------------------------------------------------------------------------- /lib/src/tracker_route_observer.dart: -------------------------------------------------------------------------------- 1 | // 监控路由状态,并保存路由栈 2 | import 'package:flutter/material.dart'; 3 | import 'page_tracker_aware.dart'; 4 | 5 | class TrackerStackObserver> extends NavigatorObserver { 6 | 7 | // 存放路由堆栈 8 | final List routes = []; 9 | final List routesPopup = []; 10 | // 每个页面对应的监听 11 | final Map> _listeners = >{}; 12 | final Map> _listenersPopup = >{}; 13 | 14 | void subscribe(PageTrackerAware pageTrackerAware, R route) { 15 | assert(pageTrackerAware != null); 16 | assert(route != null); 17 | 18 | if (route is PageRoute) { 19 | final Set subscribers = _listeners.putIfAbsent( 20 | route, () => Set()); 21 | if (subscribers.add(pageTrackerAware)) { 22 | pageTrackerAware.didPageView(); 23 | } 24 | } else if (route is PopupRoute) { 25 | final Set subscribers = _listenersPopup.putIfAbsent( 26 | route, () => Set()); 27 | if (subscribers.add(pageTrackerAware)) { 28 | pageTrackerAware.didPageView(); 29 | } 30 | } 31 | } 32 | 33 | void unsubscribe(PageTrackerAware pageTrackerAware) { 34 | assert(pageTrackerAware != null); 35 | 36 | for (R route in _listeners.keys) { 37 | final Set subscribers = _listeners[route]; 38 | subscribers?.remove(pageTrackerAware); 39 | } 40 | 41 | for (R route in _listenersPopup.keys) { 42 | final Set subscribers = _listenersPopup[route]; 43 | subscribers?.remove(pageTrackerAware); 44 | } 45 | } 46 | 47 | @override 48 | void didPush(Route route, Route previousRoute) { 49 | super.didPush(route, previousRoute); 50 | 51 | // 之后正常页面(非弹窗)入栈之后,才会触发前一个页面的离开 52 | if (route is PageRoute) { 53 | // 触发PageExit事件 54 | if (routes.length > 0) { 55 | R previousRoute = routes.last; 56 | final Set previousSubscribers = _listeners[previousRoute]; 57 | if (previousSubscribers != null) { 58 | for (PageTrackerAware pageTrackerAware in previousSubscribers) { 59 | pageTrackerAware.didPageExit(); 60 | } 61 | } 62 | } 63 | 64 | // 存储在路由栈上 65 | routes.add(route); 66 | 67 | // 触发相关PageView事件,在订阅的时候触发 68 | /*final Set subscribers = _listeners[route]; 69 | if (subscribers != null) { 70 | for (PageTrackerAware pageTrackerAware in subscribers) { 71 | pageTrackerAware.didPageView(); 72 | } 73 | }*/ 74 | } else if (route is PopupRoute) { 75 | // 存储在路由栈上 76 | routesPopup.add(route); 77 | } 78 | } 79 | 80 | @override 81 | void didPop(Route route, Route previousRoute) { 82 | super.didPop(route, previousRoute); 83 | 84 | if (route is PageRoute) { 85 | // 触发PageExit 86 | final Set subscribers = _listeners[route]; 87 | if (subscribers != null) { 88 | for (PageTrackerAware pageTrackerAware in subscribers) { 89 | pageTrackerAware.didPageExit(); 90 | } 91 | } 92 | 93 | // 清除路由栈 94 | routes.removeLast(); 95 | 96 | // 触发PageView 97 | if (routes.length > 0) { 98 | R previousRoute = routes.last; 99 | final Set previousSubscribers = _listeners[previousRoute]; 100 | if (previousSubscribers != null) { 101 | Future.microtask(() { 102 | for (PageTrackerAware pageTrackerAware in previousSubscribers) { 103 | pageTrackerAware.didPageView(); 104 | } 105 | }); 106 | } 107 | } 108 | } else if (route is PopupRoute) { 109 | // 触发PageExit 110 | final Set subscribers = _listenersPopup[route]; 111 | if (subscribers != null) { 112 | for (PageTrackerAware pageTrackerAware in subscribers) { 113 | pageTrackerAware.didPageExit(); 114 | } 115 | } 116 | 117 | // 清除路由栈 118 | routesPopup.remove(route); 119 | } 120 | } 121 | 122 | @override 123 | void didRemove(Route route, Route previousRoute) { 124 | super.didRemove(route, previousRoute); 125 | 126 | if (route is PageRoute) { 127 | routes.remove(route); 128 | } else if (route is PopupRoute) { 129 | routesPopup.remove(route); 130 | } 131 | } 132 | 133 | @override 134 | void didReplace({Route newRoute, Route oldRoute}) { 135 | super.didReplace(newRoute: newRoute, oldRoute: oldRoute); 136 | 137 | if (oldRoute is PageRoute) { 138 | int index = routes.indexOf(oldRoute); 139 | assert(index != -1); 140 | routes.removeAt(index); 141 | 142 | routes.insert(index, newRoute); 143 | } else if (oldRoute is PopupRoute) { 144 | int index = routesPopup.indexOf(oldRoute); 145 | assert(index != -1); 146 | routesPopup.removeAt(index); 147 | 148 | routesPopup.insert(index, newRoute); 149 | } 150 | } 151 | 152 | } -------------------------------------------------------------------------------- /lib/src/tracker_route_observer_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'tracker_route_observer.dart'; 3 | 4 | class TrackerRouteObserverProvider extends InheritedWidget { 5 | 6 | final TrackerStackObserver trackerStackObserver = TrackerStackObserver(); 7 | 8 | TrackerRouteObserverProvider({ 9 | Key key, 10 | @required Widget child, 11 | }): super(key: key, child: child); 12 | 13 | @override 14 | bool updateShouldNotify(InheritedWidget oldWidget) { 15 | return false; 16 | } 17 | 18 | static TrackerStackObserver of(BuildContext context) { 19 | try { 20 | return (context.inheritFromWidgetOfExactType( 21 | TrackerRouteObserverProvider) as TrackerRouteObserverProvider) 22 | .trackerStackObserver; 23 | } catch (err) { 24 | return null; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | flutter packages pub publish --server=https://pub.dartlang.org -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.2.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.0.4" 18 | charcode: 19 | dependency: transitive 20 | description: 21 | name: charcode 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.1.2" 25 | collection: 26 | dependency: transitive 27 | description: 28 | name: collection 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.14.11" 32 | flutter: 33 | dependency: "direct main" 34 | description: flutter 35 | source: sdk 36 | version: "0.0.0" 37 | flutter_test: 38 | dependency: "direct dev" 39 | description: flutter 40 | source: sdk 41 | version: "0.0.0" 42 | fluttertoast: 43 | dependency: "direct main" 44 | description: 45 | name: fluttertoast 46 | url: "https://pub.flutter-io.cn" 47 | source: hosted 48 | version: "2.2.11" 49 | matcher: 50 | dependency: transitive 51 | description: 52 | name: matcher 53 | url: "https://pub.flutter-io.cn" 54 | source: hosted 55 | version: "0.12.5" 56 | meta: 57 | dependency: transitive 58 | description: 59 | name: meta 60 | url: "https://pub.flutter-io.cn" 61 | source: hosted 62 | version: "1.1.6" 63 | path: 64 | dependency: transitive 65 | description: 66 | name: path 67 | url: "https://pub.flutter-io.cn" 68 | source: hosted 69 | version: "1.6.2" 70 | pedantic: 71 | dependency: transitive 72 | description: 73 | name: pedantic 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "1.7.0" 77 | quiver: 78 | dependency: transitive 79 | description: 80 | name: quiver 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "2.0.3" 84 | sky_engine: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.99" 89 | source_span: 90 | dependency: transitive 91 | description: 92 | name: source_span 93 | url: "https://pub.flutter-io.cn" 94 | source: hosted 95 | version: "1.5.5" 96 | stack_trace: 97 | dependency: transitive 98 | description: 99 | name: stack_trace 100 | url: "https://pub.flutter-io.cn" 101 | source: hosted 102 | version: "1.9.3" 103 | stream_channel: 104 | dependency: transitive 105 | description: 106 | name: stream_channel 107 | url: "https://pub.flutter-io.cn" 108 | source: hosted 109 | version: "2.0.0" 110 | string_scanner: 111 | dependency: transitive 112 | description: 113 | name: string_scanner 114 | url: "https://pub.flutter-io.cn" 115 | source: hosted 116 | version: "1.0.4" 117 | term_glyph: 118 | dependency: transitive 119 | description: 120 | name: term_glyph 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "1.1.0" 124 | test_api: 125 | dependency: transitive 126 | description: 127 | name: test_api 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "0.2.5" 131 | typed_data: 132 | dependency: transitive 133 | description: 134 | name: typed_data 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.1.6" 138 | vector_math: 139 | dependency: transitive 140 | description: 141 | name: vector_math 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "2.0.8" 145 | sdks: 146 | dart: ">=2.2.2 <3.0.0" 147 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_page_tracker 2 | description: flutter_page_tracker is a flutter plugin which help tracking route events of PageRoute/PopupRoute/PageView/TabView. Route events include PageView and PageExit. 3 | 4 | version: 3.0.7 5 | author: DavidTang 6 | homepage: https://github.com/SBDavid/flutter_page_tracker 7 | 8 | environment: 9 | sdk: ">=2.1.0 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | fluttertoast: ^7.0.0 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | 20 | # For information on the generic Dart part of this file, see the 21 | # following page: https://dart.dev/tools/pub/pubspec 22 | 23 | # The following section is specific to Flutter. 24 | flutter: 25 | 26 | # To add assets to your package, add an assets section, like this: 27 | # assets: 28 | # - images/a_dot_burr.jpeg 29 | # - images/a_dot_ham.jpeg 30 | # 31 | # For details regarding assets in packages, see 32 | # https://flutter.dev/assets-and-images/#from-packages 33 | # 34 | # An image asset can refer to one or more resolution-specific "variants", see 35 | # https://flutter.dev/assets-and-images/#resolution-aware. 36 | 37 | # To add custom fonts to your package, add a fonts section here, 38 | # in this "flutter" section. Each entry in this list should have a 39 | # "family" key with the font family name, and a "fonts" key with a 40 | # list giving the asset and other descriptors for the font. For 41 | # example: 42 | # fonts: 43 | # - family: Schyler 44 | # fonts: 45 | # - asset: fonts/Schyler-Regular.ttf 46 | # - asset: fonts/Schyler-Italic.ttf 47 | # style: italic 48 | # - family: Trajan Pro 49 | # fonts: 50 | # - asset: fonts/TrajanPro.ttf 51 | # - asset: fonts/TrajanPro_Bold.ttf 52 | # weight: 700 53 | # 54 | # For details regarding fonts in packages, see 55 | # https://flutter.dev/custom-fonts/#from-packages 56 | -------------------------------------------------------------------------------- /tabview_event.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/tabview_event.jpg -------------------------------------------------------------------------------- /test/xm_flutter_tracker_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | void main() { 3 | test('adds one to input values', () { 4 | 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /管理TabView的事件.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/46ec5dfe4ab065ffbd255beb319bd1ace3299f17/管理TabView的事件.graffle --------------------------------------------------------------------------------