[toc]

Flutter 最常出现的错误

典型错误一:无法掌握的Future

典型错误信息:NoSuchMethodError: The method 'markNeedsBuild' was called on null.

这个错误常出现在异步任务(Future)处理,比如某个页面请求一个网络API数据,根据数据刷新 Widget State。

异步任务结束在页面被pop之后,但没有检查State 是否还是 mounted,继续调用 setState 就会出现这个错误。

示例代码

一段很常见的获取网络数据的代码,调用 requestApi(),等待Future从中获取response,进而setState刷新 Widget:

1
2
3
4
5
6
7
8
9
10
class AWidgetState extends State<AWidget> {
// ...
var data;
void loadData() async {
var response = await requestApi(...);
setState((){
this.data = response.data;
})
}
}

原因分析

response 的获取为async-await异步任务,完全有可能在AWidgetStatedispose之后才等到返回,那时候和该State 绑定的 Element 已经不在了。故而在setState时需要容错。

解决办法: setState之前检查是否 mounted

1
2
3
4
5
6
7
8
9
10
11
12
class AWidgetState extends State {
// ...
var data;
void loadData() async {
var response = await requestApi(...);
if (mounted) {
setState((){
this.data = response.data;
})
}
}
}

这个mounted检查很重要,其实只要涉及到异步还有各种回调(callback),都不要忘了检查该值。

比如,在 FrameCallback里执行一个动画(AnimationController):

1
2
3
4
5
6
@override
void initState(){
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _animationController.forward();
});
}

AnimationController有可能随着 State 一起 dispose了,但是FrameCallback仍然会被执行,进而导致异常。

又比如,在动画监听的回调里搞点事:

1
2
3
4
5
6
7
8
9
@override
void initState(){
_animationController.animation.addListener(_handleAnimationTick);
}


void _handleAnimationTick() {
if (mounted) updateWidget(...);
}

同样的在_handleAnimationTick被回调前,State 也有可能已经被dispose了。

如果你还不理解为什么,请仔细回味一下Event loop 还有复习一下 Dart 的线程模型。

典型错误二:Navigator.of(context) 是个 null

典型错误信息:NoSuchMethodError: The method 'pop' was called on null.

常在 showDialog 后处理 dialog 的 pop() 出现。

示例代码

在某个方法里获取网络数据,为了更好的提示用户,会先弹一个 loading 窗,之后再根据数据执行别的操作…

1
2
3
4
5
6
7
8
9
10
11
12
13
// show loading dialog on request data
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) {
return Center(
child: CircularIndicator(),
);
},
);
var data = (await requestApi(...)).data;
// got it, pop dialog
Navigator.of(context).pop();

原因分析:

出错的原因在于—— Android 原生的返回键:虽然代码指定了barrierDismissible: false,用户不可以点半透明区域关闭弹窗,但当用户点击返回键时,Flutter 引擎代码会调用 NavigationChannel.popRoute(),最终这个 loading dialog 甚至包括页面也被关掉,进而导致Navigator.of(context)返回的是null,因为该context已经被unmount,从一个已经凋零的树叶上是找不到它的根的,于是错误出现。

另外,代码里的Navigator.of(context) 所用的context也不是很正确,它其实是属于showDialog调用者的而非 dialog 所有,理论上应该用builder里传过来的context,沿着错误的树干虽然也能找到根,但实际上不是那么回事,特别是当你的APP里有Navigator嵌套时更应该注意。

解决办法

首先,确保 Navigator.of(context)context 是 dialog 的context;其次,检查 null,以应对被手动关闭的情况。

showDialog 时传入 GlobalKey,通过 GlobalKey去获取正确的context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GlobalKey key = GlobalKey();


showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) {
return KeyedSubtree(
key: key,
child: Center(
child: CircularIndicator(),
)
);
},
);
var data = (await requestApi(...)).data;


if (key.currentContext != null) {
Navigator.of(key.currentContext)?.pop();
}

key.currentContextnull意为着该 dialog 已经被dispose,亦即已经从 WidgetTree 中unmount

其实,类似的XXX.of(context)方法在 Flutter 代码里很常见,比如 MediaQuery.of(context)Theme.of(context)DefaultTextStyle.of(context)DefaultAssetBundle.of(context)等等,都要注意传入的context是来自正确节点的,否则会有惊喜在等你。

