0%

混编打包遇到环境不在有效范围内,爬坑记录

1
2
3
4
5
6
7
8
ERROR: Unknown FLUTTER_BUILD_MODE: distribution.
Valid values are 'Debug', 'Profile', or 'Release' (case insensitive).
This is controlled by the FLUTTER_BUILD_MODE environment variable.
If that is not set, the CONFIGURATION environment variable is used.

You can fix this by either adding an appropriately named build
configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the
.xcconfig file for the current build configuration (Distribution).

方案一(不推荐)

根据提示的路径找到对应的flutter_export_environment.sh文件和Generated.xcconfig文件,打开添加以下代码:

export "FLUTTER_BUILD_MODE=release"

注:这两个文件是自动生成的,每次生成会覆盖,所以这个方案算是临时方案,协同工作很不方便,极其不推荐

方案二

打开flutter的package地址:xxx/Library/Developer/flutter/packages/flutter_tools/bin/xcode_backend.dart

我们可以看到在选择模式的时候,先查找set中FLUTTER_BUILD_MODE配置,如果没有取CONFIGURATION,可以直接修改package代码,添加判断如下:

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
30
31
32
33
34
35
36
37
38
39
String parseFlutterBuildMode() {
// Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
// This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
// they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
final String? buildMode =
(environment['FLUTTER_BUILD_MODE'] ?? environment['CONFIGURATION'])
?.toLowerCase();

if (buildMode != null) {
// 修改的地方
if (buildMode.contains('release') || buildMode.contains('xxx')) {
return 'release';
}
if (buildMode.contains('profile')) {
return 'profile';
}
if (buildMode.contains('debug')) {
return 'debug';
}
}
echoError(
'========================================================================');
echoError('ERROR: Unknown FLUTTER_BUILD_MODE: $buildMode.');
echoError(
"Valid values are 'Debug', 'Profile', or 'Release' (case insensitive).");
echoError(
'This is controlled by the FLUTTER_BUILD_MODE environment variable.');
echoError(
'If that is not set, the CONFIGURATION environment variable is used.');
echoError('');
echoError('You can fix this by either adding an appropriately named build');
echoError(
'configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the');
echoError(
'.xcconfig file for the current build configuration (${environment['CONFIGURATION']}).');
echoError(
'========================================================================');
exitApp(-1);
}

方案三(推荐★★★)

在方案二实施过程中,我们发现这个选择mode过程,会优先选择FLUTTER_BUILD_MODE,这个配置默认是没有的,方案一强制性设置为release会导致其他环境运行出错。
我们可以添加自定义配置项FLUTTER_BUILD_MODE,将你的环境指定对应的mode

flutter数据共享系列——随记

Provider

InheritedWidget 解决了数据共享问题。迎面也带来数据刷新导致的组件不必要更新问题。Provider基于InheritedWidget实现数据共享,数据更新,定向通知组件更新等。

接下来我们先从Provider使用开始切入,逐步分析Provider的实现,以及对组件的应用进行熟悉。

就拿官方文档开始:

新建一个模型Counter

1
2
3
4
5
6
7
8
9
10
class Counter with ChangeNotifier {
int _count = 0;

int get count => _count;

void increment() {
_count++;
notifyListeners();
}
}

在合适的位置初始化

这里我们选择main方法:

1
2
3
4
5
6
7
8
9
10
11
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}

使用并修改数据

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}

class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('You have pushed the button this many times:'),

Extracted as a separate widget for performance optimization.
As a separate widget, it will rebuild independently from [MyHomePage].

