0%

需求:

  1. 客户端表现,写完即销毁的 Notes
  2. 发送到特定的另一方查阅

大致实现:

  1. 用户名称双方绑定,储存本地
  2. 笔记本地暂存,提交后销毁
  3. 仅拉取另一方的信息

私以为这样一个仅双方交流的笔记软件用不着专门提供后端(其实是因为没有现有服务器),自行解决办法可以跨越物理距离面对面快传,甚至走 p2p 形式。最终选择使用某些公有服务。

客户端方面一开始打算使用iOS原生,cloudkit+sharing提供后端,双方共享储存。但由于 apple 系原生软件不允许私人使用(自签要给每个人手机开开发者,还只有7天),除了企业认证、tf 内测等歪门邪道,以上还需持有每年688的 appledev 会员。

于是考虑使用 flutter + web 代替,尽量完整 apple 系原生体验,后端使用 github issues api,参考评论区系统 https://jw1.dev/2022/10/23/a01/


Update

以上大部分作废,github oauth app还是需要回传 token,静态 pages 无法实现。

于是借用了一位爹的服务器打算自己造一下储存,典中典之 gin+gorm 后端。

之后又想了下用狗狗公共服务懒得自己搭数据库,使用 firebase database + messaging + google oauth + 爹的服务器上的pushNotification服务作为后端,flutter 作 web 前端。

最近在做一个相关的需求,需要判断当前的输入法是否处于拼写态,以及当前的输入法的布局。

拼写态即用户正在拼写还未形成最终的字、词的状态,例如在中文全键上 typing 拼音,输入法上还待选中文但输入框显示字母;又比如英文全键输入字母还未成单词时,候选框待选。 这个词感觉很难用中文解释,用中文关键字 google 几乎不会得到结果。

键盘布局(KeyboardLayout),例中文全键、中文九宫格、英文全键、emoji等。

Read more »

某Flutter应用发热解决过程

某版本改动过后,发热量徒增(尤其是iOS的弱散热平台),遂开始溯源。

先CPU Profiler开profile看实时cpu消耗,注意到键盘升起时报Low Memory Warning,同理键盘收取也是。并且CPU Usage飙升,并且只要键盘存在CPU Usage就维持在较高水平。

在活跃约1分钟后Thermal State变为Fair,一分半变为Serious。发热确实很严重。

初步判断flutter Textfield在iOS平台有问题,去flutter issue寻找,果真找到了#128197

于是将flutter Textfield替换成iOS原生UITextView的实现,发热解决了。

但是CPU高占用和Low Memory Warning还是未解决。

Low Memory Warning可能是大量新创建的对象Object,即可能是大量的Widget被重新创建导致的build。

rebuild范围大主要原因就是监听了不必要的dependency,比如MediaQuery.of会监听所有MediaQueryData,context.watch会监听所有来自ChangeNotifier的变化。所以应该改成如MediaQuery.paddingOf和context.select的形式,并且以抽Widget代替function的方式,可以有效减小rebuild范围。

Dart里的Mixin

一开始以为只是简单的代码混入,感觉还蛮好用。

但是多个Mixin如果有相同的函数或变量混入了如何解决?

Mixins in Dart work by creating a new class that layers the implementation of
the mixin on top of a superclass to create a new class - it is not “on the side “ but “on top” of the superclass, so there is no ambiguity in how to resolve lookups.

- Lasse R. H. Nielsen on StackOverflow.

以下的代码,实际上继承链为Disposable->A->B->AB。

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
abstract class Disposable {
void dispose() {
print('Disposable');
}
}

mixin A on Disposable {
@override
void dispose() {
super.dispose();
print('A');
}
}

mixin B on Disposable {
@override
void dispose() {
super.dispose();
print('B');
}
}

final class AB extends Disposable with A,B {
@override
void dispose() {
super.dispose();
print('AB');
}
}


void main() {
AB().dispose();
}

实现ModalBottomSheet

定义ModalBottomSheet可滑动关闭底部弹出Route,并且可以定义上一Route的Transition动画,这个动画是跟手的。

iOS13中新出现了一种弹出Route,是堆叠形式的底部弹出Route,暂且将其命名为StackModalPopup

所以StackModalPopupModalBottomSheet的子集,只是自定义了动画。

Read more »

不要靠近Flutter的CupertinoContextMenu

已存在的issue

跟原生完全没得比,child大小适配没有做https://github.com/flutter/flutter/issues/58880

