Flutter代码开发规范

[toc]

Flutter代码开发规范

一、目录

目录结构请查看:项目目录结构规范

二、文件+类命名

1、类文件命名

查看dart源码发现,源文件名都是小写英文加上下划线组成,如app_bar.dart;

使用小写加下划线来命名库和源文件

1
2
3
4
5
// good 
app_bar.dart

// bad
AppBar.dart

2、资源文件命名

命名:功能名称 _ 控件类型 _ 自定义参数*(标准或者大字版).png

1
2
标准:share_btn_weixin.png
选中:share_btn_weixin_selected.png

三、类

1、class命名

一般情况下,类名是源文件名的大驼峰写法,如源文件app_bar.dart,类名AppBar;源文件bottom_app_bar,类名BottomAppBar;

1
2
3
class AppBar {

}

2、import

2.1、import顺序

为了使你的文件前言保持整洁,我们有规定的命令,指示应该出现在其中。每个“部分”应该用空行分隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ①dart库
import 'dart:async';
import 'dart:html';

// ②三方库
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

// ③项目文件
import 'package:my_package/util.dart';

// ④相对文件
export 'src/error.dart';

2.2、import as

使用小写加下划线来命名导入前缀

1
2
3
4
5
6
7
8
9
// good
import 'dart:math' as math;
import 'package:angular_components/angular_components'
as angular_components;

// bad
import 'dart:math' as Math;
import 'package:angular_components/angular_components'
as angularComponents;

3、构造函数

3.1、构造函数定义

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
num x, y;

// good
Point2(this.x, this.y);

// bad
Point2(num x, num y) {
this.x = x;
this.y = y;
}
}

3.2、构造函数设计技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

