flutter_page_tracker 3.0.6

Flutter Android iOS web

flutter_page_tracker is a flutter plugin which help tracking route events of PageRoute/PopupRoute/PageView/TabView. Route events include PageView and PageExit.

flutter_page_tracker #

简介 #

FlutterPageTracker 是一个易用的 Flutter 应用页面事件埋点插件。它不仅支持在普通导航事件中监听页面曝光和离开,也支持弹窗的曝光和离开。

针对 TabView(PageView)形式的首页,FlutterPageTracker 可以监听每一个Tab的曝光和离开,并且把Tab形式的页面和普通页面衔接起来。

针对TabView和PageView组件相互嵌套的情况,FlutterPageTracker 可以对每一级嵌套分别监控埋点事件,大大提升埋点的效率。

它具有以下特性:

  • 1.监听普通页面的露出离开事件(PageRoute),
    • 当前页面入栈会触发当前页面的曝光事件和前一个页面的离开事件
    • 当前页面出栈会触发当前页面的离开事件和前一个页面的曝光事件
    • demo
  • 2.监听对话框的露出离开(PopupRoute),
    • 它和PageRoute的区别是,当前对话框的露出和关闭不会触发前一个页面的露出离开事件
    • demo
  • 3.监听PageView、TabView组件的切换事件
    • 当一个PageView或者TabView入栈时,前一个页面会触发页面离开事件
    • 当一个PageView或者TabView出栈时,前一个页面会触发页面曝光事件
    • 当焦点页面发生变化时,旧的页面触发页面露出,新的页面触发PageView
    • PageView组件
      • demo
    • TabView组件
      • demo
  • 4.PageView和TabView嵌套使用
    • 我们可以将这两种组件嵌套在一起使用,不限制嵌套的层次
    • 发生焦点变化的PageView(或者TabView)以及它的子级都会受到曝光事件离开事件
    • demo
  • 5.滑动曝光事件
    • 如果你对列表的滑动露出事件感兴趣,你可以参考flutter_sliver_tracker插件
    • https://github.com/SBDavid/flutter_sliver_tracker
    • demo

运行Demo程序 #

  • 克隆代码到本地: git clone git@github.com:SBDavid/flutter_page_tracker.git
  • 切换工作路径: cd flutter_page_tracker/example/
  • 启动模拟器
  • 运行: flutter run

使用 #

1. 安装 #

dependencies:
  flutter_page_tracker: ^1.2.2

2. 引入flutter_page_tracker #

import 'package:flutter_page_tracker/flutter_page_tracker.dart';

3. 发送普通页面埋点事件 #

3.1 添加路由监听

void main() => runApp(
  TrackerRouteObserverProvider(
    child: MyApp(),
  )
);

3.2 添加路由事件监听

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 添加路由事件监听
      navigatorObservers: [TrackerRouteObserverProvider.of(context)],
      home: MyHomePage(title: 'Flutter_Page_tracker Demo'),
    );
  }
}

3.3 在组件中发送埋点事件

必须使用PageTrackerAwareTrackerPageMixin这两个mixin

class HomePageState extends State<MyHomePage> with PageTrackerAware, TrackerPageMixin {
    @override
    Widget build(BuildContext context) {
        return Container();
    }

    @override
    void didPageView() {
        super.didPageView();
        // 发送页面露出事件
    }

    @override
    void didPageExit() {
        super.didPageExit();
        // 发送页面离开事件
    }
}

3.4 Dialog的埋点

class PopupPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return TrackerDialogWrapper(
     didPageView: () {
       print('dialog didPageView');
     },
     didPageExit: () {
       print('dialog didPageExit');
     },
     child: SimpleDialog(
       children: <Widget>[
         // body
       ],
     ),
   );
  }
}

3.5 PageView发送埋点事件

StatefulWidget中,推荐直接使用PageViewListenerMixin发送页面事件,如果是StatelessWidget组件则可以使用PageViewListenerWrapperPageViewListenerWrapper内部也是使用PageViewListenerMixin来发送事件。


