likes
comments
collection
share

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介

作者站长头像
站长
· 阅读数 71

前言

之前的一篇【Flutter】抄一个路由轮子到Flutter上能有多难? 中,我抱着三分记录思考,七分吐槽的目的写下了那篇文章;结果意外发现点赞还是有一些的……

那么吐槽归吐槽,既然有人关注,那么接下来该做的事就很明确了,在实现功能的基础上完成并开源这个路由框架;

现在计划将路由框架分两部分完成,一部分是自定义的路由跳转框架;另一部分就是这篇文章要介绍的核心基础——路由表部分,以证明我真的在写了;

另外提醒一下,很多功能乍看挺高大上的,其实原理特别low,实属武大郎放风筝——起手一点都不高,以至于现在回顾一下,好像路由这东西没啥好说的……

目前设计的路由表主要有这么三大部分功能:

  1. 页面跳转能力(FRouterWareHouse
  2. 跨模块依赖注入能力(FRouterProvider)
  3. 全局任务和消息中心(FRouterFlowTaskCenter)

介绍

1.1 三大能力:

FRouterWareHouse

  • 使用类似 URI Scheme 作为路由Path格式,对Web、deepLink等天然友好;
  • 类似于 Spring MVC(后端一看就笑了,注解参数名都一模一样) 的传参方式和解析模式,允许任意object的传递,在允许参数别名的基础上,不需要任何序列化等操作即可保证对象类型,(谁说路由传参只能用基本数据格式)
  • 支持 json格式 导出路由表Bundle;
  • 支持路由动态化,比如说可以依靠 远端 下发 动态json路由表Bundle,妈妈再也不用担心上线出重大bug不能降级为H5,不能换成别的页面临时过渡的问题了;
  • 支持页面跳转拦截处理,也支持模块级别的拦截器;
  • 路由表和路由模块隔离解耦,也就是说,可以本质上降为仅仅提供Widget的路由仓库,在这种情况下,没啥太大意外的话,兼容任意一种路由跳转框架;
  • 尽可能的渐进式接入;

FRouterProvider:

  • 支持跨模块依赖注入;

/// 感觉就写一条,很low啊

FRouterFlowTask:

  • 支持单模块独立初始化
  • 基于有向无环图自动维护依赖的加载次序;2023年了还在手写依赖的加载时机和顺序?
  • 动态化任务

1.2 路由方案:

2. 如何接入

下面我就从零开始,一步步演示如何接入上frouter;

首先建立一个基础的多模块演示工程:

  • 演示项目结构概览

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介演示项目结构概览,仅供参考

2.1 使用FRouter的路由表功能

2.1.1 项目导入

http依赖地址暂无,因为还没发布😛,Get路由的搬运抄袭缝合已经在计划中了;

在这里以library的引用方式演示(其实都差不多)

首先是在yaml中声明上frouter的包,这里就不再赘述,毕竟现在连发布地址都没有;

2.1.2 声明路由项

在需要路由跳转的页面上注解@RouterPath

在这里,我们给 module_b 的随便一个Widget此注解,比如说module_b 中的 PostInfoPage

参数释义

  • pathUri :路由path,必填项

    pathUri的格式跟ARouter大差不差,同样是需要至少二级;只是格式上遵循URI的格式,说白了就是URI能解析即可;例如:'user/user_info'

  • action :自定义事件,搭配全局任务用的;

  • description :描述,除了可以当注释外,也用在导出的路由表文件中当注释

示例图:

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介module_b:PostInfoPage

2.1.3 执行build_runner

如果不出意外的话,此时在声明注解的模块lib目录下,应该有一个模块名_router_export.dart的引用文件;里面的内容就是声明了注解的文件,比如这样:

library module_b_route_export;

export 'page/post_info_page.dart';

这个文件的作用就是单纯的提供跨模块的引用文件,没啥好说的;

在主工程中,也会出现一个主工程_router.dart的路由表文件;还是以上面的演示工程为例,其内容是这样的:


// Generated by lwlizhe frouter plugin, do not edit manually.
// run pub run build_runner build --delete-conflicting-outputs
// ignore_for_file: directives_ordering
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:frouter/bin/entity/frouter_router_map.dart' as _i1;
import 'package:module_a/module_a_route.dart' as _i2;
import 'package:frouter/bin/builder/frouter_widget_builder.dart' as _i3;
import 'package:module_b/module_b_route_export.dart' as _i4;
import 'package:frouter/bin/helper/safety_parameter_transform_utils.dart'
    as _i5;
import 'package:flutter/material.dart'as _i6;

class FRouterMap extends _i1.FRouterRouterMap {
  @override
  String get hostRouterGroup {
    return 'example';
  }

  @override
  String get currentRouterGroup {
    return 'example';
  }

  @override
  List<_i1.FRouterRouterMap> get subModule {
    return [
      _i2.FModuleRouterMap(),
    ];
  }

  @override
  Map<String, _i3.FRouterWidgetBuilder> get routerMap {
    return <String, _i3.FRouterWidgetBuilder>{
      'package:module_b/page/post_info_page.dart:PostInfoPage':
          (Map<String, String>? parameters) {
        return _i4.PostInfoPage(
          _i5.transform<List<String>>(parameters?['postTitleList'])
              as List<String>,
          key: _i5.transform<_i6.Key?>(parameters?['key']),
        );
      },
    };
  }

  @override
  Map<String, String> get routerMapBundle {
    return <String, String>{
      'post/post_info':
          'package:module_b/page/post_info_page.dart:PostInfoPage',
    };
  }

  @override
  Map<String, _i3.FRouterProviderBuilder> get providerMap {
    return <String, _i3.FRouterProviderBuilder>{};
  }

  @override
  Map<String, String> get providerBundle {
    return <String, String>{};
  }
}

这个就是最终生成的路由表部分了,其实也不复杂;

参数说明

hostRouterGroup:主工程名称

currentRouterGroup:当前模块名称

subModule:当前模块的子模块是哪些(本来是打算基于消息的模式,搞个渐进式路由框架;这里就是路由表下沉到子模块的引用部分)

routerMap 就是核心路由表了;(在这里也担任本地路由缓存的作用,至于为什么需要一个本地路由缓存,后面会有说)

routerMapBundle:路由Bundle(关于这里的设计,也是后面动态路由部分细说)

providerMap: 跨组件通讯服务的缓存表

providerBundle :跨组件通讯服务缓存表Bundle

其中包含的 routerMapBundle 就是路由 bundle,也是可以用于动态替换,输出json的部分;举个例子,如果PostInfoPage出现 bug,可以通过替换 routerMapBundle 中post/post_info 对应值, 将其指向其他page或者web页面;

其中的 routerMap 是路由本地缓存映射,用于通过 bundle 去寻找到真正对应的 Page

2.1.4 初始化路由表及基础路由跳转

  • 接入到现有路由框架中

    由于现在还没开发完路由部分;这里暂时由别的路由框架来实现(这就是上面吹的,完美兼容各种路由框架的部分……)

    • 首先初始化路由表

      在main.dart 的 build 方法中加载一下路由表:

      FRouter().init(FRouterMap());
      
    • 接入路由

      接入路由这块按照路由框架的要求接入即可,毕竟本质上只是个Map;

      这里就以getX、go_router、navigaotr 1.0 为例;

      class MyApp extends StatelessWidget {
        const MyApp({Key? key}) : super(key: key);
      
        // This widget is the root of your application.
        @override
        Widget build(BuildContext context) {
          final wareHouse = FRouter().init(FRouterMap());
      
          /// getX 的路由转换处理
          final List<GetPage> getRouter = [
            ...原来接入的GetPage路由部分,
            ...wareHouse.routerMapBundle.keys.map((e) => GetPage(
                name: '/$e',
                page: () {
                  String uri = (Uri.parse(e).replace(queryParameters: {
                    ...Get.parameters,
                    ...Get.arguments ?? {}
                  })).toString();
                  return FRouter().build(uri).navigation() as Widget;
                }))
          ];
      
          /// go_router 的路由转换处理
          /// 如果想支持go_router的多层级路由,可以用wareHouse.router这个Map;
          /// 其KEY值是路由路径的一级标签,可以做多层级路由的处理;
          /// 再多层级的话,建议直接根据路由路径的层级来处理;
          final GoRouter goRouter = GoRouter(
            routes: <RouteBase>[
              ...原来接入的GoRouter路由部分,
              ...wareHouse.routerMapBundle.keys
                  .map((e) => GoRoute(
                      path: e,
                      builder: (BuildContext context, GoRouterState state) {
                        String uri = (Uri.parse(e).replace(queryParameters: {
                          ...Get.parameters,
                          ...Get.arguments ?? {}
                        })).toString();
                        return FRouter().build(uri).navigation() as Widget;
                      }))
                  .toList(),
            ],
          );
      
          /// 使用get
          return GetMaterialApp(
            title: 'Flutter Demo',
            home: const HomePage(),
            getPages: getRouter,
          );
      
          /// 使用go_router
          return MaterialApp.router(
            routerConfig: goRouter,
          );
      
          /// 使用 Navigator 1.0
          return MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData(
              // This is the theme of your application.
              //
              // Try running your application with "flutter run". You'll see the
              // application has a blue toolbar. Then, without quitting the app, try
              // changing the primarySwatch below to Colors.green and then invoke
              // "hot reload" (press "r" in the console where you ran "flutter run",
              // or simply save your changes to "hot reload" in a Flutter IDE).
              // Notice that the counter didn't reset back to zero; the application
              // is not restarted.
              primarySwatch: Colors.blue,
            ),
            home: const MyHomePage(title: 'Flutter Demo Home Page'),
            onGenerateRoute: (RouteSettings settings) {
              return MaterialPageRoute<dynamic>(
                settings: settings,
                builder: (BuildContext _) {
                  return FRouter().build(settings.name ?? '').navigation() as Widget;
                },
              );
            },
          );
        }
      }
      
  • 启动导航

    对于路由表来说,其实没啥启动路由的功能~~所以我感觉这里应该属于路由部分的内容,因此这里直接用路由框架的路由导航之类的就行;

    比如说Get,直接调用Get.toNamed(xx路径,arguments:argument参数,parameters:parameters参数),对于路由表来说,关注的部分其实应该是路由传参和其解析部分;

    frouter的参数格式跟http的带参格式一样,细心的同学应该能在上面的接入方式中发现,传给frouter的的路径都是这种经过URI解析并增加parameters的:

    String fullPath = (Uri.parse(e).replace(queryParameters: {
                          ...Get.parameters,
                          ...Get.arguments ?? {}
                        })).toString();
    

    所以调用启动路由的时候,声明式路由就要自己处理一下入参转URI;如果以非声明式路由的方式的话,那就需要在整体路径上,用标准的URI格式增加上调用参数;

    /// todo: 感觉这部分应该整合到路由功能那里?毕竟路由表不用关心路由调用

    现在来看看效果:

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介基础路由功能

