Flutter頁面曝光事件埋點框架

SBDavid發表於2020-02-01

flutter_page_tracker

簡介

FlutterPageTracker是一個用於監聽頁面露出離開的plugin。它具有以下特性:

  • 1.監聽普通頁面的露出離開事件(PageRoute),
    • 當前頁面入棧會觸發當前頁面的曝光事件和前一個頁面的離開事件
    • 當前頁面出棧會觸發當前頁面的離開事件和前一個頁面的曝光事件
    • Flutter頁面曝光事件埋點框架
  • 2.監聽對話方塊的露出離開(PopupRoute),
    • 它和PageRoute的區別是,當前對話方塊的露出和關閉不會觸發前一個頁面的露出離開事件
    • Flutter頁面曝光事件埋點框架
  • 3.監聽PageView、TabView元件的切換事件
    • 當一個PageView或者TabView入棧時,前一個頁面會觸發頁面離開事件
    • 當一個PageView或者TabView出棧時,前一個頁面會觸發頁面曝光事件
    • 當焦點頁面發生變化時,舊的頁面觸發頁面露出,新的頁面觸發PageView
    • PageView元件
      • Flutter頁面曝光事件埋點框架
    • 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 在元件中傳送埋點事件

必須使用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.3 Dialog的埋點

class PopupPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return SimpleDialog(
      children: <Widget>[
        TrackerDialogWrapper(
        
          didPageView: () {
            // 傳送頁面曝光事件
          },
          
          didPageExit: () {
            // 傳送頁面離開事件
          },
          child: Container(),
        ),
      ],
    );
  }
}
複製程式碼

3.3 TabView傳送埋點事件(PageView參考example)

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.4 TabView中巢狀PageView(PageView也可以巢狀TabView,TabView也可以巢狀TabView)

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,這就解決了問題二。

相關文章