写 Flutter 代码时,脑海里一定要对context的树干脉络有清晰的认知,如果你还不是很理解context,可以看看 《深入理解BuildContext》 - Vadaski。

典型错误三:ScrollController 里薛定谔的 position

在获取ScrollControllerpositionoffset,或者调用jumpTo()等方法时,常出现StateError错误。

错误信息:StateError Bad state: Too many elementsStateError Bad state: No element

示例代码

在某个按钮点击后,通过ScrollController 控制ListView滚动到开头:

1
2
3
4
5
final ScrollController _primaryScrollController = ScrollController();
// 回到开头
void _handleTap() {
if(_primaryScrollController.offset > 0) _primaryScrollController.jumpTo(0.0)
}

原因分析

先看ScrollController的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ScrollController extends ChangeNotifier {
//...
@protected
Iterable<ScrollPosition> get positions => _positions;
final List<ScrollPosition> _positions = <ScrollPosition>[];

double get offset => position.pixels;

ScrollPosition get position {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
return _positions.single;
}
//...
}

很明显,ScrollControlleroffest 是从 position 中获得,而position 则是来自变量 _positions

StateError错误,就是_positions.single 这一行抛出:

1
2
3
4
5
6
7
8
9
10
11
abstract class Iterable<E> {
//...
E get single {
Iterator<E> it = iterator;
if (!it.moveNext()) throw IterableElementError.noElement();
E result = it.current;
if (it.moveNext()) throw IterableElementError.tooMany();
return result;
}
//...
}

那么问题来了,这个_positions 为什么忽而一滴不剩,忽而却嫌它给的太多了呢?ˊ_>ˋ

还是要回到 ScrollController 的源码里找找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ScrollController extends ChangeNotifier {
// ...
void attach(ScrollPosition position) {
assert(!_positions.contains(position));
_positions.add(position);
position.addListener(notifyListeners);
}


void detach(ScrollPosition position) {
assert(_positions.contains(position));
position.removeListener(notifyListeners);
_positions.remove(position);
}
}
  1. 为什么没有数据(No element): ScrollController还没有 attach 一个 position。原因有两个:一个可能是还没被 mount 到树上(没有被Scrollable使用到);另外一个就是已经被 detach了。

  2. 为什么多了(Too many elements): ScrollController还没来得及 detach旧的 position,就又attach了一个新的。原因多半是因为ScrollController的用法不对,同一时间被多个 Scrollable关注到了。

解决办法

针对 No element 错误,只需判断一下 _positions是不是空的就行了,即hasClients

1
2
3
4
5
final ScrollController _primaryScrollController = ScrollController();
// 回到开头
void _handleTap() {
if(_primaryScrollController.hasClients && _primaryScrollController.offset > 0) _primaryScrollController.jumpTo(0.0)
}

针对 Too many elements 错误,确保ScrollController只会被一个 Scrollable绑定,别让它劈腿了,且被正确 dispose()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class WidgetState extends State {
final ScrollController _primaryScrollController = ScrollController();


@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _primaryScrollController,
itemCount: _itemCount,
itemBuilder: _buildItem,
)
}


int get _itemCount => ...;
Widget _buildItem(context, index) => ...;


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

典型错误四:四处碰壁 null

Dart 这个语言可静可动,类型系统也独树一帜。万物都可以赋值null,就导致写惯了 Java 代码的同志们常常因为bool int double这种看起来是”primitive”的类型被null附体而头晕。

典型错误信息:

  • Failed assertion: boolean expression must not be null
  • NoSuchMethodError: The method '>' was called on null.
  • NoSuchMethodError: The method '+' was called on null.
  • NoSuchMethodError: The method '*' was called on null.

示例代码

这种错误,较常发生在使用服务端返回的数据model时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StyleItem {
final String name;
final int id;
final bool hasNew;


StyleItem.fromJson(Map<String, dynamic> json):
this.name = json['name'],
this.id = json['id'],
this.hasNew = json['has_new'];
}


StyleItem item = StyleItem.fromJson(jsonDecode(...));


Widget build(StyleItem item) {
if (item.hasNew && item.id > 0) {
return Text(item.name);
}
return SizedBox.shrink();
}

原因分析

StyleItem.fromJson() 对数据没有容错处理,应当认为 map 里的value都有可能是 null

解决办法:容错