// 嵌入到PageView组件中页面
class Page extends StatefulWidget {
  final int index;

  const Page({Key key, this.index}): super(key: key);

  @override
  PageState createState() {
    return PageState();
  }
}

class PageState extends State<Page> with PageTrackerAware, PageViewListenerMixin {

  int get pageViewIndex => widget.index;

  @override
  void didPageView() {
    super.didPageView();
    // 页面曝光事件
  }

  @override
  void didPageExit() {
    super.didPageExit();
    // 页面离开事件
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

// PageView组件
class PageViewMixinPage extends StatefulWidget {

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

class PageViewMixinPageState extends State<PageViewMixinPage> {

  PageController pageController;

  @override
  void initState() {
    super.initState();
    pageController = PageController();
  }

  @override
  Widget build(BuildContext context) {
    return PageViewWrapper(
       changeDelegate: PageViewChangeDelegate(pageController),
       pageAmount: 3,
       initialPage: pageController.initialPage,
       child: PageView(
         controller: pageController,
         children: <Widget>[
           Page(index: 0,),
           Page(index: 1,),
           Page(index: 3,),
         ],
       ),
     );
  }
}

3.6 TabView发送埋点事件

在这个例子中我们只用PageViewListenerWrapper来发送页面事件,我们也可以向例子3.3中一样使用直接使用PageViewListenerMixin。 在StatefulWidget中,荐使用mixin更简洁。

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

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

class _State extends State<TabViewPage> with TickerProviderStateMixin {
  TabController tabController = TabController(initialIndex: 0, length: 3, vsync: this);

  @override
  Widget build(BuildContext context) {

    return Scaffold(
        // 添加TabView的包裹层
        body: PageViewWrapper(
          // Tab页数量
          pageAmount: 3,
          // 初始Tab下标
          initialPage: 0, 
          // 监听Tab onChange事件
          changeDelegate: TabViewChangeDelegate(tabController),
          child: TabBarView(
            controller: tabController,
            children: <Widget>[
              Builder(
                builder: (_) {
                  // 监听由PageViewWrapper转发的PageView,PageExit事件
                  return PageViewListenerWrapper(
                    0,
                    onPageView: () {
                      // 发送页面曝光事件
                    },
                    onPageExit: () {
                      // 发送页面离开事件
                    },
                    child: Container(),
                  );
                },
              ),
              // 第二个Tab
              // 第三个Tab
            ],
          ),
        ),
    );
  }
}

3.7 TabView中嵌套PageView(PageView也可以嵌套TabView,TabView也可以嵌套TabView)

在这个例子中我们只用PageViewListenerWrapper来发送页面事件,我们也可以向例子3.5中一样使用直接使用PageViewListenerMixin。 在StatefulWidget中,荐使用mixin更简洁。

class PageViewInTabViewPage extends StatefulWidget {

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

class _State extends State<PageViewInTabViewPage> with TickerProviderStateMixin {

  TabController tabController;
  PageController pageController;

  @override
  void initState() {
    super.initState();
    tabController = TabController(initialIndex: 0, length: 3, vsync: this);
    pageController = PageController();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
        // 外层TabView
        body: PageViewWrapper(
          pageAmount: 3, // 子Tab数量
          initialPage: 0, // 首个展现的Tab序号
          changeDelegate: TabViewChangeDelegate(tabController),
          child: TabBarView(
            controller: tabController,
            children: <Widget>[
              Builder(
                builder: (BuildContext context) {
                  // 转发上层的事件
                  return PageViewListenerWrapper(
                      0,
                      // 内层PageView
                      child: PageViewWrapper(
                        changeDelegate: PageViewChangeDelegate(pageController),
                        pageAmount: 3,
                        initialPage: pageController.initialPage,
                        child: PageView(
                          controller: pageController,
                          children: <Widget>[
                            PageViewListenerWrapper(
                              0,
                              onPageView: () {
                                // 页面露出事件
                              },
                              onPageExit: () {
                                // 页面离开事件
                              },
                              child: Container()
                            ),
                            // PageView中的第二个页面
                            // PageView中的第三个页面
                          ],
                        ),
                      )
                  );
                },
              ),
              // tab2
              // tab3
            ],
          ),
        )
    );
  }
}

原理篇 #

1.概述 #

页面的埋点追踪通常处于业务开发的最后一环,留给埋点的开发时间通常并不充裕,但是埋点数据对于后期的产品调整有重要的意义,所以一个稳定高效的埋点框架是非常重要的。

2. 我们期望埋点框架所具备的功能 #

2.1 PageView,PageExit事件

我们期望当调用Navigator.of(context).pushNamed("XXX Page");时,首先对之前的页面发送PageExit,然后对当前页面发送PageView事件。当调用Navigator.of(context).pop();时则,首先发送当前页面的PageExit事件,再发送之前页面的PageView事件。

我们首先想到的是使用RouteObserver,但是PageViewPageExit发送的顺序相反。并且PopupRoute类型的路由会影响前一个页面的埋点事件发送,例如我们入栈的顺序是 A页面 -> A页面上的弹窗 -> B页面,但是在这个过程中A页面的PageExit事件没有发送。

所以我们必须自己管理路由栈,这样判断不同路由的类型,并控制事件的顺序。详细实现方案在后面展开。

2.2 TagView组件于PageView组件

这两个组件虽然与Flutter的路由无关,但是在产品经理眼中它们任属于页面。并且当Tab发生首次曝光和切换的时候我们都需要发送埋点事件。

例如当Tab页A首次曝光时,我们首先发送上一个页面的PageExit事件,然后发送TabA的PageView事件。当我们从TabA切换到TabB的时候,先发送TabA的PageExit事件,然后发送TabB的PageView事件。当我们push一个新的路由时,需要发送TabB的PageExit事件。

这套流程需要Tab页和普通页面之间通过事件机制来交互,如果直接把这套机制搬到业务代码中,那么业务代码中就会包含大量与业务无关并且重复的代码。详细的抽象方案见后文。

3. 解决这些问题 #

3.1 解决PageView,PageExit的顺序问题

RouteObserver给了我们一个不错的起点,我们重写其中的didPopdidPush方法就并调整事件发送的顺序就可以解决这个问题。详见TrackerStackObserver,在didpop方法中我们先触发上一个路由的PageExit事件,然后再触发当前路由的PageView事件。

3.2 避免弹窗的干扰(例如Dialog)

RouteObserver.didPop(Route中,我们可以通过previousRoute找到上一个路由,并更具它来发送上一个路由的PageView事件。但是如果上一个路由是Dialog,就会造成错误,因为我们实际想要的是包含这个Dialog的路由。

要解决这个问题我们必须自己维护一个路由栈,这样当didPop触发时我们就可以找到真正的上一个路由。请参考这一段代码,这里的routes是当前的路由栈。

3.3 如何上报TabView中的埋点事件,并和其它页面串联起来

这个问题可以分解为两个小问题:

    1. 如何把TabView页面和普通的路由进行串联?
    1. 当Tab发生切换时如何发送埋点事件?

为了解决这两个问题,我们需要一个容器来管理tab页面的状态并且承载事件转发的任务。详见下图: 管理TabView中的事件

其中TabsWrapper会监听来自Flutter的路由事件,并转发给当前曝光的Tab,这就可以解决了问题一。

同时TabsWrappe也会包含一个TabController和上一个被打开的Tab索引,TabsWrappe会监听来自TabController的onChange(index)事件,并把事件转发给对应的tab,这就解决了问题二。

6
likes
80
pub points
71%
popularity

flutter_page_tracker is a flutter plugin which help tracking route events of PageRoute/PopupRoute/PageView/TabView. Route events include PageView and PageExit.

Repository (GitHub)
View/report issues

Documentation

API reference

Uploader

billl89g@gmail.com

License

BSD (LICENSE)

Dependencies

flutter, fluttertoast

More

Packages that depend on flutter_page_tracker