2.1.5 动态路由使用方式

动态路由别看名字很高上的样子,其原理其实非常简单;开个异步任务更新一下当前保存的路由表Bundle就完事了;

但是需要注意的是,我这里替换的是Bundle而非整个路由表;

至于原因也很简单,flutter不支持反射,因此无法直接获取到类;因此这里参考了Fair动态化自定义Widget的做法;先保存下来缓存Widget,后续通过更新Bundle的方式来实现动态化;因此没有原先Widget的缓存,是无法实现动态化的;

因此这里和原生的动态化功能上差距还是不小的,感觉基本只是用来做各种远端降级之类的处理?

说白了,这块的动态化只能用来在已有功能的基础上做兜底拦截等处理,不能新增功能;

说了这堆可能还不如一段demo,在这里演示一下如何将一个页面改为html页面:

1、首先加一个 CommonWebPage 来担任基础功能,在这里就以 app/webView 为例:

@RouterPath(pathUri: 'app/webView')
class CommonWebPage extends StatefulWidget {
  final String tag;

  const CommonWebPage(this.tag,{super.key});

  @override
  State<CommonWebPage> createState() => _CommonWebPageState();
}

。。。。

为了方便CommonWebPage获取参数,我在路由表接入部分,将uri路径放了进去:以上面的GETX接入方式为例,应该是这样的:

final List<GetPage> getRouter = [
  ...wareHouse.routerMapBundle.keys.map((e) => GetPage(
      name: '/$e',
      page: () {
        String uri = (Uri.parse(e).replace(queryParameters: {
          ...Get.parameters,
          ...Get.arguments ?? {},
          ...{'tag': e.toString()} // 增加的部分
        })).toString();
        return FRouter().build(uri).navigation() as Widget;
      }))
];

之后运行build_runner生成CommonWebPage的路由注解;

2、在首页增加一个按钮,来模拟从网络异步加载路由表更新的功能:

这里临时写个单条json,正常情况下应该是路由表导出的完整json;目前bundle仅仅是替换操作;

GestureDetector(
  onTap: () {
    FRouter().updateBundle('{"post/post_info":"package:base/common/common_web_page.dart:CommonWebPage"}');
  },
  child: Text('替换更新路由表'),
),

经过这步,可以看到这时候再打开PostInfoPage页面,可以发现跳转的页面是WebPage;

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介动态替换路由

2.1.6 自定义参数的注解使用方式

这里就要提到frouter的一个小优势了;除了基础类型之外,frouter还支持自定义数据格式;

这里参考了后端的SpringMVC的设计;

同样还是以一个例子演示一下:

这里在moduleA中新建一个UserInfoPage,跟基础路由一样,需要先声明上路由path;