1
2
3
4
5
6
7
8
9
10
11
class StyleItem {
final String name;
final int id;
final bool hasNew;


StyleItem.fromJson(Map<String, dynamic> json):
this.name = json['name'],
this.id = json['id'] ?? 0,
this.hasNew = json['has_new'] ?? false;
}

一定要习惯 Dart 的类型系统,什么都有可能是null,比如下面一段代码,你细品有几处可能报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test {
double fraction(Rect boundsA, Rect boundsB) {
double areaA = boundsA.width * boundsA.height;
double areaB = boundsB.width * boundsB.height;
return areaA / areaB;
}

void requestData(params, void onDone(data)) {
_requestApi(params).then((response) => onDone(response.data));
}

Future<dynamic> _requestApi(params) => ...;
}

小提示,onDone()也可以是null >﹏<。

在和原生用 MethodChannel传数据时更要特别注意,小心驶得万年船。

典型错误五:泛型里的 dynamic 一点也不 dynamic

典型错误信息:

  • type 'List<dynamic>' is not a subtype of type 'List<int>'
  • type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'

常发生在给某个List、Map 变量赋值时。

示例代码

这种错误,也较常发生在使用服务端返回的数据model时。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Model {
final List<int> ids;
final Map<String, String> ext;


Model.fromJson(Map<String, dynamic> json):
this.ids = json['ids'],
this.ext= json['ext'];
}


var json = jsonDecode("""{"ids": [1,2,3], "ext": {"key": "value"}}""");
Model m = Model.fromJson(json);

原因分析

jsonDecode()这个方法转换出来的map的泛型是Map<String, dynamic>,意为 value 可能是任何类型(dynamic),当 value 是容器类型时,它其实是List<dynamic>或者Map<dynamic, dynamic>等等。

而 Dart 的类型系统中,虽然dynamic可以代表所有类型,在赋值时,如果数据类型事实上匹配(运行时类型相等)是可以被自动转换,但泛型里 dynamic 是不可以自动转换的。可以认为 List<dynamic>List<int>是两种运行时类型。

解决办法:使用 List.from, Map.from

1
2
3
4
5
6
7
8
9
class Model {
final List<int> ids;
final Map<String, String> ext;


Model.fromJson(Map<String, dynamic> json):
this.ids = List.from(json['ids'] ?? const []),
this.ext= Map.from(json['ext'] ?? const {});
}

End

[toc]

Flutter iOS 点击状态栏回到顶部

一、会回到顶部的原理

ios中一个常见的交互是:点击顶部栏时,自动将当前的滚动区滚到顶部。在flutter中,大部分时候这件事是“自然完成”的,但是也有时候会遇到这个行为失效的情况。要解决这个问题首先自然是要看这个feature是如何实现的。
其实大部分都是Scaffold里面干的事:
Scaffold里有这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
switch (themeData.platform) {
case TargetPlatform.iOS:
_addIfNonNull(
children,
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleStatusBarTap,
// iOS accessibility automatically adds scroll-to-top to the clock in the status bar
excludeFromSemantics: true,
),
_ScaffoldSlot.statusBar,
removeLeftPadding: false,
removeTopPadding: true,
removeRightPadding: false,
removeBottomPadding: true,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
break;
}

这里命名很舒服,可以直接看出来在干什么:如果是ios的话,那就给Scaffold加一个在状态栏上的点击区,点击的话就会触发一个函数,这个函数干的事情如下:

1
2
3
4
5
6
7
8
9
10
11
final ScrollController _primaryScrollController = ScrollController();

void _handleStatusBarTap() {
if (_primaryScrollController.hasClients) {
_primaryScrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.linear, // TODO(ianh): Use a more appropriate curve.
);
}
}

也就是,Scaffold会提供一个默认的ScrollController,而点击顶部栏会使得这个controller滚到顶部,在ScrollView的build函数中则会取这个controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);

// 注意,此处的primary不是传入的primary,
// primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical)
final ScrollController scrollController = primary
? PrimaryScrollController.of(context)
: controller;
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
);
return primary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
}

