Flutter 优雅封装公共页面(CommPage、BasePage)

由于Flutter没有页面的概念,而平时开发很多地方需要有页面维度,如以下场景:

  • 页面埋点
  • 感知页面生命周期,回到页面、页面退出前台、退出等
  • 页面通用方法等

直接上逻辑,三句话搞定~ ~嘿嘿,

1.借用Flutter自带的widget push 层级的感知监听 NavigatorObserver.为了方便自定义我们自己实现NavigatorObserver

///  app_route_observer.dart
///
///  Created by iotjin on 2020/11/07.
///  description: 通过路由监听页面出现或消失

import 'package:flutter/material.dart';

class AppRouteObserver {
  /// 创建路由监听(这是实际上的路由监听器)
  static final CustomRouteObserver<ModalRoute<void>> _routeObserver = CustomRouteObserver<ModalRoute<void>>();

  /// 这是个单例
  static final AppRouteObserver _appRouteObserver = AppRouteObserver._internal();

  AppRouteObserver._internal();

  /// 通过单例的get方法获取路由监听器
  CustomRouteObserver<ModalRoute<void>> get routeObserver {
    return _routeObserver;
  }

  factory AppRouteObserver() {
    return _appRouteObserver;
  }
}



class CustomRouteObserver<R extends Route<dynamic>> extends  NavigatorObserver {
  final Map<R, Set<CustomRouteAware>> _listeners = <R, Set<CustomRouteAware>>{};

  @visibleForTesting
  bool debugObservingRoute(R route) {
    late bool contained;
    assert(() {
      contained = _listeners.containsKey(route);
      return true;
    }());
    return contained;
  }

  void subscribe(CustomRouteAware routeAware, R route) {
    assert(routeAware != null);
    assert(route != null);
    final Set<CustomRouteAware> subscribers = _listeners.putIfAbsent(route, () => <CustomRouteAware>{});
    if (subscribers.add(routeAware)) {
      routeAware.didPush();
    }
  }

  void unsubscribe(CustomRouteAware routeAware) {
    assert(routeAware != null);
    final List<R> routes = _listeners.keys.toList();
    for (final R route in routes) {
      final Set<CustomRouteAware>? subscribers = _listeners[route];
      if (subscribers != null) {
        subscribers.remove(routeAware);
        if (subscribers.isEmpty) {
          _listeners.remove(route);
        }
      }
    }
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route is R && previousRoute is R) {
      final List<CustomRouteAware>? previousSubscribers = _listeners[previousRoute]?.toList();

      if (previousSubscribers != null) {
        for (final CustomRouteAware routeAware in previousSubscribers) {
          routeAware.didPopPrevious(route,previousRoute);
        }
      }

      final List<CustomRouteAware>? subscribers = _listeners[route]?.toList();

      if (subscribers != null) {
        for (final CustomRouteAware routeAware in subscribers) {
          routeAware.didPop(route,previousRoute);
        }
      }
    }
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route is R && previousRoute is R) {
      final Set<CustomRouteAware>? previousSubscribers = _listeners[previousRoute];

      if (previousSubscribers != null) {
        for (final CustomRouteAware routeAware in previousSubscribers) {
          routeAware.didPushNext(route,previousRoute);
        }
      }
    }
  }
}
//自定义生命周期方法
abstract class CustomRouteAware {

  void didPopPrevious(Route<dynamic> route, Route<dynamic>? previousRoute) { }

  void didPush() { }

  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { }

  void didPushNext(Route<dynamic> route, Route<dynamic>? previousRoute) { }
}


  1. 添加到 MaterialApp 中
MaterialApp(
         ...
          navigatorObservers: [
            AppRouteObserver().routeObserver
          ], 
        );
  1. 然后编辑CommPage,实现CustomRouteObserver
import 'dart:async';
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../support/yu_library.dart';
import '../../utils/app_route_observer.dart';
import '../../utils/storageHelper.dart';
abstract class CommonPage extends StatefulWidget {

  static BuildContext? commTopContext;

  const CommonPage({super.key});

  @override
  CommonPageState createState() => getState();

  ///子类实现
  CommonPageState getState();
}
abstract class CommonPageState<T extends CommonPage> extends State<T>  with CustomRouteAware {

   BuildContext? commContext; //当前页面上下文 为空时表明页面已经销毁
   late String commPageName; //页面路径
   dynamic commPageParams; //页面参数
   Map? commPageSettings; // 页面配置信息
   List<StreamSubscription> commEventSubscriptions = []; //页面事件订阅监听,销毁页面时自动取消订阅
   bool commPageClosed = false; // 页面是否已关闭

   bool commNeedSetState = false; // 返回页面时是否需要刷新,一般用于刷新时被mounted阻塞

  //页面打开
  void onStart(){}
  //从上一个页面退回
  void onPopPrevious(Route<dynamic> route, Route<dynamic>? currentRoute){}
  // 打开一个新页面
  void onPushNext(Route<dynamic> route, Route<dynamic>? currentRoute){}
  //页面关闭
  void onClose(Route<dynamic> currentRoute, Route<dynamic>? previousRoute){}
  //页面销毁
  void onDispose(){}

  //页面初始化
  @override
  void initState() {
    super.initState();
    commContext=context;
    commPageClosed=false;
    commNeedSetState = false;
    CommonPage.commTopContext=commContext;
  }