class WishPublishPage extends BJHBasePage {
final bool isUpdateBusiness;
final WishDetailModel wishDetailModel;
final bool reEditPublish;
final Function backCallBack;// 从哪个页面跳转过来的 用于区分游戏中心的跳转

WishPublishPage({
Key key,
this.isUpdateBusiness = false,
this.reEditPublish = false,
this.wishDetailModel,
this.backCallBack,
}) : super(key: key);

WishPublishPage.fromOtherUser({
Key key,
this.reEditPublish = false,
this.isUpdateBusiness = false,
this.wishDetailModel,
List<WishGood> selectGoodsList,
this.backCallBack,
}) : super(key: key) {
wishDetailModel.wishGoods = selectGoodsList; // 暂时只平移代码,不修改(原本可能有问题)
}

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

四、其他命名

1、常量、变量命名

使用小驼峰法命名

1.1、常量

1
2
3
const num pi = 3.14;
const int defaultTimeout = 1000;
final RegExp urlScheme = RegExp('^([a-z]+)

1.2、变量

1
2
3
List<String> bookList;
Map<String, dynamic> bookMap;
bool isShowDetail; // 使用is开头+具体的业务场景命名

非特殊需要,禁止使用dynamic。(无法发挥空安全优势)

1
2
3
4
5
6
7
8
9
10
// 错误代码
dynamic string1 = sp.getString();
if ( string1.length == 2 )

// bugly报错
string1 出现 null

// 正确修改
String? string1 = sp.getString();
if ( string1 != null && string1!.length == 2 )

1.3、枚举命名

1
2
3
4
5
/* 枚举的用途 */
enum Direction {
top, //类型的用途 xxxx
left,//类型的用途 xxxx
}

五、基础接口使用规范

判空(不使用length)

1
2
3
4
5
6
7
// good: isEmpty/isNotEmpty
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');

// bad:length
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');

End

埋点方案-页面和视图的显示和离开

[toc]

埋点方案-页面和视图的进入和离开

一、页面进入、离开的监控方案

视图创建基类 LifeCyclePage,使用 route 方法,并提供以下方法

1
2
3
4
5
// 当前页面显示了,底层必须先执行 super.viewDidAppear
void viewDidAppear(AppearBecause appearBecause)

// 当前页面消失了,底层必须先执行 super.viewDidDisappear
void viewDidDisappear(DisAppearBecause disAppearBecause);

二、视图进入、离开的监控方案

场景:运行需要知道页面下的tab、页面下的tab的《子tab/子视图》的浏览时间。

方案1:点击–view_show/hide–不推荐

使用点击 view_show/hide 方案,实现步骤如下:

点击tab1:记tab1 view_show 事件,并记下当前显示的tab,备后续切换的时候为本tab添加 view_hide 事件。

点击tab2:记tab1 view_hide 事件,tab2 view_show 事件,并记下当前显示的tab,备后续切换的时候为本tab添加 view_hide 事件。

缺点:tab点击时候,记下当前显示的tab,备后续切换的时候为本tab添加 view_hide 事件。

方案2:视图是否显示监控–visibility–推荐方案

视图创建基类 LifeCycleView,使用 visibility 方案,并提供以下方法

1
2
3
4
5
// 当前页面显示了,底层必须先执行 super.viewDidAppear
void viewDidAppear(AppearBecause appearBecause)

// 当前页面消失了,底层必须先执行 super.viewDidDisappear
void viewDidDisappear(DisAppearBecause disAppearBecause);

2.1、原理:创建 VisibilityDetector 视图类,并设置 onVisibilityChanged 回调方法;

1
2
3
4
5
6
7
const VisibilityDetector({
required Key key,
required Widget child,
required this.onVisibilityChanged,
}) : assert(key != null),
assert(child != null),
super(key: key, child: child);

优点:不用每个视图(如tab页)去处理。

总结

页面和视图的进入和离开方案,总结如下:

序号 类型 方案 优点与不足
1 页面 LifeCyclePage + route 能比view增加具体显示和离开的原因
2 页面内的视图 LifeCycleView + visibility 只能知道显示、还是离开

End

埋点规范

[toc]

埋点规范

参考文档:

一、埋点入参

1、【固定的】公共入参

1.1、【固定的】的【基本】公共入参7个

建议和《基础规范-请求规范》 中的request公共入参保持一致

属性名 说明 备注
app_id App的唯一标识 com.ciyouzen.beyond
app_name App的应用名称 XXX测试版
app_version App的应用版本 1.16.30
app_buildNumber App的应用编译号 16301610
channel_name 渠道名称 《请求规范》 中的request公共入参的渠道】
platform/lib SDK 类型 Android/iOS
account_id 用户唯一标识 用户没有登录时,客户端自动生成唯一标识

1.2、【固定的】的【设备及系统信息】公共入参

属性名 说明 备注
systemVersion/lib_version 操作系统版本 16.1.1
manufacturer 设备制造商
brand 设备品牌 iPhone
model 设备型号 iPhone
os 操作系统 iPhone13,3
os_version 操作系统版本
screen_width 屏幕宽度 390.0
screen_height 屏幕高度 844.0
screen_devicePixelRatio 设备像素比率 3.0
carrier SIM 卡的运营商名称
device_id 设备ID Android 端主要取 Android ID ,
iOS 端先尝试获取 IDFA,如果获取不到,则取 IDFV
geo_coordinate_system 坐标系:iOS 端默认为 WGS84, Android 端需要手动传入 bd09ll

1.3、【固定的】的【其他】公共入参

属性名 说明 备注
lib_method 埋点方式 固定值为code

2、【可变的】的Body入参

2.1、单记录Body:【可变的】的【基本】入参

属性名 说明 备注 是否可全局按需时时获取
network_type 网络类型
mobile / wifi / bluetooth / vpn / other
mobile
timezone App 或系统的时区
Duration timezoneOffset = DateTime.now().timeZoneOffset;
Asia/Shanghai
timezone_offset 时区偏移量(以分为单位)
int timezoneOffsetMinutes = timezoneOffset.inMinutes;
480
latitude GPS信息-纬度
longitude GPS信息-经度
request_time 单条记录生成时间,精确到毫秒 批量时,多条应是同一个时间

2.2、单记录Body:【可变的】的【其他】入参

属性名 说明 备注
param 服务端下发 如算法标识等
event_name 事件名称 如“AppInstall”
event_attr 事件属性对象集合 event_name=”AppInstall”,event_attr={“accountId”:”1234”,”city”:”厦门”}

1、event_attr 希望的其他参数

属性名 说明 备注 是否可全局按需时时获取
cur_page 当前页面,用于标明 position/scene “goods_detail_page” 可全局记录旧页面
cur_view 当前视图,用于标明 position/scene “sku_choose_window” 必须请求单独设置
referrer_title 前一个页面标题

二、埋点事件

1、App事件

事件描述 事件名
冷启动 appStart
关闭应用 appEnd
返回前台/热启动 appResumed
进入后台 appPaused

1.1、打开/冷启动 appStart

参数 说明 示例
—- —-

1.2、关闭应用 appEnd

参数 说明 示例
—- —-

1.3、返回前台/热启动 appResumed

参数 说明 示例
—- —-

1.4、进入后台 appPaused

参数 说明 示例
duration 页面停留的时长(毫秒)

2、页面停留事件

事件描述 事件名
显示页面 viewDidAppear
离开页面 viewDidDisappear
1
2
3
4
5
enum AppearBecause {
newCreate, // 新显示
pop, // 从其他界面pop回来的
resume, // 从后台进入前台
}
1
2
3
4
5
enum DisAppearBecause {
goNew, // 去新的页面
pop, // 退出当前界面
pause, // 从前台进入后台
}

2.1、原生页面停留

2.1.1、进入页面 viewDidAppear
参数 示例 其他
page 页面类名 Goods_detail_page
cause 进入的方式 newCreate / pop
2.1.2、离开页面 viewDidDisappear
参数 说明 示例
page 页面类名 Goods_detail_page
cause 离开的方式 goNew / pop
duration 页面停留的时长(毫秒)
2.1.3、页面/视图进入、离开的监控方案

页面/视图进入、离开的监控方案:ShowMonitorMethod,详见《埋点方案-页面和视图的进入和离开

2.2、游戏页面停留

2.2.1、进入页面 viewDidAppear
参数 示例 其他
page 页面类名 WebViewPage
cause 进入的方式 newCreate / pop
url 网页地址
2.2.2、离开页面 viewDidDisappear
参数 说明 示例
page 页面类名 WebViewPage
cause 离开的方式 newCreate / pop
duration 页面停留的时长
url 网页地址

3、页面加载事件

事件描述 事件名
页面加载开始 firstStart
页面首帧绘制结束 firstAppFrameEnd
页面首屏绘制结束
(取到数据即算,不论是后台还是网络)
firstUserScreenEnd
页面首屏绘制结束
(以相关请求的最后一个为结束为结束)
firstNetworkScreenEnd

3.1、页面加载开始 firstStart

参数 示例 其他
page 页面类名 GoodsDetailPage

3.2、页面首屏绘制结束 firstNetworkScreenEnd

参数 说明 示例
page 页面类名 GoodsDetailPage
requestCount 请求的网络个数 3
duration 页面加载的时长(毫秒)

4、点击与曝光事件(非页面)

描述 点击事件名 曝光事件名 属性示例
商品 click_goods exposure_goods {“id”: id, “sku_id”: sku_id}
用户 click_user exposure_user {
“userId”: userId,
“userType”: avatar/name/other
}
订单 click_order exposure_order {“id”: id}
卡片 click_card exposure_card {
“cardId”: cardId,
“cardType”: user/goods/banner,
“cardOwnerId”: cardOwnerId
}
tab(一个页面多tab) click_tab
(使用该tab的视图曝光)
{“tab_name”: tab_name}
照片切换 click_photo_change 无(不需要) {“count”: 3, “old_index”: 0, “cur_index”: 2}
照片浏览 click_photo_browse 无(不需要) {count”: 3, “index”: 0}

4.1、卡片参数

参数 说明 示例
cardPosition/cardScene 卡片位置/场景
cardType 卡片类型(banner、user、goods)
cardId 卡片id
cardOwnerId 卡片持有者id(可空)

四、全埋点

参考文章:

1、点击自动埋点 autoTrackClick

参数 示例 其他
path 按钮路径
content 按钮上的文本,如果存在
screentName 当前页面名,同于page,却值不同

四、埋点页面名

1、一级页

页面描述 页面类名 页面埋点名
主页
作品首页 WishHomePage WishHomePage
商城首页 MallHomePage MallHomePage
发布页面
消息首页
我的首页

1.1、一级页中的标签页

页面描述 页面类名 页面埋点名
作品–推荐
作品–同城 WishDiscoverNearby WishDiscoverNearby
作品–热单 WishDiscoverHot WishDiscoverHot
作品–关注 WishDiscoverFriend WishDiscoverFriend

2、二级页

2.1、作品相关

页面描述 页面类名 页面埋点名
作品–推荐–推荐用户
作品–同城–
作品–热单–获豆榜 RankBeanWidget RankBeanWidget
作品–热单–愿望榜 RankWishWidget RankWishWidget
作品–关注–关注/粉丝/互关
作品详情页

2.2、商品相关

页面描述 页面类名 页面埋点名
商品详情页 GoodsTaDetailPage GoodsTaDetailPage

2.3、发布相关

页面描述 页面类名 页面埋点名
添加商品页

2.4、消息相关

页面描述 页面类名 页面埋点名
消息–收到的打赏 MsgCostPage MsgCostPage
消息–实现的愿望 MsgWishPage MsgWishPage
消息–新增的关注 MsgFocusPage MsgFocusPage
消息–互动消息–收到的评论 MsgCommentPage MsgCommentPage
消息–互动消息–发表的评论 InteractiveMessageListPage InteractiveMessageListPage
消息–互动消息–赞过的评论 InteractiveMessageListPage InteractiveMessageListPage
消息–系统消息
消息–聊天页面

2.5、我的相关

页面描述 页面类名 页面埋点名
我的–关注 MyFriendsPage MyFriendsPage
我的–粉丝 MyRelationPage MyRelationPage
我的–愿望豆 StarHistoryPage StarHistoryPage
我的–我的农场
我的–我的订单 MyOrderMainPage MyOrderMainPage
我的–我的足迹 RecordPage RecordPage
我的–我的收藏 CollectPage CollectPage

3、其他

页面描述 页面类名 页面埋点名
意见反馈 FeedbackPage FeedbackPage
平台客服 APPIMChat APPIMChat

End

瘦身-③app安装包

[toc]

一、查看包大小

序号 类型 描述 其他
1 下载大小 此值为通过无线下载的压缩 App 大小。
2 安装大小 此值为此 App 将在用户设备上占用的磁盘空间大小。

1、查看iOS包大小

1、进入官网 https://appstoreconnect.apple.com/apps

2、选择指定app –> TestFilght –> 指定版本 –> 指定构建号

3、查看内容如下

app_package_size_lookup_ios

二、如何瘦身

iOS APP瘦身优化

Flutter 如何缩减接近 50% 的 Flutter 包体积

三、项目示例

assets_generator

Python3安装xlsxwriter

依赖关系可视化

[toc]

依赖关系可视化

参考文档:

一、cocoapods依赖关系可视化

cocoapods管理的三方库之间有时候存在依赖关系,Podfile.lock藏满了各个版本库的版本号信息和彼此的依赖关系。

1、网页可视化导出:cocoapods-graph

终端安装插件:

1
pip install cocoapods-graph

使用方式:

进入到工程的根目录,执行:

1
2
3
cd ~/Project/CQComponent/UIKit-Overlay-iOS/TSOverlayDemo

cocoapods-graph -f Podfile.lock --html

生成 Podfile.lock.html 文件,

cocoapods-graph-01

其内容展示如下:

cocoapods-graph-02

2、图片可视化导出:cocoapods-dependencies

终端安装插件:

1
2
gem install cocoapods-dependencies
brew install graphviz

使用方式:

进入到工程的根目录,执行:

1
pod dependencies [PODSPEC] [--graphviz] [--image]
  • pod 是 CocoaPods 的命令行工具。
  • dependencies 是 CocoaPods 中的一个命令,用于显示特定 pod 或项目中所有 pod 的依赖关系。
  • [PODSPEC] 是一个可选参数,用于指定特定 pod 的名称。如果你提供了 pod 的名称,它将显示该 pod 的依赖关系。如果你没有提供 pod 的名称,它将显示项目中所有 pod 的依赖关系。
  • --graphviz 是一个可选标志,它会生成一个 Graphviz DOT 文件(一般为Podfile.gv),表示 pod 的依赖关系图。这个文件可以用于可视化依赖关系。
  • --image 是一个可选标志,它会从 Graphviz DOT 文件生成一个图像文件(一般为Podfile.png )。这样你就可以以图像格式查看依赖关系图。

示例1:不提供 [PODSPEC] 将显示项目中所有 pod 的依赖关系。

1
2
3
4
5
6
cd ~/Project/CQComponent/UIKit-Overlay-iOS/TSOverlayDemo

pod install

# 不提供 [PODSPEC] 将显示项目中所有 pod 的依赖关系。
pod dependencies --graphviz --image

生成 Podfile.gvPodfile.png文件如下:

cocoapods-dependencies-01-html

其中 Podfile.gv 可用 OmniGraffle应用程序 打开

gv的打开方式

打开方式说明如下:

序号 打开方式 打开说明 示例
1 Hieracchical 层次结构
2 Force-Directed 强制导向
3 Circular 圆形的
4 Radial 径向的

其各自详细效果图,详见 依赖关系可视化-TSOverlayDemo.graffle

示例2:提供 [PODSPEC] 将显示该 pod 的依赖关系

命令:pod dependencies AFNetworking --graphviz --image

这个命令将显示 AFNetworking 这个 pod 的依赖关系,并生成一个 Graphviz DOT 文件和相应的图像文件,用于可视化查看 AFNetworking 的依赖关系。你可以在生成的图像文件中查看依赖关系图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1、安装
gem install reversepoddependency

# 2、查找所安装库里的命令路径
gem which reversepoddependency
# gem which:这是一个用于定位 Ruby gem 安装路径的命令。当你执行 gem which GEM_NAME 时,它会返回指定 gem 的安装路径。
# which:这是一个用于查找可执行文件的命令。
# 得到的结果如下:
# /usr/local/lib/ruby/gems/3.2.0/gems/reversepoddependency-0.1.0/lib/reversepoddependency.rb
# 所以该包的 specbackwarddependency 应在如下目录:
# /usr/local/lib/ruby/gems/3.2.0/gems/reversepoddependency-0.1.0/
# 经查找 specbackwarddependency 的其实际地址为如下:
# /usr/local/lib/ruby/gems/3.2.0/gems/reversepoddependency-0.1.0/exe/specbackwarddependency

# 3、使用所安装库里的命令
/usr/local/lib/ruby/gems/3.2.0/gems/reversepoddependency-0.1.0/exe/specbackwarddependency ~/.cocoapods/repos/trunk AFNetworking

在运行 gem install reversepoddependency 命令后,安装的 reversepoddependency gem 应该会被存储在你使用的 Ruby 安装目录下的 Gems 文件夹中。

问题:执行 /usr/local/lib/ruby/gems/3.2.0/gems/reversepoddependency-0.1.0/exe/specbackwarddependency ~/.cocoapods/repos/trunk AFNetworking 时候出现如下错误:

specbackwarddependency_01

我们通过进入指定文件/usr/local/lib/ruby/gems/3.2.0/gems/cocoapods-core-1.12.1/lib/cocoapods-core/source.rb 查看其210行,并添加以下代码,即可过滤掉该库:

1
2
3
4
if name == "TUICalling" || name == "TUIChat" || name == "VK-ios-sdk" 
name = "AFNetworking"
version = "3.2.1"
end

specbackwarddependency_02

不然你得通过以下命令找到该所在库,然后再一个个删掉,这种方式会造成后续使用问题。所以我们选择修改判断代码

1
2
3
4
cd ~/.cocoapods/repos/trunk/Specs

# 查找指定库
find ~/.cocoapods/repos/trunk/Specs -name 'TUICalling'

添加代码后,重新执行

1
/usr/local/lib/ruby/gems/3.2.0/gems/reversepoddependency-0.1.0/exe/specbackwarddependency ~/.cocoapods/repos/trunk AFNetworking

得到的结果如下:

specbackwarddependency_03

End

项目目录结构规范实践

[toc]

项目目录结构规范实践

步骤

1、项目目录结构分类与创建

2、

一、项目目录结构分类与创建

1、utils
2、models
3、commons

1、独立类(utils\models\common)的文件归类、迁移

1.1、库创建(0.5h)

1.2、文件归类与迁移(4h)

1、util

2、models

用户

序号 类型
1 用户
2 内容
3

1.3、内部的依赖修复与优化

内部引用优化为使用相对路径或新的绝对路径。

1
import './a.dart';

1.4、外部的使用优化

优化为

Step1:内部 export 各种文件

1
2
export './src/a.dart';
export './src/b.dart';

Step2:外部 import 基础类库的总文件

1
2
3
import 'package:app_utils/app_utils.dart';
import 'package:app_models/app_models.dart';
import 'package:app_common/app_common.dart';

2、通用组件类(commons)文件归类、迁移

1、内含适配组件 adapt

每个视图都需要依赖

2、基础用户头像、商品视图

依赖:基础的像素适配

入参:关键性参数及自定义的点击回调

附:升级版通用功能组件类common_ui(如用户头像、商品视图等),见下文。

3、基础路由(route)

4、升级版通用功能组件类common_ui(如用户头像、商品视图等)

以用户头像举例:

①需要支持传入baseUserModel的时候,显示头像信息

②需要支持传入baseUserModel的时候,点击跳转到对应用户详情页

所以需要依赖的有:

序号 需求 需要依赖 其他
1 需要支持传入baseUserModel的时候,显示头像信息 models
2 需要支持传入baseUserModel的时候,点击跳转到对应用户详情页 route

商品视图:

基础类空安全升级

网络ApiMock

[toc]

网络Api mock

一、背景

为了避免后端因为其他任务,来不及开发接口,特意建立mock环境,以此来保证后续出现类似情况,app也能够正常的开发,并能够在后端开发结束时候,快速完成整体的联调,从而在某个环节断档的情况下,也能够保证整体的进度。

二、功能介绍

  • 远程模拟
  • 本地模拟

三、远程模拟

1、远程界面样式

目前该环境,已对现有所有app接口实现mock。

image-20220119193932769

2、使用简介

2.1、修改地址

如果你已在api mock后台创建了模拟的接口,则只需要在项目中进行如下修改即可,其会自动将所请求的地址进行模拟。

1
2
3
4
static String getVirtualList = "/account/wallet/virtualAsset/page"

//模拟时候,在字符串尾部加上`.toSimulateApi()`即可
static String getVirtualList = "/account/wallet/virtualAsset/page".toSimulateApi();

2.2、修改mock数据

因为从Swagger同步过来的接口,其返回值不是完整的response结构。

完整的response={“code”:0, “msg”: “成功”, data:xxx};

同步的response=data

所以为避免每个接口都得重复的去添加response外层来调整结构,我们通过在本地网络上进行兼容,从而实现了即使你没对同步的接口进行操作,也能够直接调用请求,得到完整的数据结构。

四、本地模拟

1、使用简介

以模拟/account/wallet/wishStar/page接口为例

1.1、添加本地json

在项目中asset下的的data文件夹里添加该以请求路径命名的json文件。

image-20220322134145737

1.2、修改地址

将请求地址的尾部加上.toLocalApi(),即可自动将所请求的地址进行本地数据模拟。

1
2
3
4
static String wishStar = "/account/wallet/wishStar/page"; //愿望星流水明细

//模拟时候,在字符串尾部加上`.toLocalApi()`即可
static String wishStar = "/account/wallet/wishStar/page".toLocalApi(); //愿望星流水明细

####

_random.nextDouble() 获取的是0-1之间不为负数的小数

四舍五入

1
2
var foo = 6.28;
print(foo.round()); // 6

End

网络扩展

[toc]

网络扩展

一、背景

TPFVPN

NetworkExtension2-Client开发

NetworkExtension3-Tunnel开发

NetworkExtension4-Server开发

NetworkExtension5-App和Extension通信

https://github.com/cgcym1234/YYVPN

移动/PC客户端流量拦截与转发

深信服移动终端EasyApp SDK介绍

其他github搜索结果:https://github.com/search?q=vpn+language%3AObjective-C+&type=repositories&s=stars&o=desc

其他文章:App extension实战 - NetworkExtension 讲解连接并捕获packet

End