如果指定controller的话,就不会使用PrimaryScrollview,如果不指定的话,则在primary为true时使用PrimaryController,而默认情况下controller为null,primary为true,因此一个裸体的ListView是会相应屏幕点击的。
知道了原理,就很容易分析自己代码里出的问题是什么,常见的可能就是:
1、没加Scaffold,这个其实并不常见(自相矛盾草),不过可以检查一下,一般总是会有Scaffold的
2、真正常见的:指定了controller,如果自己创建了一个Controller丢给ScrollView,那必然是会失效的。但是使用controller又是一个很常见且重要的需求,怎么办呢?也很简单,就是不要自己创建新的ScrollController,而是直接取PrimaryScrollController.of(context)这个controller,对其进行自己要做的操作。
3、相对不太常见且需要分析具体代码的:多个Scaffold导致的冲突。
注意到其实flutter里的这个点击状态栏并不是真的点击了状态栏,而是点击了“Scaffold提供的位于状态栏的可点击区域”,也就是说,如果有多个Scaffold就会有多个这样的区域。实际情况是,只有最内部的Scaffold的状态栏会有响应,而如果ScrollView所处位置取到的和点击的Scaffold不一致,自然也就不会有滚动到顶部的feature

二、错误示例

今天遇到ios点击状态栏无法回到顶部(原理在文章后)的问题。研究后发现,Scaffold组件虽然会自带这个功能。但使用时候,必须遵循指定规则才行。

我们以点击 TapStatusNormalPage 上的状态栏返回顶部来举例。

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
// tap_status_normal_page.dart

import 'package:flutter/material.dart';

class TapStatusNormalPage extends StatefulWidget {
const TapStatusNormalPage({Key key}) : super(key: key);

@override
State<TapStatusNormalPage> createState() => _TapStatusNormalPageState();
}

class _TapStatusNormalPageState extends State<TapStatusNormalPage> {
@override
Widget build(BuildContext context) {
return Scaffold( // 注意这个页面已经包了 Scaffold 了
appBar: AppBar(
title: const Text('状态栏点击-Normal5'),
),
body: ListView.builder(
itemCount: 200,
itemBuilder: (context, index) {
return Text('$index');
},
),
);
}
}

错误一:如果Scaffold里面又套了一个Scaffold,那么这个回到顶部就会失效。

失效示例1:

1
2
3
4
5
6
7
8
9
10
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: TapStatusNormalPage(), // 失效原因:TapStatusNormalPage里已经有 Scaffold 了
);
}
}

这种情况,点击状态栏便不会回到顶部,我们需要保证的就是每个页面仅有一个Scaffold。

错误二:app的builder的Widget生成,如果child再多余包Scaffold会导致点击状态栏无法回到顶部

失效示例2:

虽然home里只有一个 Scaffold ,但app的builder的Widget生成,如果child再多余包Scaffold会导致点击状态栏无法回到顶部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: () => MaterialApp(
navigatorKey: navigatorKey,
title: 'wish',
home: TapStatusNormalPage(),
builder: EasyLoading.init(builder: (context, widget) {
return MediaQuery(
///设置文字大小不随系统设置改变
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
// child: Scaffold( // 注意:app的builder的Widget生成,如果child再多余包Scaffold会导致点击状态栏无法回到顶部
// resizeToAvoidBottomInset: false,
// body: widget,
// ),
child: widget, // OK
);
}),
),
);

错误三:一说要滚动就自定义controller

点击页面上的某个按钮,让页面上的列表滚动到顶部(不需要自定义controller)。

1
2
3
4
5
6
7
8
PrimaryScrollController.of(context).jumpTo(0);


PrimaryScrollController.of(context).animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.linear,
);

如果还要监听滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@override
void didChangeDependencies() {
super.didChangeDependencies();

if (!_hasEverInitListener) {
PrimaryScrollController.of(context).addListener(_handleScrollViewEvent);
_hasEverInitListener = true;
}
}

@override
void deactivate() {
super.deactivate();
PrimaryScrollController.of(context).removeListener(_handleScrollViewEvent);
}


_handleScrollViewEvent() {
// 滚动距离
double offsetY = PrimaryScrollController.of(context).offset;


}

错误四:Another exception was thrown: ScrollController attached to multiple scroll views.

问题:

flutter_swiper:Another exception was thrown: ScrollController attached to multiple scroll views

翻译一下:引发了另一个异常:ScrollController连接到多个滚动视图。

原因:

Flutter Swiper是一个轮播图组件,内部包含一个Widget List,当这个Widget List数量大于1,就可能会有这种情况

