{"id":18000109,"url":"https://github.com/sbdavid/flutter_page_tracker","last_synced_at":"2025-04-11T14:31:20.919Z","repository":{"id":53086024,"uuid":"228313931","full_name":"SBDavid/flutter_page_tracker","owner":"SBDavid","description":"flutter埋点、弹窗埋点、页面埋点事件捕获框架，支持普通页面的页面曝光事件（PageView），页面离开事件（PageExit）。支持在TabView和PageView组件中发送页面曝光和页面离开","archived":false,"fork":false,"pushed_at":"2021-04-07T09:59:42.000Z","size":15114,"stargazers_count":112,"open_issues_count":9,"forks_count":21,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-07T04:11:16.287Z","etag":null,"topics":["dialog","flutter","pageview","tabview","tracker"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SBDavid.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-12-16T06:02:06.000Z","updated_at":"2024-10-22T03:25:12.000Z","dependencies_parsed_at":"2022-09-12T12:12:23.267Z","dependency_job_id":null,"html_url":"https://github.com/SBDavid/flutter_page_tracker","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SBDavid%2Fflutter_page_tracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SBDavid%2Fflutter_page_tracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SBDavid%2Fflutter_page_tracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SBDavid%2Fflutter_page_tracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SBDavid","download_url":"https://codeload.github.com/SBDavid/flutter_page_tracker/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248419680,"owners_count":21100223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dialog","flutter","pageview","tabview","tracker"],"created_at":"2024-10-29T23:09:35.277Z","updated_at":"2025-04-11T14:31:15.912Z","avatar_url":"https://github.com/SBDavid.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# flutter_page_tracker\n\n## 简介\nFlutterPageTracker 是一个易用的 Flutter 应用页面事件埋点插件。它不仅支持在普通导航事件中监听页面曝光和离开，也支持弹窗的曝光和离开。\n\n针对 TabView（PageView）形式的首页，FlutterPageTracker 可以监听每一个Tab的曝光和离开，并且把Tab形式的页面和普通页面衔接起来。\n\n针对TabView和PageView组件相互嵌套的情况，FlutterPageTracker 可以对每一级嵌套分别监控埋点事件，大大提升埋点的效率。\n\n它具有以下特性：\n\n- 1.监听普通页面的`露出`和`离开`事件（PageRoute），\n    - 当前页面入栈会触发当前页面的`曝光事件`和前一个页面的`离开事件`。\n    - 当前页面出栈会触发当前页面的`离开事件`和前一个页面的`曝光事件`。\n    - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/1PageRoute.gif)\n- 2.监听对话框的`露出`和`离开`（PopupRoute），\n    - 它和PageRoute的区别是，当前对话框的露出和关闭不会触发前一个页面的`露出`、`离开`事件\n    - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/2PopupRoute.gif)\n- 3.监听PageView、TabView组件的`切换`事件\n    - 当一个PageView或者TabView`入栈`时，前一个页面会触发页面`离开事件`\n    - 当一个PageView或者TabView`出栈`时，前一个页面会触发页面`曝光事件`\n    - 当焦点页面发生变化时，旧的页面触发页面露出，新的页面触发PageView\n    - PageView组件\n        - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/3PageView.gif)\n    - TabView组件\n        - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/4TabView.gif)\n- 4.PageView和TabView嵌套使用\n    - 我们可以将这两种组件嵌套在一起使用，不限制嵌套的层次\n    - 发生焦点变化的PageView（或者TabView）以及它的子级都会受到`曝光事件`和`离开事件`\n    - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/gifs/5PageViewInTabView.gif)\n- 5.滑动曝光事件\n    - 如果你对列表的滑动露出事件感兴趣，你可以参考flutter_sliver_tracker插件\n    - `https://github.com/SBDavid/flutter_sliver_tracker`\n    - ![demo](https://raw.githubusercontent.com/SBDavid/flutter_sliver_tracker/master/demo.gif)\n    \n## 运行Demo程序\n\n- 克隆代码到本地: git clone git@github.com:SBDavid/flutter_page_tracker.git\n- 切换工作路径: cd flutter_page_tracker/example/\n- 启动模拟器\n- 运行: flutter run\n\n## 使用\n\n### 1. 安装\n```yaml\ndependencies:\n  flutter_page_tracker: ^1.2.2\n```\n\n### 2. 引入flutter_page_tracker\n```dart\nimport 'package:flutter_page_tracker/flutter_page_tracker.dart';\n```\n\n### 3. 发送普通页面埋点事件\n\n#### 3.1 添加路由监听\n```dart\nvoid main() =\u003e runApp(\n  TrackerRouteObserverProvider(\n    child: MyApp(),\n  )\n);\n```\n\n#### 3.2 添加路由事件监听\n```dart\nclass MyApp extends StatelessWidget {\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      // 添加路由事件监听\n      navigatorObservers: [TrackerRouteObserverProvider.of(context)],\n      home: MyHomePage(title: 'Flutter_Page_tracker Demo'),\n    );\n  }\n}\n```\n\n#### 3.3 在组件中发送埋点事件\n\n必须使用`PageTrackerAware`和`TrackerPageMixin`这两个mixin\n\n```dart\nclass HomePageState extends State\u003cMyHomePage\u003e with PageTrackerAware, TrackerPageMixin {\n    @override\n    Widget build(BuildContext context) {\n        return Container();\n    }\n\n    @override\n    void didPageView() {\n        super.didPageView();\n        // 发送页面露出事件\n    }\n\n    @override\n    void didPageExit() {\n        super.didPageExit();\n        // 发送页面离开事件\n    }\n}\n```\n\n#### 3.4 Dialog的埋点\n```dart\nclass PopupPage extends StatelessWidget {\n\n  @override\n  Widget build(BuildContext context) {\n    return TrackerDialogWrapper(\n     didPageView: () {\n       print('dialog didPageView');\n     },\n     didPageExit: () {\n       print('dialog didPageExit');\n     },\n     child: SimpleDialog(\n       children: \u003cWidget\u003e[\n         // body\n       ],\n     ),\n   );\n  }\n}\n```\n\n#### 3.5 PageView发送埋点事件\n\n在`StatefulWidget`中，推荐直接使用`PageViewListenerMixin`发送页面事件，如果是`StatelessWidget`组件则可以使用`PageViewListenerWrapper`。\n`PageViewListenerWrapper`内部也是使用`PageViewListenerMixin`来发送事件。\n\n```dart\n\n// 嵌入到PageView组件中页面\nclass Page extends StatefulWidget {\n  final int index;\n\n  const Page({Key key, this.index}): super(key: key);\n\n  @override\n  PageState createState() {\n    return PageState();\n  }\n}\n\nclass PageState extends State\u003cPage\u003e with PageTrackerAware, PageViewListenerMixin {\n\n  int get pageViewIndex =\u003e widget.index;\n\n  @override\n  void didPageView() {\n    super.didPageView();\n    // 页面曝光事件\n  }\n\n  @override\n  void didPageExit() {\n    super.didPageExit();\n    // 页面离开事件\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container();\n  }\n}\n\n// PageView组件\nclass PageViewMixinPage extends StatefulWidget {\n\n  @override\n  PageViewMixinPageState createState() =\u003e PageViewMixinPageState();\n}\n\nclass PageViewMixinPageState extends State\u003cPageViewMixinPage\u003e {\n\n  PageController pageController;\n\n  @override\n  void initState() {\n    super.initState();\n    pageController = PageController();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return PageViewWrapper(\n       changeDelegate: PageViewChangeDelegate(pageController),\n       pageAmount: 3,\n       initialPage: pageController.initialPage,\n       child: PageView(\n         controller: pageController,\n         children: \u003cWidget\u003e[\n           Page(index: 0,),\n           Page(index: 1,),\n           Page(index: 3,),\n         ],\n       ),\n     );\n  }\n}\n```\n\n#### 3.6 TabView发送埋点事件\n\n在这个例子中我们只用`PageViewListenerWrapper`来发送页面事件，我们也可以向例子3.3中一样使用直接使用`PageViewListenerMixin`。\n在`StatefulWidget`中，荐使用`mixin`更简洁。\n\n```dart\nclass TabViewPage extends StatefulWidget {\n  TabViewPage({Key key,}) : super(key: key);\n\n  @override\n  _State createState() =\u003e _State();\n}\n\nclass _State extends State\u003cTabViewPage\u003e with TickerProviderStateMixin {\n  TabController tabController = TabController(initialIndex: 0, length: 3, vsync: this);\n\n  @override\n  Widget build(BuildContext context) {\n\n    return Scaffold(\n        // 添加TabView的包裹层\n        body: PageViewWrapper(\n          // Tab页数量\n          pageAmount: 3,\n          // 初始Tab下标\n          initialPage: 0, \n          // 监听Tab onChange事件\n          changeDelegate: TabViewChangeDelegate(tabController),\n          child: TabBarView(\n            controller: tabController,\n            children: \u003cWidget\u003e[\n              Builder(\n                builder: (_) {\n                  // 监听由PageViewWrapper转发的PageView，PageExit事件\n                  return PageViewListenerWrapper(\n                    0,\n                    onPageView: () {\n                      // 发送页面曝光事件\n                    },\n                    onPageExit: () {\n                      // 发送页面离开事件\n                    },\n                    child: Container(),\n                  );\n                },\n              ),\n              // 第二个Tab\n              // 第三个Tab\n            ],\n          ),\n        ),\n    );\n  }\n}\n```\n\n#### 3.7 TabView中嵌套PageView（PageView也可以嵌套TabView，TabView也可以嵌套TabView）\n\n在这个例子中我们只用`PageViewListenerWrapper`来发送页面事件，我们也可以向例子3.5中一样使用直接使用`PageViewListenerMixin`。\n在`StatefulWidget`中，荐使用`mixin`更简洁。\n\n```dart\nclass PageViewInTabViewPage extends StatefulWidget {\n\n  @override\n  _State createState() =\u003e _State();\n}\n\nclass _State extends State\u003cPageViewInTabViewPage\u003e with TickerProviderStateMixin {\n\n  TabController tabController;\n  PageController pageController;\n\n  @override\n  void initState() {\n    super.initState();\n    tabController = TabController(initialIndex: 0, length: 3, vsync: this);\n    pageController = PageController();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n\n    return Scaffold(\n        // 外层TabView\n        body: PageViewWrapper(\n          pageAmount: 3, // 子Tab数量\n          initialPage: 0, // 首个展现的Tab序号\n          changeDelegate: TabViewChangeDelegate(tabController),\n          child: TabBarView(\n            controller: tabController,\n            children: \u003cWidget\u003e[\n              Builder(\n                builder: (BuildContext context) {\n                  // 转发上层的事件\n                  return PageViewListenerWrapper(\n                      0,\n                      // 内层PageView\n                      child: PageViewWrapper(\n                        changeDelegate: PageViewChangeDelegate(pageController),\n                        pageAmount: 3,\n                        initialPage: pageController.initialPage,\n                        child: PageView(\n                          controller: pageController,\n                          children: \u003cWidget\u003e[\n                            PageViewListenerWrapper(\n                              0,\n                              onPageView: () {\n                                // 页面露出事件\n                              },\n                              onPageExit: () {\n                                // 页面离开事件\n                              },\n                              child: Container()\n                            ),\n                            // PageView中的第二个页面\n                            // PageView中的第三个页面\n                          ],\n                        ),\n                      )\n                  );\n                },\n              ),\n              // tab2\n              // tab3\n            ],\n          ),\n        )\n    );\n  }\n}\n```\n\n## 原理篇\n###  1.概述\n\n页面的埋点追踪通常处于业务开发的最后一环，留给埋点的开发时间通常并不充裕，但是埋点数据对于后期的产品调整有重要的意义，所以一个稳定高效的埋点框架是非常重要的。\n\n### 2. 我们期望埋点框架所具备的功能\n\n#### 2.1 PageView，PageExit事件\n我们期望当调用`Navigator.of(context).pushNamed(\"XXX Page\");`时，首先对之前的页面发送`PageExit`，然后对当前页面发送`PageView`事件。当调用`Navigator.of(context).pop();`时则，首先发送当前页面的`PageExit`事件，再发送之前页面的`PageView`事件。\n\n我们首先想到的是使用[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页面 -\u003e A页面上的弹窗 -\u003e B页面，但是在这个过程中A页面的`PageExit`事件没有发送。\n\n所以我们必须自己管理路由栈，这样判断不同路由的类型，并控制事件的顺序。详细实现方案在后面展开。\n\n#### 2.2 TagView组件于PageView组件\n这两个组件虽然与Flutter的路由无关，但是在产品经理眼中它们任属于页面。并且当Tab发生首次曝光和切换的时候我们都需要发送埋点事件。\n\n例如当Tab页A首次曝光时，我们首先发送上一个页面的`PageExit`事件，然后发送TabA的`PageView`事件。当我们从TabA切换到TabB的时候，先发送TabA的`PageExit`事件，然后发送TabB的`PageView`事件。当我们push一个新的路由时，需要发送TabB的`PageExit`事件。\n\n这套流程需要Tab页和普通页面之间通过事件机制来交互，如果直接把这套机制搬到业务代码中，那么业务代码中就会包含大量与业务无关并且重复的代码。详细的抽象方案见后文。\n\n### 3. 解决这些问题\n\n#### 3.1 解决PageView，PageExit的顺序问题\n[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`事件。\n\n#### 3.2 避免弹窗的干扰（例如Dialog）\n在[RouteObserver.didPop(Route\u003cdynamic\u003e route, Route\u003cdynamic\u003e previousRoute)](https://github.com/flutter/flutter/blob/c06bf6503a8b6690b2740a7101852fcc8f133057/packages/flutter/lib/src/widgets/routes.dart#L1456)中，我们可以通过previousRoute找到上一个路由，并更具它来发送上一个路由的PageView事件。但是如果上一个路由是`Dialog`，就会造成错误，因为我们实际想要的是包含这个`Dialog`的路由。\n\n要解决这个问题我们必须自己维护一个路由栈，这样当`didPop`触发时我们就可以找到真正的上一个路由。请参考这一段[代码](https://github.com/SBDavid/flutter_tracker/blob/4fb20ad03fd63300f5b33a92fc38fcd4f7a8fa45/lib/src/tracker_route_observer.dart#L36)，这里的`routes`是当前的路由栈。\n\n#### 3.3 如何上报TabView中的埋点事件，并和其它页面串联起来\n这个问题可以分解为两个小问题：\n- 1. 如何把TabView页面和普通的路由进行串联？\n- 2. 当Tab发生切换时如何发送埋点事件？\n\n为了解决这两个问题，我们需要一个容器来管理tab页面的状态并且承载事件转发的任务。详见下图:\n![管理TabView中的事件](https://raw.githubusercontent.com/SBDavid/flutter_page_tracker/master/tabview_event.jpg)。\n\n其中TabsWrapper会监听来自Flutter的路由事件，并转发给当前曝光的Tab，这就可以解决了问题一。\n\n同时TabsWrappe也会包含一个`TabController`和上一个被打开的Tab索引，TabsWrappe会监听来自`TabController`的onChange(index)事件，并把事件转发给对应的tab，这就解决了问题二。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsbdavid%2Fflutter_page_tracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsbdavid%2Fflutter_page_tracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsbdavid%2Fflutter_page_tracker/lists"}