Giter Site home page Giter Site logo

sbdavid / flutter_page_tracker Goto Github PK

View Code? Open in Web Editor NEW
111.0 5.0 21.0 14.76 MB

flutter埋点、弹窗埋点、页面埋点事件捕获框架,支持普通页面的页面曝光事件(PageView),页面离开事件(PageExit)。支持在TabView和PageView组件中发送页面曝光和页面离开

License: BSD 2-Clause "Simplified" License

Java 0.64% Objective-C 1.32% Dart 92.70% Shell 1.15% Ruby 4.18%
flutter tracker pageview tabview dialog

flutter_page_tracker's Introduction

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 [email protected]: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 route, Route previousRoute)中,我们可以通过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,这就解决了问题二。

flutter_page_tracker's People

Contributors

sbdavid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

flutter_page_tracker's Issues

升级为flutter2.2.3报如下错误

../../../flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_page_tracker-3.0.7/lib/src/page_view_listener_mixin.dart:106:20: Error: The method 'ancestorStateOfType' isn't defined for the class 'BuildContext'.

  • 'BuildContext' is from 'package:flutter/src/widgets/framework.dart' ('../../../flutter/packages/flutter/lib/src/widgets/framework.dart').
    Try correcting the name to the name of an existing method, or defining a method named 'ancestorStateOfType'.
    return context.ancestorStateOfType(TypeMatcher());
    ^^^^^^^^^^^^^^^^^^^
    ../../../flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_page_tracker-3.0.7/lib/src/page_load_provider.dart:20:23: Error: The method 'inheritFromWidgetOfExactType' isn't defined for the class 'BuildContext'.
  • 'BuildContext' is from 'package:flutter/src/widgets/framework.dart' ('../../../flutter/packages/flutter/lib/src/widgets/framework.dart').