但是需要的参数,可以直接写在构造器中,比如说这样:

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介

除了基本类型参数的入参之外,可以看到还有像UserInfo这种自定义实体类;

参数说明:

  1. @requestBody @requestBody 这个注解用在需要自动序列化一个实体类的情况下,其实现方式也很见简单,就是将实体类构造器所需的参数,从传入的参数中挑选出来并放进去;

  2. @RequestParam 这个注解用来处理诸如别名之类的,像图中那样,通过此注解,将userToken别名修改为userTokenA;

这里就是新生成的路由表,可以看到除了基础数据类型,自定义格式的数据也生成了出来;别名也覆盖掉了:

现在还是老样子,跑一遍build_runner看下路由表中具体生成的内容:

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介

运行效果如下:

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介带参路由

2.1.7 多模块的单元测试搭配部分

看到这里,可能有人就要问到一个问题,如果我模块单独打包甚至做成依赖项,如何在单独的子模块上运行呢?

关于这点,frouter支持通过@RouterRegister 注解来声明生成路由文件的位置;在子模块中生成路由文件后,主模块中会注册上子模块的文件路径;子模块的路由这样既实现了自身路由跟主模块的路由解耦,也能将自身注入到主模块中;

按照惯例,上例子:

在moduleA中新建一个main.dart,用来当作单独运行测试项目的入口,给其中加上RouterRegister注解

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介

之后运行build_runner可以看到子项目生成了路由表文件,主项目的路由表文件也注册了新的子路由表;

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介module_a中生成的路由表

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介example这个主模块中的路由表注册了子模块

那么这个module_a就可以自己单独启动,提交测试了,毕竟路由表下沉到自己模块中,不再依靠主模块;

现在,moduleA模块就跟主模块彻底断开了依赖,可以自己单独运行了;

2.2 使用FRouter的跨模块通讯

跨模块通讯能力可以说是我写这个路由表的最主要的目的;而这块的设计模式,无论是TheRouter还是ARouter,都用的是同样的方式;在这里,我也没做啥创新,基本都是大同小异的东西;

不过简单介绍还是要介绍一下滴,这里就直接把TheRouter的相关解释贴一下:

在这里,由 FRouterProvider 负责服务的提供;实现方式还是参考自ARouter的部分;

2.2.1 使用方式

对于跨模块间的通讯,自然分为两个部分,服务使用方和服务提供方,这里就分别以这两个身份分别概述:

首先假想一个场景:

现在你在做一个电商APP,你将购物车相关的部分放到一个单独的模块中,此模块我们命名为商品模块;同时还存在一个直播模块;直播模块允许直接下单主播推荐的商品添加到购物车中;