This is totally optional (and rarely needed).
Similarly, we could also use [Consumer] or [Selector].
Count(),
],
),
),
floatingActionButton: FloatingActionButton(
key: const Key('increment_floatingActionButton'),

Calls `context.read` instead of `context.watch` so that it does not rebuild
when [Counter] changes.
onPressed: () => context.read<Counter>().increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

class Count extends StatelessWidget {
const Count({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Text(

Calls `context.watch` to make [Count] rebuild when [Counter] changes.
'${context.watch<Counter>().count}',
key: const Key('counterState'),
style: Theme.of(context).textTheme.headline4);
}
}

使用注意事项

1. 共享数据定义为私有属性,提供get方法和update方法

这样可以有效的保护数据结构,统一修改入口和获取方法。

2. 适当隔离会进行rebuild的组件,常用的方式有三种:

  • 单独封装组件
  • 通过Consumer包裹
  • 通过Selector包裹,selector可以在某些值不变的情况下,防止rebuild。常用的地方是针对列表中个别数据进行修改。

3. 区分watchread的使用

watchread是Provider框架内部对BuildContext的扩展类。用户获取父级组件指定数据入口。区别在于是否添加了linsten,这个关系到是否需要实时刷新。
简单区分两种场景:

  • watch:界面监听数据,更新页面
  • read:响应业务交互,去操作更新数据

两种方法的源码也很简单,只是为了方便生成的拓展类:

1
2
3
4
5
6
7
8
9
10
11
12
13
 Exposes the [read] method.
extension ReadContext on BuildContext {
T read<T>() {
return Provider.of<T>(this, listen: false);
}
}

Exposes the [watch] method.
extension WatchContext on BuildContext {
T watch<T>() {
return Provider.of<T>(this);
}
}

这里我们来看看Provider.of源码:

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
30
31
32
33
34
  static T of<T>(BuildContext context, {bool listen = true}) {
// 移出部分不必要代码

final inheritedElement = _inheritedElementOf<T>(context);

if (listen) {
context.dependOnInheritedElement(inheritedElement);
}
return inheritedElement.value;
}

static _InheritedProviderScopeElement<T> _inheritedElementOf<T>(
BuildContext context,
) {
// 移出部分不必要代码
_InheritedProviderScopeElement<T>? inheritedElement;

if (context.widget is _InheritedProviderScope<T>) {
context.visitAncestorElements((parent) {
inheritedElement = parent.getElementForInheritedWidgetOfExactType<
_InheritedProviderScope<T>>() as _InheritedProviderScopeElement<T>?;
return false;
});
} else {
inheritedElement = context.getElementForInheritedWidgetOfExactType<
_InheritedProviderScope<T>>() as _InheritedProviderScopeElement<T>?;
}

if (inheritedElement == null) {
throw ProviderNotFoundException(T, context.widget.runtimeType);
}

return inheritedElement!;
}

原来加不加listen的区别在于获取数据的方式是getElementForInheritedWidgetOfExactTypeordependOnInheritedElementdependOnInheritedElement会新增一个注册,这个注册会调用在数据变更后,调用消费者的didChangeDependencies 。稍微具体点分析可以查看我之前的文章——
记InheritedWidget使用思考

ChangeNotifier

实现Listenable接口的一个简单类,官方给的说明很简单:

A class that can be extended or mixed in that provides a change notification

可以扩展或混合的类,提供更改通知

实现了算法复杂度为O(1)去添加监听,O(N)去移除监听。对数据更新高效通知页面去刷新。provider的数据模型均得继承与它。

ChangeNotifierProvider

有了数据模型,接下来就开始创建我们的ChangeNotifier,就要用到ChangeNotifierProvider

先说一个错误的示例,错误的示例,错误的示例,在build中通过ChangeNotifierProvider.value去创建:

1
2
3
4
ChangeNotifierProvider.value(
value: new MyChangeNotifier(),
child: ...
)

这样会造成内存泄露和潜在的bug——参考

当然,这个方法存在肯定是有他的意义的——如果你已经有了ChangeNotifier实例,就可以通过ChangeNotifierProvider.value进行构造。而不是选用create

正确的方法是通过creat方法去构建:

1
2
3
4
ChangeNotifierProvider(
create: (_) => new MyChangeNotifier(),
child: ...
)

不要传入变量去构建ChangeNotifier,这样的话,当变量更新,ChangeNotifier是不会去更新的。

1
2
3
4
5
6
int count;

ChangeNotifierProvider(
create: (_) => new MyChangeNotifier(count),
child: ...
)

如果你确实需要传入变量,请使用ChangeNotifierProxyProvider。具体ChangeNotifierProxyProvider使用,这里就不探讨了。

记InheritedWidget使用思考

InheritedWidget 是项目中必不可少的组件,用户数据共享。老生常谈的Provider框架也是基于InheritedWidget实现的

简介

InheritedWidget组件是功能性组局,实现了由上向下共享数据的功能。即子组件通过BuildContext.dependOnInheritedWidgetOfExactType方法从父组件获取数据。

值得提一下,这种由上向下提供书共享数据的方式和Notification传递方向正好相反。两者相同点是:都是由子组件发起的。InheritedWidget是又子组件通过content向树上方查找数据,Notification是由子组件向上发起通知。

特点

  1. 子组件通过[BuildContext.dependOnInheritedWidgetOfExactType]方法查找对应最近的InheritedWidget并获取数据。
  2. InheritedWidget根据习惯或者协议,提供of静态方法,便于子组件获取数据。该方法并非一定返回该继承的widget,也可以返回其他数据。
  3. 调用of方法的组件,必须要在InheritedWidget内。如果在同一个widget中使用,可以通过Builder将需要使用的组件包裹起来。

作用

在使用中我们发现,InheritedWidget作用是父组件提供数据,子组件可以由下往上查找对应的共享数据,这个是它直观的作用。

常用的错误更新数据方式

**setState**: 我们发现网上的有很多文章在介绍InheritedWidget的使用,并更新数据。但是这个只是体现了该组件的作用。绝大多数是在InheritedWidget组件中通过final申明共享数据。然后在父组件中通过setState强制刷新父组件,同时也刷新了InheritedWidget组件,当然其多层子组件都会跟着一起rebuild,造成了很大的非必要消耗。

如何去优化?

InheritedWidget提供了数据的共享,避免了数据由上到下层层申明传递。我们使用数据的地方也很明确。接下来要做的是,数据刷新后,让指定组件去更新。这个时候我们就想到了ValueNotifier:**InheritedWidget在共享数据的时候通过ValueNotifier包裹,子组件通过of方法获取的数据类型也是ValueNotifier,在使用的地方通过ValueListenableBuilder实现**。

进一步思考

【老孟Flutter】源码分析系列之InheritedWidget一文中提到了,修改of方法:

  • dependOnInheritedWidgetOfExactType

  • getElementForInheritedWidgetOfExactType().widget

    两者的区别:调用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会,所以在调用dependOnInheritedWidgetOfExactType()时,InheritedWidget和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的didChangeDependencies()方法和build()方法。而当调用的是 getElementForInheritedWidgetOfExactType()时,由于没有注册依赖关系,所以之后当InheritedWidget发生变化时,就不会更新相应的子孙Widget。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @override
    T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
    {Object? aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor =
    _inheritedWidgets == null ? null : _inheritedWidgets![T];
    // 多出部分
    if (ancestor != null) {
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
    }

    @override
    InheritedElement?
    getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor =
    _inheritedWidgets == null ? null : _inheritedWidgets![T];
    return ancestor;
    }

很明显修改为getElementForInheritedWidgetOfExactType<T>().widget,就会少一个非必要注册,也算是好事。

进一步思考产生疑问

既然通过了ValueNotifierInheritedWidget结合,我们通过修改ValueNotifier的value来改变页面时,InheritedWidget组件没有重新构建并不会触发updateShouldNotify,也就是在使用数据的子组件中didChangeDependencies方法也不会因为value的改变而调用。
所以of方法具体用什么去实现,取决于具体业务设计。

文中的图片均借用于【老孟Flutter】源码分析系列之InheritedWidget

Welcome to my blog, enter password to read.
Read more »

Flutter实现自旋转的伪3D球

一个自动旋转的仿3D球体。基于https://github.com/al4fun/3DBall,进行二次封装。

特性

  • 支持手动/自动转动
  • 支持暂停/继续转动控制
  • 支持高亮处理部分标签

思路

  • 建立了一个虚拟的3d坐标系:坐标原点位于球心,Z轴垂直于屏幕并指向屏幕外。根据球面方程将需要绘制的点均匀散布到整个球面上。
  • 由点坐标可以得到点在XY平面上的投影位置;根据点到球体最前方垂直切面的距离,计算点的尺寸和颜色值(距离越远,尺寸越小,颜色越暗淡)。
  • 由球体旋转的方向和位移可以得到球体的旋转轴矢量和旋转角,进而可由罗德里格旋转公式计算出旋转后点的坐标。

Getting Started

1
2
3
4
5
6
7
8
9
10
RBallView(
isAnimate: provider.curIndex == 0,
isShowDecoration: false,
mediaQueryData: MediaQuery.of(context),
keywords: snapshot.data,
highlight: [snapshot.data[0]],
onTapRBallTagCallback: (RBallTagData data) {
print('点击回调:${data.tag}');
},
),

源码和Demo地址

https://github.com/rzrobert/RBallView

Here's something encrypted, password is required to continue reading.
Read more »

Here's something encrypted, password is required to continue reading.
Read more »

客户端需要不定期更新token,同时得保证在发送更新token同时其他网络请求挂起,否则会报token失效。待新token回来之后,重新发起挂起的请求。

应服务端要求,更新token请求时,老token立刻失效

Read more »

对于React Native的热更新,之前一直用code-push,这个API比较丰富,统计功能也比较全。但服务器在国外,对于国内来说网络不占优势。
在之前工作中,发现安卓还有热更新失败的情况。

对于拆包需求来说,现在只能说有多种方案,携程的方案比较强大,但他们的CRN并不开源。调研过一段时间,目前比较方便的还是通过Google的diff工具进行拆包。

实施方案

React Native打包之后会生成bundle文件,拆分方案是基于本次打包与前一次打包之间的差异完成的。即前一次打出来的bundle(lastbundle)作为base,本次打包出来的bundle(newbundle)与lastbundle之间的差异作为diffbundle。这样我们可以把一个最新的完整的bundle拆成两部分(如图):

Read more »