解决方案:给Swiper加一个Key即可解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
return Container(
child: AspectRatio(
aspectRatio: 1.5 / 1, // 宽高比450/300
child: Swiper(
key: UniqueKey(), // 这个必须添加,代表唯一
itemBuilder: (BuildContext context, int index) {
return new Image.network(
imgList[index]['url'],
fit: BoxFit.fill,
);
},
itemCount: imgList.length,
pagination: new SwiperPagination(),
control: new SwiperControl(),
autoplay: true,
),
),
);

End

[toc]

开发随笔

带皇冠的头像优化:

https://blog.csdn.net/lqw200931116/article/details/123372095

  • clipBehavior:此属性决定如何显示超出Stack的子组件是否被剪切;值为Clip.hardEdge时,超出部分会被剪裁(隐藏),值为Clip.none 时则不会,超出部分会显示

图片

为什么 Button 里 不要再设置颜色。猜测是由于DefaultTextStyle

[flutter 解决:setState() called after dispose()]

防止页面关闭执行setState()方法

解决方法

1
2
3
4
5
void updateState(fn){
if (mounted) {
setState(fn);
}
}

Flutter:获取状态state的几种方式

baseui_kit中的 data_util.dart

1
2
3
4
5
6
7
8
9
10
11
import 'package:flutter/material.dart';
// import 'dart:ui';

///枚举类型转string
String enumToString(o) => o.toString().split('.').last;

///string转枚举类型
T enumFromString<T>(Iterable<T> values, String value) {
return values.firstWhere((type) => type.toString().split('.').last == value,
orElse: () => null);
}

字符串与日期相互转换

String–>DateTime

1
DateTime.parse(String);

DateTime –>formatString

1
2
3
4
5
6
import 'package:date_format/date_format.dart';

formatDate(DateTime(2020, 12, 23) ,['yyyy', '-', 'mm', '-', 'dd']);
formatDate(DateTime(2020, 12, 23, 20, 40, 10), [‘HH’, ':', ‘nn’, ':', ‘ss’])

这里使用了日期格式化库:date_format。

Flutter 日期格式化库 date_format

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
flutter sdk 版本升级到2.0或者更高的版本后就发现运行时会报错:
Error: Cannot run with sound null safety, because the following dependencies
don't support null safety:

- package:loading_indicator_view
- package:flutter_swiper
- package:flutter_page_indicator
- package:transformer_page_view

For solutions, see https://dart.dev/go/unsound-null-safety


FAILURE: Build failed with an exception.

也就是说以上有些包不支持 safety模式。

解决方案:--no-sound-null-safety
run
flutter run --no-sound-null-safety
build
flutter build apk --no-sound-null-safety

Flutter Error: Cannot run with sound null safety, because the following dependencies don‘t support

Flutter2 的 Sound null safety 是个什么鬼?!

flutter_webview_plugin和webview_flutter

iOS中WKWebView加载网页失败或者无网络状态判断

flutter嵌入HTML5页面,Flutter加载Html并实现与JS 的双向调用

几种常见视频格式ContentType和FileType对照表

Content-Type的格式

Content-Type:type/subtype ;parameter

type:主类型,任意的字符串,如text,如果是号代表所有;
subtype:子类型,任意的字符串,如html,如果是
号代表所有,用“/”与主类型隔开;
parameter:可选参数,如charset,boundary等。
例如:

Content-Type: text/html;
Content-Type: application/json;charset:utf-8;
Content-Type: application/x-www-form-urlencoded;charset:utf-8;

Flutter Fix

共享数据

三方库

flutter SliverAppBar

埋点

曝光

Flutter 解决TextOverflow.ellipsis截断字符

布局

布局的性能优化

Git Flow

分支类型

  • master
  • dev
  • feature
  • hotfix
  • preproduct

master

线上代码分支。

【创建】在创建 repo 的时候创建 master 分支。

【命名】master

【描述】所有 release 的 tag 都打在 master,并且 master 不能直接 commit,只能通过 merge 来完成代码提交。

【输入】合并自 preproduct hotfix

【输出】hotfix

【销毁】master 分支是恒定存在的

dev

稳定开发分支

【创建】在创建 master 分支时,立即创建 dev 分支

【命名】dev

【描述】新版本需求来的时候管理员会从 dev 中拉出一条版本分支。dev 分支里保存完整的版本迭代和测试环境下已经修复的 bug。

【输入】合并自 feature hotfix preproduct