child大小总是占屏幕的一半

actions弹出位置总是固定,一旦action多了就会超出屏幕,没有办法点击,也没有缩小或者滑动的策略让用户点击https://github.com/flutter/flutter/issues/55025

我使用过程发现的issue

#1

flutter在弹出ctxMenu的策略是先放置overlay,在推入路由。

一旦这个overlay里面有跟其他组件交互的方法(在Widgets树中向上查找其他组件),而WidgetApp中Overlay处于较上层的位置,所以他所能找到的组件是有限的。

举个例子,Overlay在WidgetApp中是位于Navigator上层的,也就是你用Overlay下的context不可能调用Navigator.push等方法,因为在Overlay下context下找不到Navigator,此时会抛出

Navigator operation requested with a context that does not include a Navigator. The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.

所以如果想制作某个依赖上层dependency的overlay,最好做法是传context进去。

#2 CupertinoContextMenu中可触发多手势

如果在长按触发CupertinoContextMenu过程中点击其他可响应事件的地方,这时候两个手势会在分别的GestureArena中不互相竞争,导致两个都可能触发。

比如在长按一张照片1时点击另一张照片2,这时候不仅会出现照片1的ContextMenuRoute,还会出现照片2的的照片详情页。但是在iOS中原生里,点击其他地方会取消打开这个照片1的ContextMenuRoute,而不是去响应这个点击手势。

#3 [CupertinoContextMenu] Potential unremoved overlay entry

Details

According to https://github.com/flutter/flutter/blob/0ff68b8c610d54dd88585d0f97531208988f80b3/packages/flutter/lib/src/cupertino/context_menu.dart#L560C1-L583C1

_lastOverlayEntry is only removed when animation is dismissed or completed.

What if the overlayEntry is inserted, and then the CupertinoContextMenu is disposed and removed from widgets tree? So the entry will never be removed.

Temporary solution is add _lastOverlayEntry?.remove() to

https://github.com/flutter/flutter/blob/0ff68b8c610d54dd88585d0f97531208988f80b3/packages/flutter/lib/src/cupertino/context_menu.dart#L683C1-L687C4

But this is not enough, because there will be no animation, and it will be abrupt for it to suddenly disappear.

Steps to reproduce

  1. Add a CupertinoContextMenu to any visible widget.
  2. Press it to insert the overlayEntry (the _DecoyChild) , while remove the CupertinoContextMenu from the widgets tree.
  3. The entry never dismissed.

issue在https://github.com/flutter/flutter/issues/131471

Dart里坑人的extension

先随便去哪里run下面的代码(图简便直接跑DartPad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void main() {
final x = _111();
print('methods inside class');
for (int i = 0; i < 5; i++) {
print(x.helloworld.hashCode);
}
print('methods inside extension of class');
for (int i = 0; i < 5; i++) {
print(x.helloworldOnExtension.hashCode);
}
}

class _111 {
void helloworld() {}
}

extension on _111 {
void helloworldOnExtension() {}
}

看输出你发现了什么?

  1. methods inside class的地址(hashcode)是不会变的
  2. methods inside extension of class的地址是会变的,这说明dart里的extension中方法的实现机制可能是:在引用时创建一个匿名函数,临时分配地址

为什么坑人

如果想用extension做代码拆分的任务(虽然也有可能是这个初衷就存在问题,本来就不应该用extension做代码拆分),往extension里写入了一些你本来认为是「静态函数」的函数,认为引用的地址不会变。

比如

1
2
3
4
somePublisher.addListener(helloworldOnExtension)

// then
somePublisher.removeListener(helloworldOnExtension) // doesn't work

此时removeListener不会起作用,因为此时的helloworldOnExtension又是一个新的地址。

结论

少用extension(

关于dart里面的方法调度

如果将最开始的代码第二行改为

1
final dynamic x = _111();

运行时会抛出error:

1
Uncaught TypeError: x.get$helloworldOnExtension is not a functionError: TypeError: x.get$helloworldOnExtension is not a function

这也侧面印证了dart里extension函数是动态生成的,并不存在于函数表里。

来到dart类里面的函数、变量查找机制。

即使是dynamic类型只要调用的方法名存在与该类,dart就能找到并调用对应的方法。

这说明dart可能用的是和OC一样的消息传递调用方法的机制,如上也可以看出来,通过将helloworldOnExtension发送给x.get来完成方法调用。