服务使用方:直播模块

对于服务使用方来说,不知道也不需要知道谁来提供一个购物车;只需要能够获取到购物车并往里面加商品即可,因此首先需要提供一个沟通桥梁,或者说,接口:

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介比如说在公共模块中加入一个base_cart_provider接口

然后将这个接口放到公共模块中,让其他依赖此公共模块的模块能获取到这个协议桥梁,这就是上面提到的接口下沉:

最后使用的时候,调用此接口即可,具体是谁来提供服务,则看是谁接入了此服务,并符合规定路径协议;

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介通过FRouter.navigation方法,用路由的方式寻找一个Provider

服务提供方:商品模块

注意看,这个男人叫小白,它现在正面对着来自某个未知甲方的接口协议;虽然这份协议看上去非常可疑,但是作为一个月薪300的打工仔,协议是否合规,并不在它的职责范围内;

于是乎,作为流水线的一环,小白顺理成章的开始了接口的实现接入,殊不知它的一切行动均在佛波勒的监控之下:

就在小白完成了接口接入的同时,佛波勒突然出现,并将通过注解接收了实现好接口的服务:

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介要想提供一个购物车服务,先继承base中的BaseCartProvider并实现具体接口,最后通过RouterPath注解声明路径,跟路由没啥区别

惊诧于丧彪突然出现的小白,这才注意到,原来自己所实现的服务,是将我们的女主角:名为小美的商品controller提供了出去,不难想象,小美接下来会面临什么结局,当然我知道各位兄弟们不爱看后面这段,这里就不赘述了…………

然鹅小白只是个月薪300的打工仔,这些事又与他何干呢,按照剧本,接下来应该是主人公小帅英雄救美闪亮登场了;

想到这里,小白开启了下一份工作;

番外篇:拦截器小帅的英雄救美

按照剧本,这时候应该轮到一炮子能掀翻两个卡拉米的主人公小帅登场了;

但是导演说拦截器理论上应该属于路由部分,再加上资金有限;

所以导演最终决定,在这里埋个彩蛋,做好出路由篇续集的准备;

                                              ———— 某位怨气颇深不愿透露姓名的主人公小帅这么说到

2.3 全局任务FRouterFlowTask

这个全局任务其实也没啥难点;能搞出路由表来,其实剩下的东西都不算啥;基础部分都是一个原理;

不过按照TheRouter中的设计,还是可以加一点花活的:

2.3.1 模块初始化任务

因为作为跨模块的项目,虽然主模块连接了各个子模块,可以获取各自的初始化任务;

但是正像TheRouter文中说的那样,如果都放到主模块中,那么任务如果有修改,那么就需要去修改主模块的初始化方法;

麻烦或者导致git冲突之类的还是小事;假如动了别人代码导致了bug,说不定就被毕业充当输出的优质人才了;

当然,作为初始化任务管理,基于有向无环图的依赖管理,应该成标配了;

使用方法

  1. 在提供初始化方法的类中加上@FlowTask注解
  2. 在初始化方法加上@FlowTaskInject
  3. 最后在需要启动初始化的地方(比如说闪屏页),调用FRouterTask().startInitTask();启动初始化任务加载;

参数说明

  • taskIdentifier 任务的唯一标识
  • deepenOn 此任务依赖任务的唯一标识,中间用逗号隔开
  • isInitTask 是否是初始化任务,默认false
  • isNeedAwait 是否是需要等待的异步任务,默认false

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介其实UserInfoPage中已经写好了几个全局任务~

其运行结果是这样的:

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介

2.3.2 模块全局任务

全局任务的调用像这样即可:

FRouterTask()
    .loadTaskMeta('moduleA_test_parameter')
    ?.apply(parameters: ['燕子,没有你我可怎么活啊']);

apply 方法有两个可选参数,一个是positionalArguments,一个是namedArguments,作用就是字面意思,一个传位置固定类型的入参,一个传命名类型的入参;

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介

【Flutter】抄一个路由轮子到Flutter上能有多难(二)—— 路由表部分的原理与使用简介

画饼专用的RoadMap