【输出】版本 release 时并入 master

【销毁】dev 分支是恒定存在的

feature / version

功能分支、新版本分支

【创建】当有新版本计划时,从 dev 中拉取

【命名】dev_1.0.0

【描述】存放某个版本的集成代码,包括这个版本在内测环境下的 bug 修复代码

【输入】开发人员 commit

【输出】版本结束后并入 dev

【销毁】并入后销毁

preproduct

某版本预生产分支

【创建】当某个版本的测试环境通过后,从 feature 分支并入 dev 分支时,从 dev 分支拉取出的分支

【命名】pre_1.0.0

【描述】存放某个版本的预生产代码,修复预生产时候的 bug

【输入】从 dev 中拉取,开发人员 commit

【输出】版本结束后同时并入 masterdev(不是并入 dev 再从 dev 并入 master

【销毁】并入后销毁

hotfix

热补丁分支

【创建】从 master 拉取。

【命名】hotfix/新版本号

【描述】需要紧急上线一个补丁版本,且已有新版本的 feature 分支时,需要创建该分支。如果当前没有新版本的 feature 分支,则按正常开发逻辑走流程。

【输入】开发人员 commit

【输出】先并入 master;判断 preproduct 是否存在 ? 并入 preproduct : 并入 dev

【销毁】并入后销毁

提交格式

使用 SourceTree 完成 commit,message 可以换行。

大致格式如下:

1
2
3
【信息分类】简略描述
详细描述
【URL】bugly 地址,或者 JIRA 地址

信息分类

Bugfix

【修复】修复自己开发过程中自测出来的问题

【修复-jira-{id}】修复 jira 上的问题,附带 bug id

【修复-bugly-{id}】修复 bugly 上的问题,附带 bug id

Feature

【新增】增加新的功能

Optmize

【优化】优化某些代码

Resource

【资源】资源文件的修改,不涉及代码

Configuration

【配置】修改版本号,修改打包脚本,修改工程配置,不涉及代码

PS

增加文件会更改到工程文件,此部分更改属于 Feature 的内容。

举例

增加某个功能:

1
2
【新增】购物车一键清空功能
批量删除购物车内的商品。

修复了某个自测 bug

1
2
【修复】清空购物车的时跳出支付页面
清空购物车的时候并没有删除商品,而是全选并支付。

修复了某个内测 bug

1
2
3
【修复-jira-10019】清空购物车按钮点击无效果
selector 编辑错误,导致 controller 没有响应事件。
【URL】http://jira.luckincoffee.com/products/luckincoffee/id_xxxxxx.html

修复了某个线上 bug

1
2
3
【修复-bugly-1191】WebView 闪退
对 WebView 中的编辑框操作(复制、全选、粘贴)时会闪退。
【URL】http://bugly.qq.com/luckincoffee/id_xxxxxx.html

时序图

feature

feature

GitFlow.alfredsnippets 使用

电脑安装 Alfred 3

双击 GitFlow.alfredsnippets 文件,导入到 Alfred 中

GitFlow.alfredsnippets 文件在此 repo 的 /page/iOS-Git-Flow 文件夹下可以找到

光标选中 SourceTree 的 commit 信息编辑框

激活 Alfred 窗口( 默认快捷键是 Option + Space ),输入 snip git 即可出现 commit 模板。

选中想要提交的内容,按 cmd + 数字 即可。

远程连接

[TOC]

一、MacBook Pro 怎么设置smb共享文件夹

在Mac要设置smb的共享文件夹,那么如何设置的好的呢?如何去设置一共享文件夹?

MacBook Pro 怎么设置smb共享文件夹

方法/步骤

  1. 右键一个桌面新建文件夹,然后点击新建文件夹。

    MacBook Pro 怎么设置smb共享文件夹

    MacBook Pro 怎么设置smb共享文件夹

  2. 把要共享文件,拖入新建文件夹,然后点击logo。

    MacBook Pro 怎么设置smb共享文件夹

    MacBook Pro 怎么设置smb共享文件夹

  3. 点击设置,点击共享。

    MacBook Pro 怎么设置smb共享文件夹

    MacBook Pro 怎么设置smb共享文件夹

  4. 文件共享勾上,点击加。

    MacBook Pro 怎么设置smb共享文件夹

    MacBook Pro 怎么设置smb共享文件夹

  5. 然后点击桌面选择新建文件夹添加,右边默认的读权限,如果要写的,那么该为读写。

    MacBook Pro 怎么设置smb共享文件夹

    MacBook Pro 怎么设置smb共享文件夹

  6. 之后点击用户,点击锁。

    MacBook Pro 怎么设置smb共享文件夹

    MacBook Pro 怎么设置smb共享文件夹

  7. 输入密码解锁,然后把两个允许勾上。

    MacBook Pro 怎么设置smb共享文件夹

    MacBook Pro 怎么设置smb共享文件夹

远程连接

[TOC]

一、mac被远程连接

1、先请在苹果 Mac 电脑上的“系统偏好设置”窗口中打开“共享”功能

img

2、接着在共享窗口中的左侧点击启用“屏幕共享”选项

img

3、当屏幕共享功能打开以后,请点击“电脑设置”按钮

img

4、随后请勾选二个选项: VNC 显示程序可以使用密码控制屏幕,并且设置一个连接密码

img

5、随后会提示我们是否真的要连接此电脑,点击 Continue 按钮继续。

img

二、mac连接已设置允许远程连接的mac

参考文章:https://segmentfault.com/q/1010000017860880

安装类库

某一语法:npm install [<@scope>/]@
某些示例:
//安装某一指定版本
npm install test@0.0.2
//安装指定范围版本
//安装指定范围版本(大于)
npm install test@”>0.0.1”
//安装指定范围版本(小于)
npm install test@”<0.0.1”
//安装指定范围版本(之间)
npm install test@”^0.0.2”
npm install test@”>=0.0.2 <0.0.3”

更新类库

某一语法:npm update [-g] […]
某些示例:

1
2
3
4
5
6
//更新到最新版本
npm update test
//更新到某一版本
npm update test@"0.0.2"
//更新到某范围版
npm update test@"^0.0.2" //即等于npm update test@">=0.0.2 <0.0.3"

CocoaPods

官网: https://cocoapods.org/

最新ruby源

1
2
3
gem sources --remove https://gems.ruby-china.org
gem sources -l
gem sources --add https://gems.ruby-china.com

一、cocoapods 的安装和升级

1、查看pod版本和安装路径

1
2
3
pod --version

which pod

2、安装 CocoaPods

1
2
sudo gem install -n /usr/local/bin cocoapods  # OS X 版本 >  10.11,使用此命令安装
sudo gem install cocoapods # OS X 版本 <= 10.11,使用此命令安装

其他参考:百度地图中的CocoaPods文章

3、升级

1
2
3
4
5
6
7
8
sudo gem update cocoapods
# 以下该行命令可升级cocoapods
gem install cocoapods

# 移除现有pod
rm -rf /usr/local/bin/pod
# 重新下载pod
sudo gem install cocoapods

五、spec repo

1、cocoaPod 拉下来的库的本地位置

1
open ~/.cocoapods/repos

image-20240909145311054

2、拉库

常见:直接Podfile中指定source,然后执行 pod install 即可。

结果为:gitee-dvlproad-dvlproadspecs

也可以自己添加库

1
2
cd ~/.cocoapods/repos
pod repo add dvlproad https://gitee.com/dvlproad/dvlproadSpecs.git

结果为:dvlproad

iOS Xcode 14 创建新项目Pod init及Pod install 报错

in `initialize_from_file’: [Xcodeproj] Unknown object version (56). (RuntimeError)

原因:Xcode与cocoapods不兼容。

解决:修改compatibilityVersion,将Project Format 改为Xcode 13.0-compatible

给 Pod 添加资源文件

1
2
3
4
5
6
# 利用 resources 属性,这些资源文件在 build 时会被直接拷贝到 client target 的 mainBundle 里。这样就实现了把图片、音频、NIB等资源打包进最终应用程序的目的。
s.resources = ['CQImagePickerSampleView/Modules/HealthCer/NDM/SimulateApiJSON/*']
# 利用 resource_bundles 属性,这些资源文件在 build 时会被拷贝到 指定的 customBundle 里。这样就实现了把图片、音频、NIB等资源打包进最终应用程序的目的。
s.resource_bundles = {
'CQImagePickerSampleView' => ['CQImagePickerSampleView/**/*.{png,jpg}'] # CQImagePickerSampleView 为生成boudle的名称,可以随便起,但要记住,库里要用
}

End