  //页面显示
  @override
  void didPush() {
    commPageName =  ModalRoute.of(commContext!)?.settings.name ??'';
    commPageParams = ModalRoute.of(commContext!)?.settings.arguments ?? {};
    commPageSettings = AppSettingHelper.getSettingsByPageRoute(commPageName);
    CommonPage.commTopContext=commContext;

    onStart();
    if (kDebugMode) print('=======**=======页面打开-onStart  commPageName:$commPageName  commPageClosed:$commPageClosed  commNeedSetState:$commNeedSetState  commEventSubscriptions:${commEventSubscriptions.length}   $commPageParams $commPageSettings   =======**=======');
  }

  //页面依赖关系变化
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    /// 路由订阅
    AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(commContext!)!);
  }

  //返回当前页面
  @override
  void didPopPrevious(Route<dynamic> route, Route<dynamic>? previousRoute) {
    //目标widget不是页面不做处理
    if(route.settings.name?.isNotEmpty == true){
      CommonPage.commTopContext=commContext;
      onPopPrevious(route,previousRoute);
      print('=======**=======返回当前页面-onPopNext  commNeedSetState:$commNeedSetState  commPageName:$commPageName    currentPage:${previousRoute?.settings.name}    ${route.settings.name}   =======**=======');
      if(commNeedSetState){
        commNeedSetState=false;
        commSetState(() { });
      }
    }
  }

 //打开其他页面
  @override
  void didPushNext(Route<dynamic> route, Route<dynamic>? previousRoute) {
    //目标widget不是页面不做处理
    if(route.settings.name?.isNotEmpty == true) {
      onPushNext(route,previousRoute);
      print('=======**=======打开其他页面-onPushNext  commPageName:$commPageName currentPage:${previousRoute?.settings.name}    ${route.settings.name}   =======**=======');
    }
  }

  //页面退出
  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    //目标widget不是页面不做处理
    if(route.settings.name?.isNotEmpty == true) {
      commPageClosed = true;
      onClose(route,previousRoute);
      print('=======**=======页面退出-onDetach commPageName:$commPageName currentPage: ${route.settings.name}    ${previousRoute?.settings.name}  =======**=======');
    }
  }

  //页面销毁
  @override
  void dispose() {
    /// 取消路由订阅
    for (var element in commEventSubscriptions) {
      element.cancel();
    }
    commContext=null;
    super.dispose();
    onDispose();
   print('=======**=======页面销毁-onDispose  commPageName:$commPageName  =======**=======');
    AppRouteObserver().routeObserver.unsubscribe(this);
  }

  //当前页面是否位于前台
  bool commCurrentIsTop(){
   return   CommonPage.commTopContext == commContext;
  }

//page 上所有的setState可以走这里,防止异步调用报错
  void commSetState(VoidCallback fn){
   print("commSetState mounted:$mounted  commPageClosed:$commPageClosed  ${DateTime.now().microsecondsSinceEpoch}");
   if(mounted) {
     setState(fn);
   }else{
     commNeedSetState=true;
   }
  }
}


可以看出我们区分页面的判断 route.settings.name ,所以我们页面push的时候务必要设置这个值,可以封装一个通用的打开页面的方法,例如

  //打开新页面
  static Future<T?> push<T>(BuildContext context, String routeName, {Map<dynamic, dynamic>? arguments, bool isReplaced = false, bool pushAndRemoveUntil = false}) async {

    final Function? buildPage = routes[routeName];

    LogHelper.d("push: $routeName $arguments ");

    if (buildPage == null) {
      return await Navigator.push(context, CupertinoPageRoute(builder: (_) => const NotFoundPage()));
    }

    late Widget page ;
    
    try{
      page = (arguments == null)? buildPage(context) : buildPage(context, arguments: arguments) ;
    } catch(e){
      page= buildPage(context);
    }

    if (page is! CommonPage) {
      LogHelper.e("!!!!! $routeName页面需继承CommPage,请及时处理 !!!!");
      if(AppConfig.isSitEnv) ToastHelper.showToast("$routeName 没有继承CommPage,请及时处理!");
    }
    if (isReplaced) {
      //替换页面
      return await Navigator.pushReplacement(
          context,
          CupertinoPageRoute(
              builder: (context) => page,
              settings: RouteSettings(name: routeName,arguments:arguments)));
    } else if (pushAndRemoveUntil) {
      //跳转并关闭所有页面
      return await Navigator.pushAndRemoveUntil(
          context,
          CupertinoPageRoute(
              builder: (context) => page,
              settings: RouteSettings(name: routeName,arguments:arguments)),
          (route) => false);
    } else {
      return await Navigator.push(
          context,
          CupertinoPageRoute(
              builder: (context) => page,
              settings: RouteSettings(name: routeName,arguments:arguments)));
    }
  }

到这里大功告成,只需要在页面的widget种继承我们的Commpage即可。

class LoginPage extends CommonPage {
  const LoginPage({super.key});

  @override
  CommonPageState<CommonPage> getState() => _LoginPageState();
}

class _LoginPageState extends CommonPageState<LoginPage> {

  @override
  void onPopPrevious(Route route, Route? currentRoute) {
    // TODO: implement onPopPrevious
    super.onPopPrevious(route, currentRoute);
  }

  @override
  void onPushNext(Route route, Route? currentRoute) {
    // TODO: implement onPushNext
    super.onPushNext(route, currentRoute);
  }

}
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容