todo

  • 路由框架

    就像之前说的,现在仅仅是路由库的部分,可以说现在是能用而已,就好比一个仅仅支持了命令行操作,但没有GUI界面的程序;离方便好用还缺点东西;

    另外我也想故意不小心保留一点自己理想路由的一些功能;

  • 模块mock和测试支持

    这块的东西该咋结合mock,还真没细细了解过;之后看看TheRouter怎么实现的;

  • 启动优化加速(flutter3.7版本才可以)

    其实看到有向无环图,再结合上flutter3.7的特性,老android们估计就笑了,毕竟这些都是镌刻在灵魂深处的八股文~~不过细想一下,好像任务量还不少,基础部分缺失的还不少…………好像实现完,都可以单出一个插件了?

    不过不得不说,这回flutter3.7更新了一些底层开放能力,这才好玩嘛~~~

  • 单模块自动初始化任务?

    看了下TheRouter中的这部分,感觉其实就是提供了一个特殊的deepOn标志,然后监听路由变化,在特定页面调用这些特定标志标记的任务而已

    感觉需要结合上路由的监听;所以打算放到路由部分中统一完成;

  • 任务结果自动注入

    后来想了想,可能还存在某些任务需要其他任务的返回结果什么的;举个例子,根据ABTest模块的初始化返回结果,判断是否启用某个悄咪咪窃取用户信息模块的初始化任务;

    虽说这玩意现在也不是不能实现,实在不行我通过调用全局任务的做法获取返回值之类的,并将后续逻辑都统统写在一起放到一个大方法中,再以全局任务的形式提供出去;但感觉这块如果有个依赖注入的功能更好;毕竟,如果以第一段中的例子为例,也不是不存在ABTest模块和目标模块之间有着一大段其他模块初始化任务的可能性;强行写到一个任务中非常难管理;

总结

其实总结也没啥好写的,甚至那一套什么欢迎Issue欢迎PR之类的东西都因为项目还处于私有demo阶段也没法写%……;

不过要说的话,虽说路由这东西本身并不难,主要难点还是实现这块,毕竟资料少;实际上项目功能点实现思路和设计方面,其实主要还是靠下面这些优秀的开源项目:

特别感谢部分

  • 要实现的功能列表:TheRouter(确实是最符合我心目中的跨模块解决方案,所以对标的功能直接按他们的来,好像这篇文章的模版也是洗稿他们的~)

  • 路由部分的核心框架和思路来自于:ARouter(虽然前面提了TheRouter,但单单挑出路由设计上,我还是感觉ARouter的方式更符合模块化设计)

  • 路由表的思路收到了 CC 的影响;(可惜Flutter不支持完美的那种基于事件消息而非路由的通讯方式,要实现这点的话,估计要接原生了,那就太重了)

  • 参数解析功能直接照抄:SpringMVC(还是SpringMVC比较符合多端URI通讯的方案)

  • ast 分析受到了 ff_annotation_route 的启发(虽然并没有采用它的方案,但是通过阅读它的源码收到了很大的启发)

  • 路由JSON处理和动态化能力思路来自 Fair(动态化的部分思路基本照搬Fair,也就核心实现不同,不过因此,像在没有映射缓存的情况下无法做到动态化之类的问题,也给继承过来了✌️)

  • 启动优化部分来自 AppStartFaster (Android启动优化的老祖宗之一,虽然可能随着它的方案被集成进了官方而降低了维护频率,但确实是我启动优化的思路来源)

另外,特别感谢 Github Copilot!!!

学习api的过程中没有文档和demo怎么办?就给你一堆没写注释的test工程,连这个test作用是什么都不知道,两眼抓瞎不知道怎么写怎么办?

试试Colilot吧,Github Copilot 收录了全github的代码,你不懂没关系,总有人懂并通过copilot告诉你怎么写;AI的发展速度确实真的令人惊喜~

///todo: 问下ChatGpt如何用ChatGpt润色文章,再问下ChatGPT写啥说辞,才能让像我这种的白嫖精能给点改进建议啥的;

转载自:https://juejin.cn/post/7209825720385011771
评论
请登录