Try correcting the name to the name of an existing method, or defining a method named 'inheritFromWidgetOfExactType'.
return (context.inheritFromWidgetOfExactType(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

使用PageViewListenerMixin 在切换时不会触发对应页面的didPageView方法

`点击tabbar` 切换到我的页面 didPageView 不调用 ,请问是什么原因导致的?

首页代码如下:
class TabNavigator extends StatefulWidget {
  final int index;
  TabNavigator({Key key, this.index = 0}) : super(key: key);

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

class _TabNavigatorState extends State<TabNavigator> {
  final _defaultColor = Color(0xff979797); // icon默认颜色
  TabsModel model = TabsModel();
  final List<String> _appBarTitles = ['首页', '商品分类', '常用清单', '购物车', '个人中心'];

  List<BottomNavigationBarItem> __buildDarkBottomNavigationBarItem() {
    const _tabsItem = [
      [
        'assets/images/tabs/home_icon.png',
        'assets/images/tabs/home_active_icon.png'
      ],
      [
        'assets/images/tabs/shopping_list_icon.png',
        'assets/images/tabs/shopping_list_active_icon.png'
      ],
      [
        'assets/images/tabs/common_list_icon.png',
        'assets/images/tabs/common_list_active_icon.png'
      ],
      [
        'assets/images/tabs/shop_card_icon.png',
        'assets/images/tabs/shop_card_active_icon.png'
      ],
      [
        'assets/images/tabs/mine_icon.png',
        'assets/images/tabs/mine_active_icon.png'
      ]
    ];
    return List.generate(
        _tabsItem.length,
        (index) => BottomNavigationBarItem(
            icon: BadgeItem(
                index: index,
                child: Image.asset(
                  _tabsItem[index][0],
                  width: 24.0.w,
                  height: 24.0.h,
                  color: _defaultColor,
                )),
            activeIcon: BadgeItem(
                index: index,
                child: Image.asset(_tabsItem[index][1],
                    width: 24.0.w, height: 24.0.h)),
            title: Padding(
              padding: EdgeInsets.only(top: 4.0),
              child: Text(_appBarTitles[index]),
            )));
  }


  @override
  void initState() {
    controller = PageController(initialPage: widget.index);
    model.value = widget.index;
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
    controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print(context);
    // 设计基于iPhone6(iPhone6 750*1334)的大小
    ScreenUtil.init(context, width: 375, height: 667, allowFontScaling: true);

    return DoubleTapBackExitApp(
      child: Scaffold(
          body: PageViewWrapper(
              changeDelegate: PageViewChangeDelegate(controller),
              pageAmount: 4,
              initialPage: controller.initialPage,
              child: PageView(
                controller: controller,
                physics: NeverScrollableScrollPhysics(),
                onPageChanged: (index) => model.value = index,
                children: <Widget>[
                  HomePage(index: 0),
                  GoodsListPage(index: 1),
                  CommonMainPage(index: 2),
                  ShoppingCartPage(index: 3),
                  MinePage(index: 4)
                ],
              )),
          bottomNavigationBar: ProviderWidget<TabsModel>(
            model: model,
            builder: (_, model, __) {
              return BottomNavigationBar(
                  currentIndex: model.value,
                  selectedFontSize: 10.0,
                  unselectedFontSize: 10.0,
                  unselectedItemColor: _defaultColor,
                  selectedItemColor: HmColors.colorMain,
                  onTap: (index) {
                    // 埋点
                    _eventTracking(index);
                    PageViewToPage.jumpToPage(index, context);
                  },
                  type: BottomNavigationBarType.fixed,
                  items: __buildDarkBottomNavigationBarItem());
            },
          )),
    );
  }
}

我的页面如下:

class MinePage extends StatefulWidget {
  final int index;
  MinePage({Key key, this.index}) : super(key: key);
  @override
  _MinePageState createState() => _MinePageState();
}

class _MinePageState extends State<MinePage>
    with
        AutomaticKeepAliveClientMixin<MinePage>,
        PageTrackerAware,
        PageViewListenerMixin {
  int get pageViewIndex => widget.index;

  MineModel model = MineModel();
  EasyRefreshController refreshController;

  @override
  void didPageView() {
    super.didPageView();
    PageViewEventTracking.pageEnter();
    model.load();
  }

  @override
  void didPageExit() {
    super.didPageExit();
    PageViewEventTracking.pageLeave(widget.index);
  }

  @override
  void initState() {
    super.initState();
    refreshController = EasyRefreshController();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    Global.appPrecacheImage.precacheMineImages(context);
  }

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    double _mediaQueryTop = MediaQuery.of(context).padding.top; // padding高度
    return AnnotatedRegion(
      value: SystemUiOverlayStyle.dark,
      child: Scaffold(
          body: EasyRefresh(
        enableControlFinishRefresh: true,
        controller: refreshController,
        header: HmCustomHeader(
            bgColor: Colors.transparent,
            textColor: Colors.black87,
            extent: _mediaQueryTop + 60,
            triggerDistance: _mediaQueryTop + 70,
            showInfo: false),
        onRefresh: () async {
          await model.load();
          refreshController.finishRefresh();
        },
        child: Container(
          child: Column(
            children: <Widget>[
              ProviderWidget<MineModel>(
                model: model,
                onModelReady: (MineModel model) => model.load(),
                type: BaseType.NORMAL,
                child: Column(
                  children: <Widget>[
                    MineHeaderView(model: model),
                    MineOrderView(model: model),
                    CommonFunctionView(model: model),
                  ],
                ),
              ),
           
            ],
          ),
        ),
      )),
    );
  }
}

flutter sdk 2.0.3 接口变动报错

  • 'BuildContext' is from 'package:flutter/src/widgets/framework.dart' ('../../../../../flutter/packages/flutter/lib/src/widgets/framework.dart').
    Try correcting the name to the name of an existing method, or defining a method named 'ancestorStateOfType'.
    .ancestorStateOfType(
    ^^^^^^^^^^^^^^^^^^^
    ../../../../../flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_page_tracker-3.0.7/lib/src/page_view_listener_mixin.dart:106:20: Error: The method 'ancestorStateOfType' isn't defined for the class 'BuildContext'.
    • 'BuildContext' is from 'package:flutter/src/widgets/framework.dart' ('../../../../../flutter/packages/flutter/lib/src/widgets/framework.dart').
      Try correcting the name to the name of an existing method, or defining a method named 'ancestorStateOfType'.
      return context.ancestorStateOfType(TypeMatcher

请问有多个navigatorObservers,会报错。

应该怎么解决?

Offstage(
          offstage: _tabIndex != 0,
          child: TickerMode(
              enabled: _tabIndex == 0,
              child: MaterialApp(
                theme: ThemeData(fontFamily: "Meiryo"),
                home: HomeView(),
                navigatorObservers: [TrackerRouteObserverProvider.of(context)],
              )),
        ),
        Offstage(
          offstage: _tabIndex != 1,
          child: TickerMode(
            enabled: _tabIndex == 1,
            child: MaterialApp(
              theme: ThemeData(fontFamily: "Meiryo"),
              home: LifelogView(),
              navigatorObservers: [TrackerRouteObserverProvider.of(context)],
            ),
          ),
        ),

和flutter_bloc报provider版本冲突

Because no versions of flutter_bloc match >5.0.1 <6.0.0 and flutter_bloc 5.0.1 depends on provider ^4.0.5, flutter_bloc ^5.0.1 requires provider ^4.0.5.
And because flutter_page_tracker ^1.1.1 depends on provider ^3.2.0, flutter_bloc ^5.0.1 is incompatible with flutter_page_tracker ^1.1.1.
So, because flutter_module depends on both flutter_bloc ^5.0.1 and flutter_page_tracker ^1.2.2, version solving failed.

作者可以建一个 Flutter 埋点群吗

RT,最近公司也在做 Flutter 无痕埋点,希望作者能够建一个群,让各位有志之士能够在一起研究,共同进步,最终克服 Flutter 全埋点方面所有问题,真正做到开源,同时建立这方面的领导地位和影响力。

如何获取页面信息

比如通过路由跳转到了 B 页面,那么如何在didpush里面获取 B 页面的信息呢,例如B页面的类名等

PopUtil的问题

A页面push到B页面,B页面push到C页面,然后C页面直接pop到A页面,按照目前您这个框架,B页面的didPageView()和didPageExit()都会调用,实际上按照产品的定义,从C页面直接pop到A页面,B页面的两个的didPageView()和didPageExit()是不需要埋点的,怎么解决这个问题?

C页面pop到A页面的方法如下:

              Navigator.of(context).popUntil((Route route){
                  if (route.settings.name == "/"){
                    return true;
                  }

                  return false;
              });

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.