注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Java好用的时间类,别在用Date了

前言假设你想获取当前时间,那么你肯定看过这样的代码public static void main(String[] args) { Date date = new Date(System.currentTimeMillis()); Syste...
继续阅读 »

前言

假设你想获取当前时间,那么你肯定看过这样的代码

public static void main(String[] args) {

Date date = new Date(System.currentTimeMillis());
System.out.println(date.getYear());
System.out.println(date.getMonth());
System.out.println(date.getDate());
}

获取年份,获取月份,获取..日期?
运行一下

121
9
27

怎么回事?获取年份,日期怎么都不对,点开源码发现

/**
* Returns a value that is the result of subtracting 1900 from the
* year that contains or begins with the instant in time represented
* by this <code>Date</code> object, as interpreted in the local
* time zone.
*
* @return the year represented by this date, minus 1900.
* @see java.util.Calendar
* @deprecated As of JDK version 1.1,
* replaced by <code>Calendar.get(Calendar.YEAR) - 1900</code>.
*/
@Deprecated
public int getYear() {
return normalize().getYear() - 1900;
}

原来是某个对象值 减去了 1900,注释也表示,返回值减去了1900,难道我们每次获取年份需要在 加上1900?注释也说明了让我们 用Calendar.get()替换,并且该方法已经被废弃了。点开getMonth()也是一样,返回了一个0到11的值。getDate()获取日期?不应该是getDay()吗?老外的day都是sunday、monday,getDate()才是获取日期。再注意到这些api都是在1.1的时候被废弃了,私以为是为了消除getYear减去1900等这些歧义。收~

Calendar 日历类

public static void main(String[] args) {

Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int dom = calendar.get(Calendar.DAY_OF_MONTH);
int doy = calendar.get(Calendar.DAY_OF_YEAR);
int dow = calendar.get(Calendar.DAY_OF_WEEK);
int dowim = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
System.out.println(year+"年"+ month+"月");
System.out.println(dom+"日");
System.out.println(doy+"日");
System.out.println(dow+"日");
System.out.println(dowim);
}

打印(运行时间2021年10月27日 星期三 晴)

2021年9月
27日
300日
4日
4

问:月份怎么是上个月的?
答:是为了计算方便,月是0到11之间的值。
问:计算方便?
答:比如月份从1月开始,增加一个月,12月+1=13,没有13月。假设取余,(12+1)=1 正好为1月,那11月增加一个月,(11+1)=0,这就有问题了。所以为了计算方便1月,返回了0值。date.getMonth()也是一个道理。 问:那下面的DAY_OF_XXX 又是什么意思?
答:猜!根据结果猜。
Calendar.DAY_OF_MONTH 在这个月 的这一天
Calendar.DAY_OF_YEAR 在这一年 的这一天
Calendar.DAY_OF_WEEK 在这一周 的这一天
Calendar.DAY_OF_WEEK_IN_MONTH 在这一个月 这一天在 第几周
到这里 Calendar.DAY_OF_WEEK 为什么是 4 ,你肯定也猜到了
Calendar.HOUR
Calendar.HOUR_OF_DAY
Calendar.SECOND
...其他的 你肯定也会用了

LocalDate 本地日期类

LocalDate localDate = LocalDate.now();
System.out.println("当前日期:"+localDate.getYear()+" 年 "+localDate.getMonthValue()+" 月 "+localDate.getDayOfMonth()+"日" );

//结果
当前日期:2021 年 10 月 27日

也可以通过 LocalDate.of(年,月,日)去构造

LocalDate pluslocalDate = localDate.plusDays(1);//增加一天
LocalDate pluslocalDate = localDate.plusYears(1);//增加一年

其他api

LocalDate.isBefore(LocalDate);
LocalDate.isAfter();
LocalDate.isEqual();

也就是对两个日期的判断,是在前、在后、或者相等。

LocalTime 本地时间类

LocalTime localTime = LocalTime.now();
System.out.println("当前时间:"+localTime.getHour()+"h "+localTime.getSecond()+"m "+localTime.getMinute()+"s" );

LocalDate和LocalTime 都有类似作用的api
LocalDate.plusDays(1) 增加一天
LocalTime.plusHours(1) 增加一小时 等等~
其他api

LocalTime.isBefore(LocalTime);
LocalTime.isAfter();

对两个时间的判断。肯定碰到过一个需求,今天离活动开始时间还剩多少天。

LocalDateTime 本地日期时间类

public final class LocalDateTime ...{

private final LocalDate date;

private final LocalTime time;
}

LocalDateTime = LocalDate + LocalTime 懂的都懂

Instant 类

Instant 是瞬间,某一时刻的意思

Instant.ofEpochMilli(System.currentTimeMillis())
Instant.now()

通过Instant可以创建一个 “瞬间” 对象,ofEpochMilli()可以接受某一个“瞬间”,比如当前时间,或者是过去、将来的一个时间。
比如,通过一个“瞬间”创建一个LocalDateTime对象

LocalDateTime now = LocalDateTime.ofInstant(
Instant.ofEpochMilli(System.currentTimeMillis()),ZoneId.systemDefault());

System.out.println("当前日期:"+now.getYear()+" 年 "+now.getMonthValue()+" 月 "+now.getDayOfMonth()+"日" )

Period 类

Period 是 时期,一段时间 的意思
Period有个between方法专门比较两个 日期 的

LocalDate startDate = LocalDateTime.ofInstant(
Instant.ofEpochMilli(1601175465000L), ZoneId.systemDefault()).toLocalDate();//1601175465000是2020-9-27 10:57:45
Period p = Period.between(startDate, LocalDate.now());

System.out.println("目标日期距离今天的时间差:"+p.getYears()+" 年 "+p.getMonths()+" 个月 "+p.getDays()+" 天" );

//目标日期距离今天的时间差:1 年 1 个月 1 天

看一眼源码

public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive) {
return startDateInclusive.until(endDateExclusive);
}

public Period until(ChronoLocalDate endDateExclusive) {
LocalDate end = LocalDate.from(endDateExclusive);
long totalMonths = end.getProlepticMonth() - this.getProlepticMonth(); // safe
int days = end.day - this.day;
if (totalMonths > 0 && days < 0) {
totalMonths--;
LocalDate calcDate = this.plusMonths(totalMonths);
days = (int) (end.toEpochDay() - calcDate.toEpochDay()); // safe
} else if (totalMonths < 0 && days > 0) {
totalMonths++;
days -= end.lengthOfMonth();
}
long years = totalMonths / 12; // safe
int months = (int) (totalMonths % 12); // safe
return Period.of(Math.toIntExact(years), months, days);
}

他只接受两个LocalDate对象,对时间的计算,算好之后返回Period对象

Duration 类

Duration 是 期间 持续时间 的意思 上代码

LocalDateTime end = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault());
LocalDateTime start = LocalDateTime.ofInstant(Instant.ofEpochMilli(1601175465000L), ZoneId.systemDefault());
Duration duration = Duration.between(start, end);

System.out.println("开始时间到结束时间,持续了"+duration.toDays()+"天");
System.out.println("开始时间到结束时间,持续了"+duration.toHours()+"小时");
System.out.println("开始时间到结束时间,持续了"+duration.toMillis()/1000+"秒");

可以看到between也接受两个参数,LocalDateTime对象,源码是对两个时间的计算,并返回对象。

对象转换

再贴点api

//long -> LocalDateTime
LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault())

//String -> LocalDateTime
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime.parse("2021-10-28 00:00:00", dateTimeFormatter1);

//LocalDateTime -> long
LocalDateTime对象.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();

//LocalDateTime -> String
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime对象.format(dateTimeFormatter1)

对象转换几乎都涵盖了,里面有个时区对象,这个一般用默认时区。

总结

用LocalDate、LocalTime、LocalDateTime代替了Date类。Date管日期,Time管时间
LocalDateTime = LocalDate + LocalTime
Period 只能用LocalDate
Duration 持续时间,所以LocalDate、LocalTime、LocalDateTime 都能处理
至于Calendar 日历类,这里面的api,都是针对日历的,比如这个月的第一天是星期几。
总体来说,都是api的使用,非常清晰,废弃date.getMonth()等,使用localDate.getMonthValue()来获取几月,更易理解,更易贴合使用。代码都贴在了github上了


作者:回眸婉约
链接:https://juejin.cn/post/7024389549652443172
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Flutter 与原生通信的三种方式

Flutter 与原生之间的通信依赖灵活的消息传递方式 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)应用(原生应用) 宿主监听平台通道,并接收该消息。然后它会调用该...
继续阅读 »

Flutter 与原生之间的通信依赖灵活的消息传递方式




  • 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)应用(原生应用)




  • 宿主监听平台通道,并接收该消息。然后它会调用该平台的 API,并将响应发送回客户端,即应用程序的 Flutter 部分




Flutter 与原生存在三种交互方式




  • MethodChannel:用于传递方法调用(method invocation)通常用来调用 native 中某个方法




  • BasicMessageChannel:用于传递字符串和半结构化的信息,这个用的比较少




  • EventChannel:用于数据流(event streams)的通信。有监听功能,比如电量变化之后直接推送数据给flutter端




三种 Channel 之间互相独立,各有用途,但它们在设计上却非常相近。每种 Channel 均有三个重要成员变量:




  • name: String类型,代表 Channel 的名字,也是其唯一标识符




  • messager:BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具




  • codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器




具体使用



  • 首先分别创建 Native 工程和 Flutter Module。我这里是以 iOS 端和 Flutter 通信为例,创建完 iOS 工程后,需要通过 CocoaPods 管理 Flutter Module。


截屏2021-11-27 下午3.09.28.png



  • 然后在 iOS 工程里面创建 Podfile ,然后引入 Flutter Module ,具体代码如下:


platform :ios,'11.0'
inhibit_all_warnings!

#flutter module 文件路径
flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'Native_iOS' do

install_all_flutter_pods(flutter_application_path)

end

注意: flutter_application_path 这个是 Flutter 工程的路径,我是原生项目和 Flutter在一个目录下



  • 最后在终端 pod install 一下,看是否能正常引入 Flutter Module。这样就可以在iOS工程里面导入#import <Flutter/Flutter.h>

一、MethodChannel的使用


这里写的代码实现了以下功能


1.实现了点击原生页面的按钮跳转到 Flutter 页面,在 Flutter 点击返回按钮能正常返回原生页面


2.实现在Flutter页面点击当前电量,从原生界面传值到 Flutter 页面


原生端代码


@property (nonatomic, strong)FlutterEngine *flutterEngine;

@property (nonatomic, strong)FlutterViewController *flutterVC;

@property (nonatomic, strong)FlutterMethodChannel *methodChannel;

- (void)viewDidLoad {
    [super viewDidLoad];

   //隐藏了原生的导航栏
    self.navigationController.navigationBarHidden = YES;

    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 80, 80)];
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action: @selector(onBtnClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];

    self.flutterVC = [[FlutterViewController alloc] initWithEngine:self.flutterEngine nibName:nil bundle:nil];
//创建channel
    self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"methodChannel" binaryMessenger:self.flutterVC.binaryMessenger];

}

- (void)onBtnClick {

    //告诉Flutter对应的页面
//Method--方法名称,arguments--参数
    [self.methodChannel invokeMethod:@"EnterFlutter" arguments:@""];

//push进入Flutter页面
    [self.navigationController pushViewController:self.flutterVC animated:YES];

    __weak __typeof(self) weakSelf = self;
//监听Flutter发来的事件
    [self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
//响应从Flutter页面发送来的方法
        if ([call.method isEqualToString:@"exit"]) {
            [weakSelf.flutterVC.navigationController popViewControllerAnimated:YES];
        } else if ([call.method isEqualToString:@"getBatteryLevel"]) {
//传值回Flutter页面
            [weakSelf.methodChannel invokeMethod:@"BatteryLevel" arguments:@"60%"];
        }
    }];
}

//创建引擎,真正在项目中,引擎可以定义为一个单例。这样处理防止在原生里面存在多引擎,是非常占有内存的
- (FlutterEngine *)flutterEngine {
    if (!_flutterEngine) {
        FlutterEngine * engine = [[FlutterEngine alloc] initWithName:@"flutterEngin"];
        if (engine.run) {
            _flutterEngine = engine;
        }
    }
    return _flutterEngine;
}

Flutter 端代码


class _MyHomePageState extends State<MyHomePage> {

String batteryLevel = '0%';
//定义通道
final MethodChannel _methodhannel =
const MethodChannel('com.pages.your/native_get');

@override
void initState() {
super.initState();

//Flutter端监听发送过来的数据
_methodhannel.setMethodCallHandler((call) {
if (call.method == 'EnterFlutter') {
print(call.arguments);
} else if (call.method == 'BatteryLevel') {
batteryLevel = call.arguments;
}
setState(() {});
return Future(() {});
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
ElevatedButton(
onPressed: () {
//发送消息给原生
_methodhannel.invokeListMethod('exit');
},
child: Text('返回'),
),
ElevatedButton(
onPressed: () {
//发送消息给原生
_oneChannel.invokeListMethod('getBatteryLevel');
},
child: Text('当前电量${batteryLevel}'),
),
],
),
),
);
}
}

二、BasicMessageChannel的使用


它是可以双端通信的,Flutter 端可以给 iOS 发送消息,iOS 也可以给 Flutter 发送消息。这段代码实现了在 Flutter 中的 TextField 输入文字,在 iOS 端能及时输出。


原生端代码


需要在上面代码的基础上增加 MessageChannel ,并接收消息和发送消息


@property (nonatomic, strong) FlutterBasicMessageChannel *messageChannel;

self.messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"messgaeChannel" binaryMessenger:self.flutterVC.binaryMessenger];

[self.messageChannel setMessageHandler:^(id _Nullable message, FlutterReply  _Nonnull callback) {

        NSLog(@"收到Flutter的:%@",message);
    }];

Flutter 端代码


//需要创建和iOS端相同名称的通道
final messageChannel =
const BasicMessageChannel("messgaeChannel", StandardMessageCodec());

监听消息


messageChannel.setMessageHandler((message) {
print('收到来自iOS的$message');
return Future(() {});
});

发送消息


messageChannel.send(str);

三、EventChannel的使用


只能是原生发送消息给 Flutter 端,例如监听手机电量变化,网络变化,传感器等。


我这里在原生端实现了一个定时器,每隔一秒发送一个消息给 Flutter 端,模仿这个功能。


原生端代码


记得所在的类要实现这个协议 FlutterStreamHandler


//定义属性
//通道
@property (nonatomic, strong) FlutterEventChannel *eventChannel;
//事件回调
@property (nonatomic, copy) FlutterEventSink events;
//用于计数
@property (nonatomic, assign) NSInteger count;

//初始化通道
self.eventChannel = [FlutterEventChannel eventChannelWithName:@"eventChannel" binaryMessenger:self.flutterVC.binaryMessenger];

[self.eventChannel setStreamHandler:self];

//调用创建定时器
[self createTimer];

//创建定时器
- (void)createTimer {

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector: @selector(timeStart) userInfo:nil repeats:YES];
}

//发送消息
- (void)timeStart{

    self.count += 1;
    NSDictionary *dic = [NSDictionary dictionaryWithObject:@(self.count) forKey:@"count"];
    if (self.events != nil) {
        self.events(dic);
    }
}

//代表通道已经建好,原生端可以发送数据了
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink)eventSink {

    self.events = eventSink;
    return nil;
}

//代表Flutter端不再接收
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {

    self.events = nil;
    return nil;
}

Flutter 端代码


//创建通道
final EventChannel eventChannel = const EventChannel('eventChannel');

//开始监听数据
eventChannel.receiveBroadcastStream().listen((event) {
print(event.toString());
});

以上就是iOS原生和Flutter通信的三种方式,消息传递是异步的,这确保了用户界面在消息传递时不会被挂起。


作者:周糖果
链接:https://juejin.cn/post/7035211019618091045
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

如何开发一款易用的,并且可以扩展的空页面呢?那么今天我将带领大家手把手开发一款可扩展的空页面。

开发前注意事项

1、定义好空页面状态 2、可扩展思想(用抽象或基类替代实体) 3、抽离出空页面的结构

空页面展示

在这里插入图片描述

开始搭建

一、页面分析

空页面需要元素有:

  1. 展示图片
  2. 展示文案
  3. 展示刷新按钮

页面功能点:

  1. 文案可自定义
  2. 图片可自定义
  3. 按钮可隐藏

wiget作用范围:

  1. 可包裹其他widget
  2. 不包裹其他widget

二、定义状态

2.1 几种状态

enum EmptyStatus {
fail, //失败视图
loading, //加载视图
nodata, //没有数据视图
none //没有状态
}

没有状态该空页面就隐藏掉

2.2 空页面刷新回调

abstract class IEmptyRefresh{

void pressedReload();

}

2.3 定义copy类(复用做准备)&定义空接口(抽离要扩展的方法)

abstract class Copyable {
T copy();
}
abstract class IEmpty implements Copyable{
IEmptyRefresh? iEmptyRefresh;
Widget? diyImage; // 自定义图片替换
Widget? diyText;// 自定义文案替换
Widget? image();

Widget? text();

Widget? refresh();
}

2.4 空页面实现类

默认加载中页面

class DefaultLodingPage extends IEmpty{

@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageLoding,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return null;
}

@override
Widget? refresh() => null;

@override
IEmpty copy() {
return DefaultLodingPage()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;
}


}
默认空页面

class DefaultEmptyPage extends IEmpty{

@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageNoData,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: diyImage??Icon(LibEmptyManager.instance.imageNoData,color: AppTheme.instance.imageColor(),size: LibEmptyManager.instance.imageSize,),
);
}

@override
Widget? refresh() => null;

@override
IEmpty copy() {
return DefaultEmptyPage()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;;
}


}
默认网络失效页

class DefaultNetWorkError extends IEmpty {
@override
Widget? text() {
return diyText??Text(
LibEmptyManager.instance.libEmptyPageNetError,
style: TextStyle(fontSize: LibEmptyManager.instance.textSize, color: AppTheme.instance.textColor()),
);
}

@override
Widget? image() {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: diyImage??Icon(LibEmptyManager.instance.imageNetWork,color: AppTheme.instance.imageColor(),size: LibEmptyManager.instance.imageSize,),
);
}

@override
Widget? refresh() {
return Padding(
padding: const EdgeInsets.only(top: 20),
child: Padding(
padding: const EdgeInsets.only(left: 20,right: 20),
child: ElevatedButton(onPressed: () => iEmptyRefresh?.pressedReload(),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(AppTheme.instance.btnBackColor()),
shape: MaterialStateProperty.all(const StadiumBorder()),
)
, child: Text(LibEmptyManager.instance.libRefresh,style: TextStyle(fontSize: LibEmptyManager.instance.libRefreshSize,color: AppTheme.instance.btnTextColor())),),
),
);
}

@override
IEmpty copy() {
return DefaultNetWorkError()
..diyImage = diyImage
..diyText = diyText
..iEmptyRefresh=iEmptyRefresh;;
}
}

2.5 空页面管理类

可进行外部配置


class LibEmptyManager{
IEmpty emptyPage = DefaultEmptyPage();
IEmpty loadingPage = DefaultLodingPage();
IEmpty netWorkError = DefaultNetWorkError();

late LibEmptyConfig libEmptyConfig;

LibEmptyManager._();

static final LibEmptyManager _instance = LibEmptyManager._();

static LibEmptyManager get instance {
return _instance;
}

2.6 核心逻辑

判断状态,并进行类型拷贝,并增加自定义参数

switch(widget.layoutType){
case EmptyStatus.none:
visable = true;
break;
// return widget.child;
case EmptyStatus.fail:
iEmpty = LibEmptyManager.instance.netWorkError.copy()
..diyText = widget.networkText
..diyImage = widget.networkImage
;
break;
case EmptyStatus.nodata:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
break;
case EmptyStatus.loading:
iEmpty = LibEmptyManager.instance.loadingPage;
break;
default:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
}

如果是包裹类型需要stack进行包装

return Stack(
children: [
Offstage(
offstage: !visable,
child: widget.child,
),
Offstage(
offstage: visable,
child: Container(
width: double.infinity,
color: AppTheme.instance.backgroundColor(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _listEmpty(iEmpty),
),
),),
],
);

判断是否有网,有网的话,就刷新,没网的话,就提示


@override
void pressedReload() async
{
bool isConnectNetWork = await isConnected();
if(isConnectNetWork){
widget.refresh.call();
}else{
TipToast.instance.tip(LibLocalizations.getLibString().libNetWorkNoConnect!,tipType: TipType.error);
}
}

// 是否有网
Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}

组装empty


List _listEmpty(IEmpty? iEmpty) {
List tempEmpty = [];
if(iEmpty!=null){
Widget? image = iEmpty.image();
Widget? text = iEmpty.text();
Widget? refresh = iEmpty.refresh();
if(image!=null){
tempEmpty.add(image);
}
if(text!=null){
tempEmpty.add(text);
}
if(refresh!=null){
tempEmpty.add(refresh);
}

}
return tempEmpty;
}

三、空页面widget实现完整代码

class LibEmptyView extends StatefulWidget{
Widget? child;
EmptyStatus layoutType;
VoidCallback refresh;


Widget? networkImage;Widget? networkText;
Widget? emptyImage;Widget? emptyText;

LibEmptyView({Key? key, this.child,required this.refresh,required this.layoutType,this.networkImage,this.networkText, this.emptyImage,this.emptyText}) : super(key: key);

@override
State createState() => _LibEmptyViewState();

}

class _LibEmptyViewState extends State implements IEmptyRefresh{
//控制器

@override
Widget build(BuildContext context) {
IEmpty? iEmpty;
bool visable = false;
switch(widget.layoutType){
case EmptyStatus.none:
visable = true;
break;
case EmptyStatus.fail:
iEmpty = LibEmptyManager.instance.netWorkError.copy()
..diyText = widget.networkText
..diyImage = widget.networkImage
;
break;
case EmptyStatus.nodata:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
break;
case EmptyStatus.loading:
iEmpty = LibEmptyManager.instance.loadingPage;
break;
default:
iEmpty = LibEmptyManager.instance.emptyPage.copy()
..diyText = widget.emptyText
..diyImage = widget.emptyImage
;
}
iEmpty?.iEmptyRefresh = this;



return Stack(
children: [
Offstage(
offstage: !visable,
child: widget.child,
),
Offstage(
offstage: visable,
child: Container(
width: double.infinity,
color: AppTheme.instance.backgroundColor(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _listEmpty(iEmpty),
),
),),
],
);
}

@override
void pressedReload() async{
bool isConnectNetWork = await isConnected();
if(isConnectNetWork){
widget.refresh.call();
}else{
TipToast.instance.tip(LibLocalizations.getLibString().libNetWorkNoConnect!,tipType: TipType.error);
}
}


// 是否有网
Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
}

List _listEmpty(IEmpty? iEmpty) {
List tempEmpty = [];
if(iEmpty!=null){
Widget? image = iEmpty.image();
Widget? text = iEmpty.text();
Widget? refresh = iEmpty.refresh();
if(image!=null){
tempEmpty.add(image);
}
if(text!=null){
tempEmpty.add(text);
}
if(refresh!=null){
tempEmpty.add(refresh);
}

}
return tempEmpty;
}

四、空页面widget使用代码

包裹使用 (代码中的webview封装参见:跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview

LibEmptyView(
layoutType: status,
refresh: () {

status = EmptyStatus.none;
_innerWebPageController.reload();

},

child: InnerWebPage(widget.url,titleCallBack: (title){
setState(() {
urlTitle = title;
});
},javascriptChannels: widget._javascriptChannels,urlIntercept: widget._urlIntercept,onInnerWebPageCreated: (innerWebPageController){
_innerWebPageController = innerWebPageController;
widget._javascriptChannels?.webPageCallBack = webPageCallBack;
widget._urlIntercept?.webPageCallBack = webPageCallBack;
},onWebResourceError: (error){
setState(() {
status = EmptyStatus.fail;
});
},),
),
));

非包裹使用

if(_status == EmptyStatus.none){
return _listViewUi.call(_allReportItems);
}else{
var empty = LibEmptyView(
layoutType: _status,
refresh: () {
_status = EmptyStatus.loading;
LibLoading.show();
_refreshCenter.refreshData();
},networkImage: networkImage,networkText: networkText,emptyImage: emptyImage,emptyText: emptyText,);
if(builder!=null){
return builder.call(context,empty);
}else{
return empty;
}
}

感谢大家阅读我的文章

收起阅读 »

Flutter桌面端开发——复制和粘贴内容

复制和粘贴这个功能,一般系统都自带,简单的按几个键就能完成。但是有时候我们想要自己在应用中集成这个功能,或者想在用户复制文字后不使用粘贴操作,就让复制的内容直接出现在我们的应用中。想要实现该功能,就可以用我今天介绍的几个插件。 screen_capturer ...
继续阅读 »

复制和粘贴这个功能,一般系统都自带,简单的按几个键就能完成。但是有时候我们想要自己在应用中集成这个功能,或者想在用户复制文字后不使用粘贴操作,就让复制的内容直接出现在我们的应用中。想要实现该功能,就可以用我今天介绍的几个插件。


screen_capturer


这个方法是用来截取屏幕的。本来想写一期介绍截屏插件的,但是找了一圈只找到这个适用于桌面端,只写这一个插件篇幅又太短,所以直接加了进来。


安装🛠


点击screen_capturer获取最新版本。以下是在编写本文章时的最新版本:


screen_capturer: ^0.1.0

使用🥟


该插件的主体是 ScreenCapturer.instance ,其下有3个方法:isAccessAllowed 、requestAccess 和 capture。


isAccessAllowed 和 requestAccess仅在 macOS中适用,分别用来检测和申请截图的权限。


await ScreenCapturer.instance.requestAccess();  // 申请权限
bool _isAllowed = await ScreenCapturer.instance.isAccessAllowed (); // 检测是否拥有权限

我们截图的目的是把图片显示出来,所以在正式截图前先定义个图片路径的参数:


String _image;

接下来介绍截图的主要方法 capture,该方法可以传递2个参数:



  • String? imagePath:该属性为必填,传递一个图片保存的路径和名称

  • bool silent:设置是否开启截屏的提示音


String _imageName = '${DateTime.now().millisecondsSinceEpoch}.png';  // 设置图片名称
String _imagePath = 'C:\\Users\\ilgnefz\\Pictures\\$_imageName'; // 设置图片保存的路径
CapturedData? _capturedData = await ScreenCapturer.instance.capture(imagePath: _imagePath);
if (_capturedData != null) {
_image = _capturedData.imagePath;
setState(() {});
} else {
BotToast.showText(text: '截图被取消了');
}

1


通过运行可以发现,这里调用的其实是系统的截图功能,然后将截取的图片进行了保存。和windows自带的截图又有些差别,自带的只会将截图保存到剪切板中。


screen_text_extractor


安装🛠


点击screen_text_extractor获取最新版本。以下是在编写本文章时的最新版本:


screen_text_extractor: ^0.1.0

使用🥟


该插件的主体是 ScreenTextExtractor.instance ,其下有7个方法:



  1. isAccessAllowed:检测是否有进行相关操作的权限,仅macOS

  2. requestAccess:申请进行相关操作的权限,仅macOS

  3. extractFromClipboard:从剪切板提取内容

  4. extractFromScreenSelection:从选择的屏幕提取

  5. simulateCtrlCKeyPress:模拟 Ctrl + c ,返回一个布尔值


前面4个仅macOS使用的方法和screen_capturer一样,这里就不多赘述。后面3个方法将会返回一个 Future 的 ExtractedData 对象。


我们先定义一个String对象用来显示获取到的内容:


String _text = '获取的内容将会在这里🤪';

获取剪切板内容


ExtractedData data = await ScreenTextExtractor.instance.extractFromClipboard();
_text = data.text;
setState((){});

我们先在 windows 中按 windows键 + v 来调出剪切板,清空


无标题


使用该方法看一下:


1


我们将会得到一个空白的内容。为了更好的用户体验,我们可以添加个条件。


if (data.text!.isEmpty) {
BotToast.showText(text: '剪切板什么都没有🤨');
} else {
_text = data.text!;
setState(() {});
}

我们现在复制一段内容:


无标题


然后看看效果:


2


获取成功😀,但是剪切板除了能存储文本,还是能存储图片的。


1


但是ExtractedData只有个text属性,我们来看下会发生什么:


3


直接为空了😶


获取选区内容


ExtractedData data = await ScreenTextExtractor.instance.extractFromScreenSelection(
useAccessibilityAPIFirst: false, // 使用辅助功能API,仅macOS
);
if (data.text!.isEmpty) {
BotToast.showText(text: '剪切板什么都没有🤨');
} else {
_text = data.text!;
setState(() {});
}

👻该方法如果是在windows端,返回的就是extractFromClipboard()方法的结果,在macOS端和Linux端暂时无法演示😪


pasteboard


安装🛠


点击pasteboard获取最新版本。以下是在编写本文章时的最新版本:


pasteboard: ^0.0.2

使用🥟


该插件中的 Pasteboard 对象一共拥有4个方法:



  • image:复制图片

  • file:复制文件/文本

  • writeImage:粘贴图片

  • writeFile:粘贴文件/文本


复制粘贴文本


当然,第一步先定义一个用来存储结果的变量:


String _text = '还没粘贴任何内容';

定义一个文本控制器,用来获取输入的内容:


final TextEditingController _controller = TextEditingController();

接下来使用 pasteboard 来实现复制和粘贴的功能:




  • 复制文本


    void _copyText() async {
    if (_controller.text.isEmpty) {
    BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
    } else {
    final lines = const LineSplitter().convert(_controller.text);
    await Pasteboard.writeFiles(lines);
    }
    }



  • 粘贴文本


    void _pastText() async {
    final results = await Pasteboard.files();
    if (results.isNotEmpty) {
    _text = result.toString();
    setState(() {});
    } else {
    BotToast.showText(text: '我什么都不能给你,因为我也咩有😭');
    }
    }



我们先来试一下,不用复制直接直接粘贴会发生什么。此时我的剪切板有一条内容:


无标题


来看看效果:


1


我们可以发现,它并不能读取我们剪切板的内容。试下复制再粘贴:


2


通过测试可以知道,最终的结果是一个数组。我们再来看看剪切板有没有记录:


无标题


这里其实用的是上面同一张图,因为没有变化所以就没再截图了。


通过以上内容,我们可以发现,pasteboard 的复制粘贴是和系统隔开的。


复制粘贴文件


其实代码可以不用修改,但是为了更好的显示,我们还是修改以下:


void _pastText() async {
final results = await Pasteboard.files();
if (results.isNotEmpty) {
_text = '';
for (final result in results) {
_text += '$result\n';
}
setState(() {});
} else {
BotToast.showText(text: '我什么都不能给你,因为我也咩有😭');
}
}

在这里,我使用了 url_launcher 插件,用来打开系统的文件浏览器。代码如下:


void _openExplorer() async {
const _filePath = r'C:\Users\ilgnefz\Pictures';
final Uri _uri = Uri.file(_filePath);
await launch(_uri.toString());
}

来看看效果:


image


图片本质上也是文件,可以直接使用上面的方法进行复制粘贴。所以关于图片的方法就不讲解了


(🤫ps: 其实是我使用官方例子的方法,用Base64图片进行测试,发现无法得到想要的结果。使用了官方的例子也是一样。复制图片的方法需要传递一个Uint8List参数,虽然可以使用其他方法转换,但是就变得麻烦了。以后我会出一篇关于用 CustomPaint 绘制图片的文章,里面会用到将图片转换成Uint8List对象的方法)。


clipboard


安装🛠


点击clipboard获取最新版本。以下是在编写本文章时的最新版本:


clipboard: ^0.1.3

使用🥟


该插件拥有4个方法:



  • controlC:模仿 cttr + c 键,复制

  • controlC:模仿 cttr + v 键,粘贴

  • copy:复制

  • paste:粘贴


先来看看前面两个方法:


void _useCtrC() async {
if (_controller.text.isEmpty) {
BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
} else {
await FlutterClipboard.controlC(_controller.text);
}
}

void _useCtrV() async {
ClipboardData result = await FlutterClipboard.controlV();
_text = result.text.toString();
setState(() {});
}

使用 controlV 会返回一个 ClipboardData 对象。


4


后面两个方法和前面的唯一不同,就是返回的是一个 String 对象:


void _useCopy() async {
if (_controller.text.isEmpty) {
BotToast.showText(text: '啥都没输入,你要我复制什么🥴');
} else {
await FlutterClipboard.copy(_controller.text);
}
}

void _usePaste() async {
_text = await FlutterClipboard.paste();
setState(() {});
}

5


我们打开系统的剪切板可以发现,以上复制的内容都会被记录。我们试一下不按复制看能不能直接读取剪切板的信息进行粘贴:


6


试试 paste 方法:


7


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。


最后,感谢 leanflutterMixin Network 两个团队还有 samuelezedi 对以上插件的开发和维护😁。本应用代码已上传至 githubgitee,有需要的可以下载下来查看学习。


作者:菠萝橙子丶
链接:https://juejin.cn/post/7076983397691686919
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter好用的轮子推荐03:一套精美实用的内置动画弹窗组件

前言 Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。 本专栏为大家收...
继续阅读 »

前言


Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。


本专栏为大家收集了Github上近70个优秀开源库,后续也将持续更新。希望可以帮助大家提升搬砖效率,同时祝愿Flutter的生态越来越完善🎉🎉。


正文


一、🚀 轮子介绍



  • 名称:awesome_dialog

  • 概述:一个简单易用的内置动画弹窗

  • 出版商:marcos930807@gmail.com

  • 仓库地址:awesomeDialogs

  • 推荐指数: ⭐️⭐️⭐️⭐️

  • 常用指数: ⭐️⭐️⭐️⭐️

  • 效果预览:


gif.gif


二、⚙️ 安装及使用


dependencies:
awesome_dialog: ^2.1.2

import 'package:awesome_dialog/awesome_dialog.dart';

三、🔧 常用属性






















































































































































































属性类型描述
dialogTypeDialogType设置弹窗类型
customHeaderWidget设置自定义标题(如果设置了,DiaologType将被忽略。)
widthdouble弹窗最大宽度
titleString弹窗标题
descString弹窗描述文本
bodyWidget弹窗主体,如果设置了此属性,标题和描述将被忽略。
contextBuildContext@required
btnOkTextString确认按钮的文本
btnOkIconIconData确认按钮的图标
btnOkOnPressFunction确认按钮事件
btnOkColorColor确认按钮颜色
btnOkWidget创建自定义按钮,以上确认按钮相关属性将被忽略
btnCancelTextString取消按钮的文本
btnCancelIconIconData取消按钮的图标
btnCancelOnPressFunction取消按钮事件
btnCancelColorColor取消按钮颜色
btnCancelWidget创建自定义按钮,以上取消按钮相关属性将被忽略
buttonsBorderRadiusBorderRadiusGeometry按钮圆角
dismissOnTouchOutsidebool点击外部消失
onDissmissCallbackFunction弹窗关闭回调
animTypeAnimType动画类型
aligmentAlignmentGeometry弹出方式
useRootNavigatorbool使用根导航控制器而不是当前根导航控制器,可处理跨界面关闭弹窗。
headerAnimationLoopbool标题动画是否循环播放
paddingEdgeInsetsGeometry弹窗内边距
autoHideDuration自动隐藏时间
keyboardAwarebool键盘弹出内容被遮挡时是否跟随移动
dismissOnBackKeyPressbool控制弹窗是否可以通过关闭按钮消失
buttonsBorderRadiusBorderRadiusGeometry按钮圆角
buttonsTextStyleTextStyle按钮文字风格
showCloseIconbool是否显示关闭按钮
closeIconWidget关闭按钮图标
dialogBackgroundColorColor弹窗背景色
borderSideBorderSide整个弹窗形状

四、🗂 示例


1.带有点击动画的按钮


animatedButton-2.gif


AnimatedButton(
color: Colors.cyan,
text: '这是一个带有点击动画的按钮',
pressEvent: () {},
)

2.固定宽度并带有确认 / 取消按钮的提示框


fixedWidthAndButtons.gif


AnimatedButton(
text: '固定宽度并带有确认 / 取消按钮的提示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.INFO_REVERSED,
borderSide: const BorderSide(
color: Colors.green,
width: 2,
),
width: 380,
buttonsBorderRadius: const BorderRadius.all(
Radius.circular(2),
),
btnCancelText: '不予理会',
btnOkText: '冲啊!',
headerAnimationLoop: false,
animType: AnimType.BOTTOMSLIDE,
title: '提示',
desc: '一个1级bug向你发起挑衅,是否迎战?',
showCloseIcon: true,
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

3.自定义按钮样式的问题对话框


questionDialogWithCustomButtons.gif


AnimatedButton(
color: Colors.orange[700],
text: '具有自定义按钮样式的问题对话框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.QUESTION,
headerAnimationLoop: false,
animType: AnimType.BOTTOMSLIDE,
title: '触发额外剧情',
desc: '发现一名晕倒在草丛的路人,你会?',
buttonsTextStyle: const TextStyle(color: Colors.black),
btnCancelText: '拿走他的钱袋',
btnOkText: '救助',
showCloseIcon: true,
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

4.无按钮的信息提示框


noHeaderDialog.gif


AnimatedButton(
color: Colors.grey,
text: '无按钮的信息提示框',
pressEvent: () {
AwesomeDialog(
context: context,
headerAnimationLoop: true,
animType: AnimType.BOTTOMSLIDE,
title: '提示',
desc:
'你救下路人,意外发现他是一位精通Flutter的满级大佬,大佬为了向你表示感谢,赠送你了全套Flutter的学习资料...',
).show();
});

5.警示框


warningDialog.gif


AnimatedButton(
color: Colors.orange,
text: '警示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.WARNING,
headerAnimationLoop: false,
animType: AnimType.TOPSLIDE,
showCloseIcon: true,
closeIcon: const Icon(Icons.close_fullscreen_outlined),
title: '警告',
desc: '意外发现bug的窝点,你准备?',
btnCancelOnPress: () {},
onDissmissCallback: (type) {
debugPrint('Dialog Dissmiss from callback $type');
},
btnCancelText: '暂且撤退',
btnOkText: '发起战斗',
btnOkOnPress: () {},
).show();
});

6.错误提示框


errorDialog.gif


AnimatedButton(
color: Colors.red,
text: '错误提示框',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.ERROR,
animType: AnimType.RIGHSLIDE,
headerAnimationLoop: true,
title: '挑战失败',
desc: '你寡不敌众,败下阵来,(回到出生点后,拿出大佬赠送的全套学习资料,立志学成后报仇血恨... )',
btnOkOnPress: () {},
btnOkIcon: Icons.cancel,
btnOkColor: Colors.red,
).show();
});

7.成功提示框


successDialog.gif


AnimatedButton(
color: Colors.green,
text: '成功提示框',
pressEvent: () {
AwesomeDialog(
context: context,
animType: AnimType.LEFTSLIDE,
headerAnimationLoop: false,
dialogType: DialogType.SUCCES,
showCloseIcon: true,
title: '挑战成功',
desc: '经过三天三夜的苦战,你成功消灭了所有的bug',
btnOkOnPress: () {
debugPrint('OnClcik');
},
btnOkIcon: Icons.check_circle,
onDissmissCallback: (type) {
debugPrint('Dialog Dissmiss from callback $type');
},
).show();
});

8.不带顶部动画的弹窗


noHeaderDialog.gif


AnimatedButton(
color: Colors.cyan,
text: '不带顶部动画的弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
headerAnimationLoop: false,
dialogType: DialogType.NO_HEADER,
title: 'No Header',
desc:'Dialog description here...',
btnOkOnPress: () {
debugPrint('OnClcik');
},
btnOkIcon: Icons.check_circle,
).show();
});

9.自定义内容弹窗


customBodyDialog.gif


AnimatedButton(
color: Colors.purple,
text: '自定义内容弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
animType: AnimType.SCALE,
dialogType: DialogType.INFO,
body: const Center(
child: Text(
'If the body is specified, then title and description will be ignored, this allows to further customize the dialogue.',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
title: 'This is Ignored',
desc: 'This is also Ignored',
).show();
});

10.自动隐藏弹窗


autoHideDialog.gif


AnimatedButton(
color: Colors.grey,
text: '自动隐藏弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
dialogType: DialogType.INFO,
animType: AnimType.SCALE,
title: 'Auto Hide Dialog',
desc: 'AutoHide after 2 seconds',
autoHide: const Duration(seconds: 2),
).show();
});

11.测试弹窗


testingDialog.gif


AnimatedButton(
color: Colors.blue,
text: '测试弹窗',
pressEvent: () {
AwesomeDialog(
context: context,
keyboardAware: true,
dismissOnBackKeyPress: false,
dialogType: DialogType.WARNING,
animType: AnimType.BOTTOMSLIDE,
btnCancelText: "Cancel Order",
btnOkText: "Yes, I will pay",
title: 'Continue to pay?',
desc:'Please confirm that you will pay 3000 INR within 30 mins. Creating orders without paying will create penalty charges, and your account may be disabled.',
btnCancelOnPress: () {},
btnOkOnPress: () {},
).show();
});

12.文本输入弹窗


bodyWithInput.gif


AnimatedButton(
color: Colors.blueGrey,
text: '带有文本输入框的弹窗',
pressEvent: () {
late AwesomeDialog dialog;
dialog = AwesomeDialog(
context: context,
animType: AnimType.SCALE,
dialogType: DialogType.INFO,
keyboardAware: true,
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Text('Form Data',
style: Theme.of(context).textTheme.headline6,),
const SizedBox(height: 10,),
Material(
elevation: 0,
color: Colors.blueGrey.withAlpha(40),
child: TextFormField(
autofocus: true,
minLines: 1,
decoration: const InputDecoration(
border: InputBorder.none,
labelText: 'Title',
prefixIcon: Icon(Icons.text_fields),
),
),
),
const SizedBox(height: 10,),
Material(
elevation: 0,
color: Colors.blueGrey.withAlpha(40),
child: TextFormField(
autofocus: true,
keyboardType: TextInputType.multiline,
minLines: 2,
maxLines: null,
decoration: const InputDecoration(
border: InputBorder.none,
labelText: 'Description',
prefixIcon: Icon(Icons.text_fields),
),
),
),
const SizedBox(height: 10,),
AnimatedButton(
isFixedHeight: false,
text: 'Close',
pressEvent: () {
dialog.dismiss();
},
)
],),
),
)..show();
});

作者:晚夜
链接:https://juejin.cn/post/7076083351668555813
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

从0-1背包问题看动态规划的几种方式

0-1 knapsack Problem Statement Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has ...
继续阅读 »

0-1 knapsack


Problem Statement


Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack that has a capacity ‘C’. The goal is to get the maximum profit from the items in the knapsack. Each item can only be selected once, as we don’t have multiple quantities of any item.


Items: { Apple, Orange, Banana, Melon }
Weights: { 2, 3, 1, 4 }
Profits: { 4, 5, 3, 7 }
Knapsack capacity: 5
Output: 10

暴力解法


每个 Item 都可以放或者不放,可以对每个元素一次进行递归


export function bruteforce(
profits: number[],
weights: number[],
capacity: number
) {
const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;

return Math.max(
c + weights[i] > capacity
? p
: recursive(i + 1, c + weights[i], p + profits[i]),
recursive(i + 1, c, p)
);
};

const ans = recursive(0, 0, 0);
return ans;
}

时间复杂度:O(2n)


空间复杂度:O(n)


Top-Down DP


稍微变一个 recursive 的逻辑,recursive 函数表示,把 0~i 范围内的 Item 装入 capacity 为 c 的背包中,能够获得的最大的 profit


image.png


可以看到有重复的分支,我们需要做的就是在这个基础上记忆


export function topDown(
profits: number[],
weights: number[],
capacity: number
) {
const dp: Record<string, number> = {};

const recursive = (i: number, c: number, p: number): number => {
if (i >= profits.length) return p;

const key = `${i}_${c}`;

if (dp[key]) {
return dp[key];
}

const ans = Math.max(
c - weights[i] >= 0
? recursive(i + 1, c - weights[i], p + profits[i])
: p,
recursive(i + 1, c, p)
);

return (dp[key] = ans);
};

const ans = recursive(0, capacity, 0);
return ans;
}

时间复杂度:O(n * c),因为记忆化后,最多有 n * c 个子问题


空间复杂度:O(n * c + n)


Bottom-Up DP


dp 记录对于前 i 个元素,当 capacity 为 c 时,能获得的最大 profit,那么


dp[i][c] = max (
// 不取当前元素
dp[i-1][c],
// 取当前元素
profits[i] + dp[i-1][c-weights[i]]
)

图解如下:


image.png


export function bottomUp(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[][] = new Array(N)
.fill(0)
.map(() => new Array(capacity + 1).fill(0));

// 初始化
for (let i = 0; i < N; i++) dp[i][0] = 0;
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[0][c] = profits[0];
}

// dp
for (let i = 1; i < N; i++) {
for (let c = 1; c <= capacity; c++) {
dp[i][c] = Math.max(
dp[i - 1][c],
c < weights[i] ? 0 : profits[i] + dp[i - 1][c - weights[i]]
);
}
}

return dp[N - 1][capacity];
}

时间复杂度:O(n * c)


空间复杂度:O(n * c)


Bottom-Up DP 优化


在计算第 i 个元素的过程中,只需要用到前一次的 dp[c] and dp[c-weight[i]] ,所以,在空间复杂度上我们可以做优化,可以使用同一个数组进行前后两次的记忆


如果 capacity 从 c ~ 0 ,而不是 0 ~ c 循环,去修改 dp[i][c ~ capacity],可以确保 dp[i][0 ~ c-1] 的值是前一次的,但是,如果按照之前的反过来,计算 dp[i][c] 的时候,dp[i][0 ~ c-1] 已经变成第 i 次的值在存储,所以行不通


export function bottomUp2(
profits: number[],
weights: number[],
capacity: number
) {
const N = profits.length;
const dp: number[] = new Array(capacity + 1).fill(0);

// 初始化
for (let c = 0; c <= capacity; c++) {
if (weights[0] <= c) dp[c] = profits[0];
}

// dp
for (let i = 1; i < N; i++) {
for (let c = capacity; c >= 0; c--) {
dp[c] = Math.max(
dp[c],
c < weights[i] ? 0 : profits[i] + dp[c - weights[i]]
);
}
}

return dp[capacity];
}

作者:sharp_zhou
链接:https://juejin.cn/post/7076458536250638343
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

经验复盘-力扣刷题给我带来了什么?

起因 虽然在去年跳槽前有刷过一段时间的力扣,但是一直都没有将自己的刷题的过程记录下来。直到2021年5月,掘金举办了一个Java 刷题打卡的活动,我才开始将自己的第一篇刷题文章发布在掘金👇 后面掘金又举办了2021年6月更文挑战活动,活动大奖要求有 30 篇...
继续阅读 »

起因


虽然在去年跳槽前有刷过一段时间的力扣,但是一直都没有将自己的刷题的过程记录下来。直到2021年5月,掘金举办了一个Java 刷题打卡的活动,我才开始将自己的第一篇刷题文章发布在掘金👇


image.png


后面掘金又举办了2021年6月更文挑战活动,活动大奖要求有 30 篇文章,最终这次活动中我输出了 23 篇力扣刷题的文章。


至今为止,我在掘金上已发表了 124 个力扣题解,有兴趣的话可以关注我的专栏:力扣刷题


image.png


我是怎么做的?


1. 代码存储


为了更方便的调试代码以及存储自己写的测试用例,所以我在Github上建了一个私人库用于存储代码。这样做的好处就是:随时随地获取你的历史代码、而且可以保留自己写的测试用例。我非常推荐你也使用仓库来存储自己的解题代码!


image.png


2.模仿和总结


刚开始刷题的时候我只会简单的暴力求解,所以在提交第一题的时候击败率连 10% 都不到,后来通过看官方题解发现了可以使用 HashMap 用空间换取时间(因 HashMap 中链表节点超过8时会转为红黑树)。


所以在刚开始很长一段时间我都是 题目看完 -> 思考15分钟,没有思路 -> 看官方题解,但经过了一段时间后我发现自己的提升很小。就像是一种脑子会了,手没会的感觉,下次碰到类似的题目还是不会做。


后来经过不断的摸索,我刷题的步骤大概是这样的:



  1. 看题目,列出题目中的重点

  2. 想清楚解题的关键和思路(碰到链表或二叉树相关的题目,我一般都会在草稿纸上画一下)

  3. 实在不会的可以看一下官方题解,但是不抄官方的代码

  4. 运行调试:写好代码后,将力扣的测试用例写在main方法中测试(像一些二叉树的题目就需要根据官方的二叉树数组自己转为 TreeNode 了)

  5. 提交代码,然后再在本地调试不通过的测试用例(一般来说需要调试二十分钟左右,不要着急!)

  6. 直到通过所有的测试用例


经过这段时间的刷题,我也有了一些小感悟:



  • 算法的本质就是利用空间换区时间

  • 有时候看到数据结构就能猜到用什么算法:例如碰到 二叉树 就会想到 递归、碰到 截取字符串 会想到 滑动窗口、碰到大问题可以分解的情况会想到 动态规划

  • 使用 HashMap 替代数组或链表,可有效降低时间复杂度

  • 遍历链表时一般都需要临时指针指向头结点

  • ......


我收获了什么?


虽然刷题碰到的一些算法在实际业务中无法很好落地,但是刷题能够对数据结构有更深刻的理解调试能力的提升(先将步骤分解,然后寻找非预期的输入或输出)、锻炼思维能力、以及对代码执行的效率更敏感了(减少时间复杂度)。


实际项目如何运用?



下面我举得这个例子就可以很好的体现:递归算法的运用和提高执行效率。



在常见的业务系统中,不可避免的会需要动态菜单和权限的功能。动态菜单本质上是一个 ,而某个菜单路由下面的权限就像是树中的 叶子节点。但是数据库并不能够存储树这样的结构,那怎么办呢?


常规的树形结构的路由菜单如下所示:


image.png


设计的数据库表如下所示:


parentId:表示父节点的Id  
menuType:当类型为 C 标识为路由下的权限,即二叉树中的叶子节点

image.png


实际业务需要将查找出来的列表转为树形结构返回给前端,这怎么实现呢?大致的思路如下所示:


    /**
* list转为树结构
*/
private List<MenuBO> list2Tree(List<MenuBO> list, Integer pId) {
List<MenuBO> tree = new ArrayList<>();
Iterator<MenuBO> it = list.iterator();
while (it.hasNext()) {
MenuBO m = it.next();
if (m.getParentId() == pId) {
tree.add(m);
// 已添加的元素删除掉
it.remove();
}
}
// 寻找子元素
tree.forEach(n -> n.setChildren(list2Tree(list, n.getId())));
return tree;
}

这段代码的注释比较清楚,就是找到原列表中所有 pid 相同的元素放入新列表,并将新列表设为孩子节点,直到所有节点都遍历完成。又因为一行记录只会成为树中的一个节点,故每个元素只需遍历一次。所以再将元素放入到新列表后,就在原列表中将此元素移除。算法复杂度从 O(N*logN) 变为了 0(N),大大提升了执行效率。


总结


去年的这个时候我是零基础开始刷题的,刚开始写一个中等难度的题目需要一两个小时,再加上写完题目后还要输出一篇文章,一般都要搞到凌晨左右。但现在我简单题已经可以重拳出击了,中等难度的题目解题的时间更少了,至于困难题还是需要看题解。


从我一年多刷题的经历来看,我总结出来两句话:



  1. 万事开头难

  2. 实践才是检验真理的唯一标准!


所以行动起来吧!就现在!



最后我想感谢一下稀土掘金!来这里不仅能刷沸点段子,还能写文章拿奖品。不得不说,掘金的活动实在是太多了(PS:奖品也很多)!


作者:SloppyJack
链接:https://juejin.cn/post/7075727393926676511
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter终极大杀器,一个它顶四个库!

每次新建Flutter项目,是我最痛苦的时间,每次去pub仓库找库,再复制粘贴到 pubspec.yaml ,再执行 flutter pub get 。这套操作往往需要重复十几次。毕竟Flutter大到路由,状态管理,小到工具类,国际化都需要库来支持,等找齐这...
继续阅读 »

每次新建Flutter项目,是我最痛苦的时间,每次去pub仓库找库,再复制粘贴到 pubspec.yaml ,再执行 flutter pub get 。这套操作往往需要重复十几次。毕竟Flutter大到路由,状态管理,小到工具类,国际化都需要库来支持,等找齐这些东西,终于可以准备开发的时候,半天已经过去了。


所幸,我在pub仓库发现了它,GetX,这玩意是真的牛皮,使用它大大小小开发了四五个项目后,确定了稳定性和性能后,决定进行分享一波。


本文只是简单分享,GetX没有官方文档,只有github的README,所以我结合自己的经验,整理了一份。


github


gitee


GetX为何物?




  • GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。




  • GetX 有3个基本原则:



    • 性能: GetX 专注于性能和最小资源消耗。GetX 打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。如果你感兴趣,这里有一个性能测试

    • 效率: GetX 的语法非常简捷,并保持了极高的性能,能极大缩短你的开发时长。

    • 结构: GetX 可以将界面、逻辑、依赖和路由完全解耦,用起来更清爽,逻辑更清晰,代码更容易维护。




  • GetX 并不臃肿,却很轻量。如果你只使用状态管理,只有状态管理模块会被编译,其他没用到的东西都不会被编译到你的代码中。它拥有众多的功能,但这些功能都在独立的容器中,只有在使用后才会启动。




GetX能干什么?


GetX包含的功能:



  • 状态管理

  • 路由

  • 国际化

  • 工具类

  • IDE拓展

  • 工程化Cli

  • ......


GetX的优点?


对于一个菜鸟来说,它最大的优点当然是 简单易上手


举几个例子:


状态管理


创建一个 Controller 管理你的状态变量


class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}

然后直接使用


class Home extends StatelessWidget {

@override
Widget build(context) {

// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
final Controller c = Get.put(Controller());

return Scaffold(
// 使用Obx(()=>每当改变计数时,就更新Text()。
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
body: Center(child: ElevatedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
}

class Other extends StatelessWidget {
// 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
final Controller c = Get.find();

@override
Widget build(context){
// 访问更新后的计数变量
return Scaffold(body: Center(child: Text("${c.count}")));
}
}

你只需要 put 一个 Controller 后,再将 widget 包裹在 Obx 中,这样就将 count 绑定在了你的 widget 中,只要 count 发生改变, widget 就很跟着更新。



注意,Controller是与Widget解耦的,只有进行了put才会进行绑定,所以是局部状态还是全局状态完全由你自己决定。



路由


GetX的路由最大的特点就是,不需要 context ,直接使用即可


导航到新页面


Get.to(NextScreen());

用别名导航到新页面。


Get.toNamed('/details');

要关闭snackbars, dialogs, bottomsheets或任何你通常会用Navigator.pop(context)关闭的东西。


Get.back();

进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)。


Get.off(NextScreen());

进入下一个页面并取消之前的所有路由(在购物车、投票和测试中很有用)。


Get.offAll(NextScreen());

国际化


GetX的国际化尤其简单,我们只需要新建一个 Translations


import 'package:get/get.dart';

class Messages extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'zh_CN': {
'hello': '你好 世界',
},
'de_DE': {
'hello': 'Hallo Welt',
}
};
}

并且将你的 MaterialApp 更改为 GetMaterialApp ,并绑定上刚刚创建的 Translations 类。



不用担心,GetMaterialApp支持所有MaterialApp的接口,它们是兼容的



return GetMaterialApp(
translations: Messages(), // 你的翻译
locale: Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
);

然后直接使用


Text('title'.tr);

是的,你只需要在字符串后面加上 .tr 即可使用国际化功能


GetX Cli是何物?


GetX Cli是一个命令行脚本,类似vue cli,但更强大一些,它可以做到:



  • 创建项目

  • 项目工程化

  • 生成Model

  • 生成page

  • 生成view

  • 生成controller

  • 自定义controller模板

  • 生成翻译文件

  • ......


想要使用GetX Cli,你需要安装dart环境或者Flutter环境


然后直接安装即可使用


pub global activate get_cli 
# or
flutter pub global activate get_cli

创建项目


get create project:my_project

这个命令会调用 flutter create ,然后再执行 get init


项目工程化


get init

生成page


get create page:home

生成controller


get create controller:dialogcontroller on home

生成view


get create view:dialogview on home

生成model


get generate model on home with assets/models/user.json

更多详细命令去看文档


IDE拓展



结语


祝大家在编程路上飞黄腾达,越走越远。


作者:HeStudy
链接:https://juejin.cn/post/6946477749309538334
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter桌面端开发——选择读取本地文件

file_selector 安装🛠 点击file_selector获取最新版本。以下是在编写本文章时的最新版本: file_selector: ^0.8.4+1 👻注意:在开发 macOS 端的程序时,还需要额外的操作,具体可以查看这里 了解🧩 在 file_...
继续阅读 »

file_selector


安装🛠


点击file_selector获取最新版本。以下是在编写本文章时的最新版本:


file_selector: ^0.8.4+1

👻注意:在开发 macOS 端的程序时,还需要额外的操作,具体可以查看这里


了解🧩


在 file_selector 插件中,我们需要了解对象 XTypeGroup 和方法 openFiles。


XTypeGroup的作用是使用给定的标签和文件扩展名创建一个新组,没有提供任何类型选项的组表示允许任何类型。


XTypeGroup 可以传入5个参数:



  • String? label:用来自己作区分

  • List<String>? extensions:过滤非描述的后缀文件,默认加载全部类型的文件

  • List<String>? mimeTypes:主要针对 Linux 系统下的文件类型

  • List<String>? macUTIs:主要针对 mac 系统下的文件类型

  • List<String>? webWildCards:主要针对网页开发时的类型


openFile 方法返回一个 XFile 对象,可以传入3个参数:



  • List acceptedTypeGroups = const []:接受的类型组,传入一个XTypeGroup列表

  • String? initialDirectory:初始化文件夹。打开文件对话框以加载文件并返回文件路径

  • String? confirmButtonText:弹窗“打开”按钮的显示文字


openFiles 方法返回 XFile 对象列表,传入的参数和 openFile 一样。


使用🥩


选择的文件我们最后需要读取出来,读取需要接受一个 String 类型的路径:


String path = '';

这里先以选择图片为例,我们需要先定义一个 XTypeGroup 对象:


final xType = XTypeGroup(label: '图片', extensions: ['jpg', 'png']);

选择单张图片


打开弹窗选取单个文件,使用 openFile 方法:


final XFile? file = await openFile(acceptedTypeGroups: [xType]);

将获取到的 XFile 对象的路径值传给 path。当然,并不是每次打开弹窗都会选择图片,所以需要判断一下:


if (file != null) {
path = file.path;
setState((){});
} else {
BotToast.showText(text: '你不选择图片打开干啥😤');
}

image


openFile 方法中还有两个属性,我们修改试一下:


final XFile? file = await openFile(
acceptedTypeGroups: [xType],
initialDirectory: r'C:\Users\ilgnefz\Pictures',
confirmButtonText: '嘿嘿嘿',
);

image


initialDirectory 属性貌似没用😕去看了官方的例子,也没用到过这个参数,以后就忽略它吧。


选择多张图片


选取多张图片,我们就需要定义一个路径的数组了:


final List<String> paths = []

XTypeGroup 对象和刚才的一样就行,重要的是使用 openFiles 方法:


final List<XFile> files = await openFiles(acceptedTypeGroups: [xType]);

将获取到的文件路径列表赋值给 paths:


if (file != null) {
paths.addAll(files.map((e) => e.path).toList());
setState((){});
} else {
BotToast.showText(text: '你不选择图片打开干啥😤');
}

好了,来看看效果如何


image


读取文本文件


读取文本文件,我们需要获取文件的名称和内容:


final String title = '';
final String content = '';

再更改一下 XTypeGroup 对象就行:


final XTypeGroup xType = XTypeGroup(label: '文本', extensions: ['txt']);
final XFile? file = await openFile(acceptedTypeGroups: [xType]);

将获取到的 XFile 对象的属性赋值给我们定义的对象:


if (file != null) {
title = file.name;
content = await file.readAsString();
setState((){});
} else {
BotToast.showText(text: '打开了个寂寞🙄');
}

image


存储文本文件


存储文本需要用到 XFile 对象中的 fromData 方法。让我们来看看这个方法中需要传入什么参数:



  • Uint8List bytes:存储的主要内容

  • String? mimeType:文件的 mine 类型

  • String? name:文件名?测试了毫无用处😑

  • int? length:不知道是什么的长度,反正无法截取内容😑

  • DateTime? lastModified:最后修改文件的时间

  • String? path:文件保存的路径?测试了毫无效果😑

  • CrossFileTestOverrides? overrides:覆盖CrossFile的某些方法用来测试


(以上几个参数要是有朋友测试出来了,可以告知一下😁)


在存储文本文件前,我们需要先知道应该存储在哪个文件夹:


final String? path = await getSavePath();

然后再把 XFile.fromData 需要的参数放进去:


if (path != null) {
// 将内容编码成utf8
final Uint8List fileData = const Utf8Encoder().convert(content);
const String fileMimeType = 'text/plain';
final XFile xFile = XFile.fromData(
fileData,
mimeType: fileMimeType,
name: title,
);
await xFile.saveTo(path);
} else {
BotToast.showText(text: '给你个眼神自己体会😑');
}

5


获取文件夹路径


读取文件夹路径需要使用 getDirectoryPath 方法:


final String? path = await getDirectoryPath();
if (path != null) {
title = '目录';
content = path;
setState((){});
}

6


file_picker


安装🛠


点击file_picker获取最新版本。以下是在编写本文章时的最新版本:


file_picker: ^4.5.1

使用🥩


先定义一个默认的路径:


String path = '';

选择单个文件


选择单个文件需要用到 pickFiles 方法,该方法可以传入10个参数:



  • String? dialogTitle:弹窗的标题

  • String? initialDirectory:初始化的文件夹

  • FileType type = FileType.any:文件的类型

  • List<String>? allowedExtensions:允许的文件后缀名称

  • dynamic Function(FilePickerStatus)? onFileLoading:监听文件选择的状态

  • bool allowCompression = true:是否允许压缩

  • bool allowMultiple = false:是否允许选择多个文件

  • bool withData = false:如果为true,选取的文件将在内存中立即以“Uint8List”的形式提供其字节数据,如果您选择它进行服务器上传或类似操作,这将很有用。但是,请记住,如果您允许多个选择或选择大文件,则在 IO(iOS 和 Android)上启用此功能可能会导致内存不足问题。请改用 [withReadStream]。在 web 上默认为 true,其他为 false

  • bool withReadStream = false:拾取的文件将以 [Stream<List>] 的形式提供其字节数据,这对于上传和处理大文件很有用

  • bool lockParentWindow = false:是否将子窗口(文件选择器窗口)一直停留在 Flutter 窗口的前面,直到它关闭(如模态窗口)。此参数仅适用于 Windows


FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
File file = File(result.files.single.path!);
path = file.path;
setState((){});
}

image


我们试着添加一些参数:


FilePickerResult? result = await FilePicker.platform.pickFiles(
dialogTitle: '我的地盘我做主',
initialDirectory: r'C:\Users\ilgnefz\Pictures\Saved Pictures',
type: FileType.image,
);

8


initialDirectory 又没起作用😑


选择多个文件


定义一个接受所有路径的数组:


final List<String> paths = [];

FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
);
if (result != null) {
paths = result.files.map((e) => e.path!).toList();
setState((){});
}

读取文件信息


通过以上的方法,我们会得到一个 PlatformFile 对象:


FilePickerResult? result = await FilePicker.platform.pickFiles();
PlatformFile file = result.files.single;

该对象有以下几个属性:



  • name:文件名称

  • size:文件大小,以字节为单位

  • bytes:此文件的字节数据。如果您想操作其数据或轻松上传到其他地方,则特别有用。 在常见问题解答中查看此处 一个关于如何使用它在网络上上传的示例。

  • extension:文件后缀

  • path:文件路径

  • identifier:原始文件的平台标识符,是指 Android 上的 Uri 和 iOS 上的 NSURL。其他为null

  • readStream:将文件内容转换成流读取


1


存储文件


存储文件需要使用 saveFile 方法,该方法有可以传入6个参数:



  • String? dialogTitle:同 pickFiles 方法

  • String? fileName:存储文件的名字

  • String? initialDirectory:同 pickFiles 方法

  • FileType type = FileType.any:同 pickFiles 方法

  • List ? allowedExtensions:同 pickFiles 方法

  • bool lockParentWindow = false:同 pickFiles 方法


String? outputFile = await FilePicker.platform.saveFile();

(⊙o⊙)…这个方法连保存内容的参数都没有,诶!就是玩😄。官方说这个方法没有实际意义。


获取文件夹路径


获取文件夹需要使用 getDirectoryPath 方法,可以传入3个参数:



  • String? dialogTitle:同 pickFiles 方法

  • bool lockParentWindow = false:同 pickFiles 方法

  • String? initialDirectory:同 pickFiles 方法


final String title = '';
final String content = '';

String? dir = await FilePicker.platform.getDirectoryPath();
if (path != null) {
title = '目录';
content = path;
setState((){});
}

9


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。


最后,感谢 flutter 团队和 miguelpruivo 对以上插件的开发和维护😁。本应用代码已上传至 githubgitee,有需要的可以下载下来查看学习。


作者:菠萝橙子丶
链接:https://juejin.cn/post/7075889517210632200
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter桌面端开发——发送本地悬浮通知?

在我用的大部分桌面端中,发送本地悬浮通知的软件可以说是屈指可数。但是,这不妨碍到我们学习✊,奋斗说不定以后就能用到呢! 在选择该使用哪些插件开发桌面端的时候,由 lijy91 主导的 LeanFlutter 可以说是帮了很大的忙,有需要的可以自己去看看。 接下...
继续阅读 »

在我用的大部分桌面端中,发送本地悬浮通知的软件可以说是屈指可数。但是,这不妨碍到我们学习✊,奋斗说不定以后就能用到呢!


在选择该使用哪些插件开发桌面端的时候,由 lijy91 主导的 LeanFlutter 可以说是帮了很大的忙,有需要的可以自己去看看。


接下来要介绍的两个发送通知的插件,也是从 LeanFlutter 下的 awesome-flutter-desktop 仓库中找的。


local_notifier


安装🛠


点击local_notifier获取最新版本。以下是在编写本文章时的最新版本:


local_notifier: ^0.1.1

👻注意:在开发 Linux 端的程序时,还需要额外的操作,具体可以查看这里


使用🍖


要想实现发送通知的功能,需要用到一个实例化的 LocalNotifier 对象:


final localNotifier = LocalNotifier.instance;

如果你想全局使用,可以把该行代码放到一个自定义的全局类里。


要想编辑想发送的内容,需要用到 LocalNotification 类。实例化该类可以传入5个参数:



  • String? identifier:用来当做通用唯一识别码

  • required String title:发送通知的标题,一般是软件名称

  • String? subtitle:发送的通知内容的标题

  • String? body:发送的内容的主体

  • bool silent = false:在发送通知时是否静音


final notification = LocalNotification(
identifier: '12345',
title: '古诗鉴赏从',
subtitle: '桃夭 - 佚名〔先秦〕',
body: '桃之夭夭,灼灼其华。之子于归,宜其室家。\n桃之夭夭,有蕡其实。之子于归,宜其家室。\n桃之夭夭,其叶蓁蓁。之子于归,宜其家人。',
silent: false,
);
localNotifier.notify(notification);

现在我们就能愉快地发送一条自定义的通知了🎉


local-notifier


我们发现,其中其实只有title参数是必传的,我们就试一下只传入这个参数:


final notification = LocalNotification(
title: '古诗鉴赏从',
);
localNotifier.notify(notification);

local-notifier1


我们发现,只传 title 参数,它会自动将 title 的参数值赋值给 subtitle,而 body 参数会以“新通知”代替。
好了,以上就是 local_notifier 目前的全部功能,如果你只是简单的发送一条提示使用该插件完全够用。


win_toast


安装🛠


点击win_toast获取最新版本。以下是在编写本文章时的最新版本:


win_toast: ^0.0.2

使用🍖


在全局使用该插件,需要在app初始化时初始化。在某个页面使用,只要在页面初始化时初始化就行。


初始化时需要传递3个参数:



  • required String appName:程序名称

  • required String productName:产品名称

  • required String companyName:公司名称


await WinToast.instance().initialize(
appName: '第一个Desktop应用',
productName: '第一个Desktop应用',
companyName: 'Hiden Intelligence',
);

没写过原生,插件作者贴出Pick a unique AUMID that will identify your Win32 app告诉我们为什么要填这些内容👀,想了解的可以看一下。


要想发送一条通知,需要使用 showToast 方法,该方法有5个参数可以传:




  • required ToastType type:传入toast显示的类型,一共有8种:



    • imageAndText01至imageAndText04

    • text01至text04


    至于这些类型的异同,可以点这里👀




  • required String title:通知显示的标题




  • String subtitle = '':通知显示的主要内容




  • String imagePath = '':选择 imageAndText 类型时,要显示的图片




  • List actions = const []:显示通知中的按钮




使用text类型


先定义几个变量和常量:


Toast? toast;
final List<String> _title = 'Shining For One Thing(《一闪一闪亮星星》影视剧歌曲) - 赵贝尔';
final List<String> _subtitle = 'I fall in love\nI see your love\n遇见你才发现\n我在等你到来';
final List<String> _actione = ['上一首', '播放/暂停', '下一首'];

先来看看只传入文字的 text01 类型:


toast = await WinToast.instance().showToast(
type: ToastType.text01,
title: _title,
actions: _actione,
);

👻注意:当使用 ToastType.text01 或 ToastType.imageAndText01 时不能传入 subtitle 参数。


1


再来看看只传入文字的 text02 类型:


toast = await WinToast.instance().showToast(
type: ToastType.text02,
title: _title,
subtitle: _subtitle,
actions: _actione,
);

2


用了一下 ToastType.text03 和 ToastType.text04,发现显示的效果和 ToastType.text02 没有差别。大家可以自己试试。


使用imageAndText类型


修改一下常量的值(非必要):


Toast? toast;
final List<String> _title = '又下雨了,你的心情怎么样?';
final List<String> _subtitle = '偷偷告诉你,明天就天晴了😏\n好雨知时节,当春乃发生。随风潜入夜,润物细无声。野径云俱黑,江船火独明。晓看红湿处,花重锦官城。';
final List<String> _actione = ['不开森😭', '只想睡觉🥱', '非常高兴😃'];

还需要传入一张图片,目前无法得知应该传入图片的路径怎么填,所以先准备一张资源图片传入它的相对路径:


final String _imagePath = 'assets/images/pdx.jpg';

来看看 imageAndText01 类型:


toast = await WinToast.instance().showToast(
type: ToastType.imageAndText01,
title: _titles * 3,
imagePath: _imagePath,
actions: _action,
);

3


😲嗯?我们传入的图片怎么没显示?换个网络图片的链接试试:


final String _imagePath = 'https://gitee.com/ilgnefz/image-house/raw/master/images/pdx.jpg';

发现效果还是一样的。通过查看文档里的第一个链接中的例子,可以发现,这里需要传入图片的绝对路径。


那在 Flutter 中怎么获取文件的绝对路径呢🤔?当然,可以直接在 Android Studio 中选中图片点右键的 Copy Path,但是程序被打包安装后就不一定在这个位置了。学过Node.js,在里面获取文件的绝对路径要用到 Path 模块,那么 Flutter 是否也用同样的插件。打开 pub.dev 搜索,还真有。复制到 pubspec.yaml 进行安装,报错,告诉我们 Flutter Desktop 中已经集成了该插件,但是版本不一样。😀那不就好办了,第一步导入:


import 'package:path/path.dart' as path;

path 中没有 __dirname 方法,可以通过path. 查看提示,发现有一个 current的方法。虽然我们不知道这个方法是干什么的,但也不妨试试。修改 imagePath 为如下代码:


final String _imagePath = path.join(path.current, 'assets/images/pdx.jpg');

3


成功🎉🎉🎉🎉🎉


接下来使用 imageAndText02 类型来看看:


toast = await WinToast.instance().showToast(
type: ToastType.imageAndText02,
title: _titles,
subtitle: _subtitle,
imagePath: _imagePath,
actions: _action,
);

3


imageAndText03 和 imageAndText04 显示的效果也和 imageAndText02 无差别,这里就不放图了。


大家可能已经发现,通知中的3个按钮是由 actions 参数决定的,但是这个参数传入的是 String 类型,那我们要怎么才能获取到用户对这些按钮的点击事件呢?


前期我们定义了一个toast对象用来赋值,接下来就要用到这个参数:


if (toast != null) {
toast.eventStream.listen((event) {
if (event is ActivatedEvent) {
print(event);
}
});
}

在这里我们会获得一个 event 对象,通过打印会发现该对象下面只有一个属性actionIndex,返回的是 int? 类型。通过该属性,我们就可以获取到用户点击的是第几个按钮:


WinToast.instance().bringWindowToFront(); // 用户点击后关闭弹窗通知
BotToast.showText(text: '你当前的状态是${_action[event.actionIndex!]}');

4


知道了用户点击的是哪个按钮,接下来编写事件的代码就容易了。


🛫OK,以上就是这篇文章的全部内容,仅针对插件的当前版本,并不能保证适用于以后插件用法的更新迭代。本人只处于对代码的实践部分,如某些内容的概念或叫法出错还请指正🙏。


最后,感谢 lijy91boyan01 对以上插件的维护和开发😁。本程序相关代码已上传至 githubgitee,有需要的可以下载下来查看学习。


作者:菠萝橙子丶
链接:https://juejin.cn/post/7074482758747160590
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 实现卡片堆叠,钱包管理效果(带动画)

先上效果图 源码 github.com/woshiwzy/Ca… 实现原理: 1.继承LinearLayout 2.重写onLayout,onMeasure 方法 3.利用ValueAnimator 实施动画 4.在动画回调中requestLayout 实现...
继续阅读 »

先上效果图


result.gif


源码
github.com/woshiwzy/Ca…


实现原理:


1.继承LinearLayout

2.重写onLayout,onMeasure 方法

3.利用ValueAnimator 实施动画

4.在动画回调中requestLayout 实现动画效果


思路:


1.用Bounds 对象记录每一个CardView 对象的初始位置,当前位置,运动目标位置


2.点击时计算出对应的view以及可能会产生关联运动的View的运动的目标位置,从当前位置运动到目标位置,然后以这2个位置作为动画参数实施ValueAnimator动画,在动画回调中触发onLayout,达到动画的效果。


重写adView 方法,确保新添加的在这里确保所有的子view 都有一个初始化的bounds位置


   @Override
public void addView(View child, ViewGroup.LayoutParams params) {
super.addView(child, params);
Bounds bounds = getBunds(getChildCount());
}

确保每个子View的测量属性宽度填满父组件



boolean mesured = false;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mesured == true) {//只需要测量一次
return;
}
mesured = true;
int childCount = getChildCount();
int rootWidth = getWidth();
int rootHeight = getHeight();
if (childCount > 0) {
View child0 = getChildAt(0);
int modeWidth = MeasureSpec.getMode(child0.getMeasuredWidth());
int sizeWidth = MeasureSpec.getSize(child0.getMeasuredWidth());

int modeHeight = MeasureSpec.getMode(child0.getMeasuredHeight());
int sizeHeight = MeasureSpec.getSize(child0.getMeasuredHeight());

if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.measure(MeasureSpec.makeMeasureSpec(sizeWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(sizeHeight, MeasureSpec.EXACTLY));
int top = (int) (i * (sizeHeight * carEvPercnet));
getBunds(i).setTop(top);
getBunds(i).setCurrentTop(top);
getBunds(i).setLastCurrentTop(top);
getBunds(i).setHeight(sizeHeight);
}

}

}
}

重写onLayout 方法是关键,是动画触发的主要目的,这里layout参数并不是写死的,而是计算出来的(通过ValueAnimator 计算出来的)


@Override
protected void onLayout(boolean changed, int sl, int st, int sr, int sb) {
int childCount = getChildCount();
if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
int mWidth = view.getMeasuredWidth();
int mw = MeasureSpec.getSize(mWidth);
int l = 0, r = l + mw;
view.layout(l, getBunds(i).getCurrentTop(), r, getBunds(i).getCurrentTop() + getBunds(i).getHeight());
}
}
}

源码


github:
github.com/woshiwzy/Ca…


作者:Sand
链接:https://juejin.cn/post/7073371960150851615
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview

前言 跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企...
继续阅读 »
前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

Flutter作为基础的应用,如果要在flutter 中嵌入webview 去做Hybrid混合开发,咱们就必须要封装一套易用的webview,但网上关于flutter webview的文章极其的少。但的确也有做封装的文章,但是封装手法不够优雅,封装效果不够扩展。于是我打算把我的封装与大家分享,看我如何做到高扩展,高易用性。


目标:


Flutter 中嵌入 webview ,能够与 flutter 双向通信,并且易用。


搭建前夕准备


三方库:


webview_flutter flutter网页widget


开始搭建


一、基本回调


1.1 webview外部基本管理器


typedef void InnerWebPageCreatedCallback(InnerWebPageController controller);

涉及到的管理器源码之后介绍


1.2 显示处理回调


typedef WebPageCallBack = Function(String name,dynamic value);

由url拦截器,或者js返回数据后调用flutter页面代码,可以更新页面,或者状态变更。
使用如下:


webPageCallBack = (String name,dynamic value){
switch(name){
case LibWebPage.ACTION_SHOW_BAR:
setState(() {
widget.isShowToolBar = value;
});
break;
case LibWebPage.ACTION_SHOW_RIGHT:
setState(() {
widget.isShowRight = value;
});
break;
case LibWebPage.ACTION_BACK:
if(value){
Navigator.of(context).pop();
}else{
_goBack(context).then((value) => {
Navigator.of(context).pop()
});
}
break;
}

};

1.3 url拦截器处理


typedef WebPageUrlIntercept = bool Function(String url,InnerWebPageController? _controller);

一般用于处理 请求的url中的特殊文字处理


1.4 网页title 回调


typedef TitleCallBack = Function(String title);

网页加载完成后调用该回调展示当前html 的 title标签


二、构建web widget 控制管理器


class InnerWebPageController {
//webview原有管理器
WebViewController _controller;

InnerWebPageController(this._controller);
//执行网页js,在原有基础上封装,只需要发送jsname与参数
Future runJavascript(String funname,List? param,bool brackets) async{
String javaScriptString = getJavaScriptString(funname,param,brackets);
await _controller.runJavascript(javaScriptString);
}
//带返回值执行网页js,在原有基础上封装,只需要发送jsname与参数
Future runJavascriptReturningResult(String funname,List? param,bool brackets) async {
String javaScriptString = getJavaScriptString(funname,param,brackets);
_controller.runJavascript(javaScriptString);
return await _controller.runJavascriptReturningResult(javaScriptString);
}
//是否可以返回
Future canGoBack() {
return _controller.canGoBack();
}
//返回网页历史
Future goBack() {
return _controller.goBack();
}
//重新加载
Future reload() {
LibLoading.show();
return _controller.reload();
}
//获取js请求(工具)
String getJavaScriptString(String funname, List? param,bool brackets) {
var strb = StringBuffer(funname);
if(brackets){
strb.write("(");
}
if(param!=null&¶m.length>0){
for(int i=0;i ${strb.toString()}");
return strb.toString();
}

}


三、构建JavascriptChannels js注册抽象基础类


abstract class JavascriptChannels{

WebPageCallBack? webPageCallBack;
InnerWebPageController? controller;
JavascriptChannels();
//log日志
void logFunctionName(String functionName, String data) {
ULog.d("JS functionName -> $functionName JS params -> $data");
}

Set? baseJavascriptChannels(BuildContext context){
var javascriptChannels = {
_alertJavascriptChannel(context),
};
var other = otherJavascriptChannels(context);
if(other!=null){
javascriptChannels.addAll(other);
}
return javascriptChannels;
}

//lib库基本方法
JavascriptChannel _alertJavascriptChannel(BuildContext context) {
var jname = 'Toast';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
TipToast.instance.tip(message.message);
});
}
//实现类实现方法
Set? otherJavascriptChannels(BuildContext context);

}

三、构建UrlIntercept url拦截抽象基础类



abstract class UrlIntercept{
WebPageCallBack? webPageCallBack;

WebPageUrlIntercept _webPageUrlIntercept;

InnerWebPageController? controller;

UrlIntercept(this._webPageUrlIntercept);
//基本拦截
bool baseUrlIntercept(String url){
ULog.d('intercept: ${url}');
return _libUrlIntercept( url)||otherUrlIntercept( url);
}
//其他拦截
bool otherUrlIntercept(String url) {
return _webPageUrlIntercept.call(url,controller);
}
//lib 库默认拦截
bool _libUrlIntercept(String url) {
return _openPay(url);
}

// 跳转外部支付
bool _openPay(String url) {
if (url.startsWith('alipays:') || url.startsWith('weixin:')) {
canLaunch(url).then((value) => {
if(value){
launch(url)
}else{
TipToast.instance.tip('未安装支付软件')
}
});
return true;
}
return false;
}
}

四、webview widget实现




class InnerWebPage extends StatefulWidget{

String _url;
TitleCallBack? _titleCallBack;
JavascriptChannels? _javascriptChannels;
UrlIntercept? _urlIntercept;
InnerWebPageCreatedCallback? _onInnerWebPageCreated;
WebResourceErrorCallback? _onWebResourceError;
InnerWebPage(String url,{TitleCallBack? titleCallBack,JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,InnerWebPageCreatedCallback? onInnerWebPageCreated,WebResourceErrorCallback? onWebResourceError}):_url = url,_titleCallBack = titleCallBack,_javascriptChannels = javascriptChannels,_urlIntercept = urlIntercept,_onInnerWebPageCreated = onInnerWebPageCreated,_onWebResourceError = onWebResourceError;

@
override
State
createState() => _InnerWebPageState();

}

class _InnerWebPageState extends State
{

late
WebViewController _controller;
InnerWebPageController? _innercontroller;

@
override
void initState() {
super.initState();
// Android端复制粘贴问题
if (Platform.isAndroid) {
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}

}

@
override
Widget build(BuildContext context) {
return WebView(
onWebViewCreated: (controller){
ULog.i("WebView is create");
LibLoading.show();
_controller = controller;
_innercontroller =
InnerWebPageController(_controller);
widget._onInnerWebPageCreated?.call(_innercontroller!);
widget._javascriptChannels?.controller = _innercontroller;
widget._urlIntercept?.controller = _innercontroller;
//本地与线上文件展示
if(!TextUtil.isNetUrl(widget._url)){
_loadHtmlAssets(controller);
}
else{
controller.loadUrl(widget._url);
}
},
onPageFinished: (url) async{
//加载完成
LibLoading.dismiss();
ULog.d("${url} loading finish");
_controller.runJavascriptReturningResult(
"document.title").then((result){
widget._titleCallBack?.call(result);
});
},
onPageStarted: (
String url) {
ULog.d("${url} loading start");
},
onWebResourceError: (error){
//错误回调
LibLoading.dismiss();
ULog.d("loading error -> ${error.errorCode},${error.description},${error.domain},${error.errorType},${error.failingUrl}");
widget._onWebResourceError?.call(error);
},
navigationDelegate : (
NavigationRequest request){
//拦截处理
if (widget._urlIntercept?.baseUrlIntercept(request.url)??false) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
// initialUrl : TextUtil.isNetUrl(widget._url)?widget._url:Uri.dataFromString(widget._url, mimeType: 'text/html', encoding: Encoding.getByName('utf-8')).toString(),
// 是否支持js,默认是不支持的
javascriptMode:
JavascriptMode.unrestricted,
gestureNavigationEnabled:
true, //启用手势导航
//js 调用 flutter
javascriptChannels: widget._javascriptChannels?.baseJavascriptChannels(context),
);
}


//加载本地文件
_loadHtmlAssets(
WebViewController controller) async {
String htmlPath = await DefaultAssetBundle.of(context).loadString(widget._url);
controller.loadUrl(
Uri.dataFromString(htmlPath,mimeType: 'text/html', encoding: Encoding.getByName('utf-8'))
.
toString());
}


}


四、app中的实现与使用


4.1 JavascriptChannels 实现



class WisdomworkJavascriptChannels extends JavascriptChannels{
@override
Set? otherJavascriptChannels(BuildContext context) {
return {_appInfoJavascriptChannel(context),
_reportNameJavascriptChannel(context),
_saveImageJavascriptChannel(context),
};
}
//调用函数
JavascriptChannel _appInfoJavascriptChannel(BuildContext context) {
var jname = 'appInfo';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
Map user = convert.jsonDecode(message.message);
if(user.containsKey("showBar")){
webPageCallBack?.call(LibWebPage.ACTION_SHOW_BAR,user["showBar"]!);
// setState(() {
// isShowToolBar = user["showBar"]!;
// });
}

if(user.containsKey("shareFlag")){
webPageCallBack?.call(LibWebPage.ACTION_SHOW_RIGHT,user["shareFlag"]!);
// setState(() {
// hasShare = user["shareFlag"]!;
// });
}

// 数据传输
String callbackname = message.message; //实际应用中要通过map通过key获取
Map backParams = {
"userToken": UserStore().getUserToken()??"",
"userId": UserStore().getUserId()??"",
"userName": UserStore().getUserName()??"",
"titleHeight":MediaQuery.of(context).size.height * 0.07,
"statusHeight":MediaQueryData.fromWindow(window).padding.top,
"role": "teacher"
};

String jsondata= convert.json.encode(backParams);
controller?.runJavascript("callJS", [jsondata],true);
});
}



JavascriptChannel _reportNameJavascriptChannel(BuildContext context) {
var jname = 'getReportName';

return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);

Map user = convert.jsonDecode(message.message);
if(user.containsKey("reportName")){
webPageCallBack?.call(WisdomworkLibWebPageCallback.REPORT_NAME,user['reportName']!);
}
});
}


JavascriptChannel _saveImageJavascriptChannel(BuildContext context) {
var jname = 'savePicture';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
Map user = convert.jsonDecode(message.message);
if(user.containsKey("url")){
var url = user['url']!;
if(url.isNotEmpty){
ULog.d("下载的地址:$url");
ImageTool.saveImageToPhoto(url);
}
}
});
}
}

4.2 UrlIntercept 实现



class WisdomworkUrlIntercept extends UrlIntercept{
WisdomworkUrlIntercept() : super((String url,InnerWebPageController? _controller) {
return false;
});

}

4.3 web通用页实现



import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:flutter_base_ui/src/widget/appbar/default_app_bar.dart';
import 'package:flutter_base_ui/src/widget/web/inner_web_page.dart';
import 'package:flutter_base_ui/src/widget/web/url_intercept.dart';

import 'javascript_channels.dart';

abstract class LibWebPageCallBack{

void libWebPagerightBtn(String? key,dynamic value,InnerWebPageController _controller);
void libWebPageCallBack(String? key,dynamic value,InnerWebPageController _controller);

}

class LibWebPage extends StatefulWidget{

static const String TITLE = "title";
static const String URL = "url";
static const String RIGHT = "right";
static const String RIGHT_VALUE = "rightValue";
static const String RIGHT_KEY = "rightKey";
static const String BACKPAGE = "backpage";


static const String ACTION_SHOW_BAR = "actionShowBar";
static const String ACTION_BACK = "actionBack";
static const String ACTION_SHOW_RIGHT = "actionShowRight";

static LibWebPage start(Map argument,{JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,LibWebPageCallBack? libWebPageCallBack,Widget? back}){
return LibWebPage(argument[URL]!,title: argument[TITLE],javascriptChannels: javascriptChannels,urlIntercept: urlIntercept,back:back,libWebPageCallBack: libWebPageCallBack
,backPage: argument[BACKPAGE],right: argument[RIGHT],rightValue: argument[RIGHT_VALUE],rightkey: argument[RIGHT_KEY],isShowRight: argument[ACTION_SHOW_RIGHT],isShowToolBar: argument[ACTION_SHOW_BAR],);
}

static Map getArgument(String url,{String? title,bool? backPage, Widget? right,bool? isShowToolBar,bool? isShowRight,String? rightkey,dynamic rightValue}){
return {
URL :url,
TITLE :title,
RIGHT :right,
RIGHT_VALUE :rightValue,
RIGHT_KEY :rightkey,
BACKPAGE :backPage,
ACTION_SHOW_BAR :isShowToolBar,
ACTION_SHOW_RIGHT :isShowRight,
};
}



String? title;
final String url;
JavascriptChannels? _javascriptChannels;
UrlIntercept? _urlIntercept;
LibWebPageCallBack? _libWebPageCallBack;
bool _backPage;
Widget? _right;
String? _rightkey;
dynamic _rightValue;
Widget? _back;
bool isShowToolBar;
bool isShowRight;
LibWebPage(String url,{String? title,bool? isShowToolBar,bool? isShowRight,JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,Widget? back,bool? backPage, Widget? right,String? rightkey,dynamic rightValue,LibWebPageCallBack? libWebPageCallBack})
:this.url = url,_javascriptChannels = javascriptChannels,_urlIntercept = urlIntercept,this.title = title,this._back = back,this.isShowToolBar = isShowToolBar??true,this.isShowRight = isShowRight??true,
_backPage = backPage??false,_right = right,_rightkey = rightkey,_rightValue = rightValue,_libWebPageCallBack = libWebPageCallBack;

@override
State createState() => _LibWebPageState();

}

class _LibWebPageState extends State{

late InnerWebPageController _innerWebPageController;

String? urlTitle;
// EmptyStatusController? emptyStatusController;
var status = EmptyStatus.none;

WebPageCallBack? webPageCallBack;

@override
void initState() {
super.initState();
webPageCallBack = (String name,dynamic value){
widget._libWebPageCallBack?.libWebPageCallBack(name, value, _innerWebPageController);
switch(name){
case LibWebPage.ACTION_SHOW_BAR:
setState(() {
widget.isShowToolBar = value;
});
break;
case LibWebPage.ACTION_SHOW_RIGHT:
setState(() {
widget.isShowRight = value;
});
break;
case LibWebPage.ACTION_BACK:
if(value){
Navigator.of(context).pop();
}else{
_goBack(context).then((value) => {
Navigator.of(context).pop()
});
}
break;
}

};

}

@override
Widget build(BuildContext context) {
var title;
if(widget.title == null){
if(urlTitle!=null){
title = urlTitle;
}
}else{
title = widget.title;
}

return WillPopScope(child: Scaffold(


appBar: !widget.isShowToolBar? null
: DefalutBackAppBar(title??"",back : widget._back,showRight :widget.isShowRight,tap: () => _goBack(context),right: widget._right,rightcallback: (){
widget._libWebPageCallBack?.libWebPagerightBtn(widget._rightkey, widget._rightValue, _innerWebPageController);
},),
body: LibEmptyView(
layoutType: status,
refresh: () {

status = EmptyStatus.none;
_innerWebPageController.reload();

},

child: InnerWebPage(widget.url,titleCallBack: (title){
setState(() {
urlTitle = title;
});
},javascriptChannels: widget._javascriptChannels,urlIntercept: widget._urlIntercept,onInnerWebPageCreated: (innerWebPageController){
_innerWebPageController = innerWebPageController;
widget._javascriptChannels?.webPageCallBack = webPageCallBack;
widget._urlIntercept?.webPageCallBack = webPageCallBack;
},onWebResourceError: (error){
setState(() {
status = EmptyStatus.fail;
});
},),
),
),
onWillPop: () {
return _goBack(context);
});
}

Future _goBack(BuildContext context) async {
if(widget._backPage){
return true;
}
if (await _innerWebPageController.canGoBack()) {
_innerWebPageController.goBack();
return false;
}
return true;
}
}

4.3 外部页面WebPageCallBack 回调,处理js交互逻辑


例子(处理下载pdf,并分享)



class WisdomworkLibWebPageCallback extends LibWebPageCallBack{
static const String REPORT_DETAIL = "ReportDetail";
static const String REPORT_NAME = "reportName";

String? reportName;

@override
void libWebPageCallBack(String? key, dynamic value, InnerWebPageController _controller)
{
switch(key){
case REPORT_NAME:
reportName = value;
break;
}
}

@override
void libWebPagerightBtn(String? key, dynamic value, InnerWebPageController _controller)
{
switch(key){
case REPORT_DETAIL:
if(reportName?.isEmpty??true){
TipToast.instance.tip("网页加载完毕后再分享");
return;
}
LibLoading.show(status: "下载中");
ReportResponsitory.instance.createFileOfPdfUrl(value.toString(),reportName!).then((f) {
ULog.d(f);
String pdfpath = f.path;
List imagePaths = [];
imagePaths.add(pdfpath);
final box = LibRouteNavigatorObserver.instance.navigator!.context.findRenderObject() as RenderBox?;
LibLoading.dismiss();
Share.shareFiles(imagePaths,
mimeTypes: ["application/pdf"],
text: null,
subject: null,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
});
break;
}
}

}

以上就是flutter 的Hybrid 混合开发封装


本人将js与拦截操作从原有的web组件中抽离出来,相当于业务抽离在外。与webview的耦合降低。


感谢大家阅读我的文章

收起阅读 »

Kotlin - 改良观察者模式

一、前言 观察者模式 作用:定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。 核心操作: 观察者(订阅者)添加或删除对 被观察者(主题)的状态监听...
继续阅读 »

一、前言



  • 观察者模式

    • 作用:定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。

    • 核心操作:

      • 观察者(订阅者)添加或删除对 被观察者(主题)的状态监听

      • 被观察者(主题)状态改变时,将事件通知给所有观察者,观察者执行响应逻辑






二、使用观察者模式



  • 例子:监听股票价格变动

  • 重点:使用 Java API 或 自定义实现 观察者模式


1、使用 Java API 实现观察者模式


Java 标准库中提供了通用观察者模式的 API,分别是:



  • java.util.Observable:被观察者(主题)

    • setChanged():标记状态更新

    • addObserver():添加观察者

    • deleteObserver():删除观察者

    • countObservers():获取观察者数量

    • notifyObservers():通知所有观察者

    • notifyObservers(Object arg):通知所有观察者(携带参数 arg)



  • java.util.Observer:观察者(订阅者)


利用 Java API,可以实现监听股票价格变动这个功能:


import java.util.Observable
import java.util.Observer

/**
* 被观察者(主题)
*
* @author GitLqr
*/
class StockSubject : Observable() {
fun changeStockPrice(price: Int) {
this.setChanged() // 标识状态更新
this.notifyObservers(price) // 通知所有观察者当前股票价格
}
}

/**
* 观察者(订阅者)
*
* @author GitLqr
*/
class StockDisplay(val name: String) : Observer {
override fun update(o: Observable?, price: Any?) {
println("$name receive stock price : $price") // 注意 price 的类型是 Any?
}
}

// 使用
val subject = StockSubject()
subject.addObserver(StockDisplay("observer 1"))
subject.addObserver(StockDisplay("observer 2"))
subject.changeStockPrice(200)

// 输出
// observer 2 receive stock price : 200
// observer 1 receive stock price : 200


注意:在主题中通过 notifyObservers() 方法通知订阅者之前,需要先调用 setChanged() 标识状态更新,才能正常通知给订阅者,这是使用 Java API 实现观察者模式时需要注意的一点。



Java 提供的 API 已经涵盖了观察者模式的完整实现,所以我们在使用的时候,只需要关注业务本身,而不用自己去做模式的具体实现,但是呢,Java 提供的 API 是一种通用实现,从上面的例子中可以注意到,StockDisplay.update(o: Observable?, price: Any?) 中的 price 参数类型是 Any? ,这就会有以下几个问题:



  • 参数判断:因为参数类型是 Any?,所以开发中不得不对 参数是否为空 以及 参数的实际类型 做判断。

  • 通知入口单一:实际业务需求会更加复杂,而 java.util.Observer 只有唯一一个通知入口 update(o: Observable?, arg: Any?),所以我们不得不在该方法中分离响应逻辑,比如股票价格升降,这会让代码显得臃肿。


2、自定义实现观察者模式


虽然 Java 提供了现成的观察者模式 API,但是实际开发中,我们通常还是会自定义实现观察者模式,以便更好的控制代码结构:


/**
* 回调接口(解耦业务通知入口)
*
* @author GitLqr
*/
interface StockUpdateListener {
fun onRise(price: Int)
fun onFall(price: Int)
}

/**
* 被观察者(主题)
*
* @author GitLqr
*/
class StockSubject {
val listeners = mutableSetOf<StockUpdateListener>()
var price: Int = 0

fun subscribe(observer: StockUpdateListener) {
listeners.add(observer)
}

fun unsubscribe(observer: StockUpdateListener) {
listeners.remove(observer)
}

fun changeStockPrice(price: Int) {
val isRise = price > this.price
listeners.forEach { if (isRise) it.onRise(price) else it.onFall(price) }
this.price = price
}
}

/**
* 观察者(订阅者)
*
* @author GitLqr
*/
class StockDisplay : StockUpdateListener {
override fun onRise(price: Int) {
println("The latest stock price has rise to $price")
}

override fun onFall(price: Int) {
println("The latest stock price has fell to $price")
}
}

// 使用
val subject = StockSubject()
subject.subscribe(StockDisplay())
subject.changeStockPrice(200) // The latest stock price has rise to 200

可见,自定义实现观察者模式,可以让代码结构变得更加简单直观。


三、改良观察者模式



  • 例子:监听股票价格变动

  • 重点:委托属性 Delegates.observable()


Kotlin 标准库引入了可被观察的委托属性,可通过 xxx by Delegates.observable() 的方式,用来监听 xxx 属性的改变,于是可以用来改良上面的自定义观察者模式:


import kotlin.properties.Delegates

/**
* 观察者模式改良:使用委托属性监听值变化后通知
*
* @author GitLqr
*/
class StockSubject {
val listeners = mutableSetOf<StockUpdateListener>()

var price: Int by Delegates.observable(0) { prop, old, new ->
val isRise = new > old
listeners.forEach { if (isRise) it.onRise(price) else it.onFall(price) }
}

fun subscribe(observer: StockUpdateListener) {
listeners.add(observer)
}

fun unsubscribe(observer: StockUpdateListener) {
listeners.remove(observer)
}

// fun changeStockPrice(price: Int) { ... }
}

// 使用
val subject = StockSubject()
subject.subscribe(StockDisplay())
subject.price = 250 // The latest stock price has rise to 200

使用 Delegates.observable() 之后,StockSubject 相比之前减少了一个 changeStockPrice() 方法。使用上,一旦对 price 属性赋值,就可以触发通知,显然,这对使用者更加友好了(直观,少记一个方法)。


四、补充


前面说到,Kotlin 标准库引入可被观察的委托属性,除了 Delegates.observable() 之外,还有
Delegates.vetoable() 也很实用,当我们不希望被监控的属性被随意修改时,就可以用它来否决属性赋值:


import kotlin.properties.Delegates

var value: Int by Delegates.vetoable(0) { prop, old, new ->
// 新值大于0时,才给属性赋值
new > 0
}

// 使用
value = 1
println(value) // 1
value = -1
println(value) // 1(没能赋值成功)

作者:GitLqr
链接:https://juejin.cn/post/7074590108925722655
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

面试官:private修饰的方法可以通过反射访问,那么private的意义是什么?

在一个类中,为了不让外界访问到某些属性和方法,通常将其设置为private,用正常的方式(对象名.属性名,对象名.方法名)将无法访问此属性与方法,但有没有其他方法可以访问呢?答案是有的,这就是java反射带来的便利。利用反射访问类的私有属性及方法如下: /**...
继续阅读 »

在一个类中,为了不让外界访问到某些属性和方法,通常将其设置为private,用正常的方式(对象名.属性名,对象名.方法名)将无法访问此属性与方法,但有没有其他方法可以访问呢?答案是有的,这就是java反射带来的便利。利用反射访问类的私有属性及方法如下:


/**  * @Description: 反射  * @author: Mr_VanGogh  */
public class Reflect {
 
    private String name;
    private int age;
 
    private Reflect(int age) {
        this.age = age;
    }
 
    private void speak(String name) {
        System.out.println("My name is" + name);
    }
 
    public Reflect(String name) {
        this.name = name;
    }
}

首先,我们要了解三个反射包中的类:



  • Constructor:代表类的单个构造方法,通过Constructor我们可执行一个类的某个构造方法(有参或者无参)来创建对象时。

  • Method:代表类中的单个方法,可以用于执行类的某个普通方法,有参或无参,并可以接收返回值。

  • Field:代表类中的单个属性,用于set或get属性

  • AccessibleObject:以上三个类的父类,提供了构造方法,普通方法,和属性的访问控制的能力。


使用Class类中的方法可以获得该类中的所有Constructor对象,Method对象,和Field对象。但是任然无法访问私有化的构造方法,普通方法,和私有属性,此时我们可以使用他们继承父类(AccessibleObject)中的setAccessible()方法,来设置或取消访问检查,以达到访问私有对象的目的。


public static void main(String[] args)  throws Exception {
 
        Reflect reflect = new Reflect("a");
 
        Method[] methods = Reflect.class.getMethods();
        Field[] fields = Reflect.class.getDeclaredFields();
 
        for (int i = 0; i < fields.length; i ++) {
            fields[i].setAccessible(true);
            System.out.println(fields[i].getName());
        }
 
        for (int j = 0; j < methods.length; j ++) {
            methods[j].setAccessible(true);
            System.out.println(methods[j].getName());
 
            methods[j].invoke(reflect);
            System.out.println(methods[j].getName());
        }
    }

这样,我们就获得了私有属性的值


当然,凡事有利就有弊,然后我们再来说一下java反射的优缺点;


优点:



  • 能够运行时动态获取类的实例,大大提高了系统的灵活性和扩展性;

  • 与java动态编译相结合,可以实现无比强大的功能。


缺点:



  • 使用反射的性能较低;

  • 使用反射来说相对不安全;

  • 破坏了类的封装性,可以通过反射来获取这个类的属性,和私有方法。


Q:private修饰的方法可以通过反射访问,那么private的意义是什么?


A:


1、Java的private修饰符不是为了绝对安全设计的,而是对用户常规使用Java的一种约束。就好比饭店厨房门口挂着“闲人免进”的牌子,但是你还是能够通过其他方法进去。


2、从外部对对象进行常规调用时,能够看到清晰的类结构


作者:MarkerHub
链接:https://juejin.cn/post/7073654386886443022
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

快慢指针的妙用

快慢指针是双指针的一种典型用法,通常控制两个指针以不同的速度移动来解决问题。采用快慢指针解决问题往往都很巧妙。本文我们通过分析几个例子来学习快慢指针的用法,并分析其本质,最终达到方便记忆、灵活使用的目的。 接下来我们先看四个例子:判断链表是否有环、寻找链表中环...
继续阅读 »

快慢指针是双指针的一种典型用法,通常控制两个指针以不同的速度移动来解决问题。采用快慢指针解决问题往往都很巧妙。本文我们通过分析几个例子来学习快慢指针的用法,并分析其本质,最终达到方便记忆、灵活使用的目的。


接下来我们先看四个例子:判断链表是否有环、寻找链表中环的入口、寻找链表的中间结点、寻找链表的倒数第 n 个结点。


判断链表是否有环


题目(来源Leetcode)


“给你一个链表的头结点 head ,判断链表中是否有环。


如果链表中有某个结点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。


如果链表中存在环 ,则返回 true 。 否则,返回 false 。”



环形链表:leetcode-cn.com/problems/li…



分析


这道题有两种常见解法:1. 哈希表 2. 快慢指针。我们主要分析快慢指针的解法:


我们定义两个指针:一个移动的慢叫慢指针,一个移动的快叫快指针。慢指针在链表中一次移动一个结点,快指针在链表中一次移动两个结点,如果两个指针最终相遇了,说明有环;如果快指针顺利到达了链表的终点说明没有环。



相遇:快指针 = 慢指针;到达终点:快指针的下一个结点为Null。



要想理解这个解决办法需要先了解:Floyd 判圈算法


Floyd 判圈算法又称为龟兔赛跑算法(Tortoise and Hare Algorithm)。乌龟跑得慢、兔子跑得快。乌龟和兔子在赛跑,如果存在环的话,兔子必然会追上乌龟(乌龟被套圈了)。


我们把乌龟比作慢指针、兔子比作快指针同时在链表中移动。跑步和指针移动不太相同的是,跑步的路程是连续的,快指针一次移动两个结点是不连续的。又反过来想一下如果链表中存在环,那最小的环也需要两个结点,所以对于是否有环来说快指针一次移动两个结点也是不会错过任何一个环的。而且环的结点不管是奇数还是偶数个,快指针也最终会和慢指针在某个结点重合。


答案


基于上面的分析我们给出代码如下:


func hasCycle(head *ListNode) bool {
    if head == nil || head.Next == nil { // 如果链表有0个或者1个结点,则链表不存在环
        return false
    }
    slowPoint, fastPoint := head, head.Next // 定义快慢指针
    for fastPoint != slowPoint { // 如果快慢指针相等则结束循环,证明有环
        if fastPoint == nil || fastPoint.Next == nil { // 如果快指针到达终点或者终点前的倒数第一个结点,说明没有环
            return false
        }
        slowPoint = slowPoint.Next
        fastPoint = fastPoint.Next.Next
    }
    return true
}

寻找链表中环的入口


题目(来源Leetcode)


“给定一个链表的头结点 head ,返回链表开始入环的第一个结点。 如果链表无环,则返回 null。


如果链表中有某个结点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。”



环形链表 II:leetcode-cn.com/problems/li…



分析


这道题和上题一样,通常有哈希表、快慢指针两种解法。我们主要分析快慢指针的解法:


我们定义两个指针:一个移动的慢叫慢指针,一个移动的快叫快指针。慢指针在链表中一次移动一个结点,快指针在链表中一次移动两个结点,如果两个指针最终相遇了,说明有环;如果快指针顺利到达了链表的终点说明没有环。



当有环存在时,我们假设快慢指针在 D 点相遇。a 代表入环之前的长度,b 代表慢指针进入环后又走了b的长度,c 代表环余下的长度。指针的指向是圆的顺时针方向。



  1. 如果快指针和慢指针在 D 点相遇,此时快指针比慢指针多走了 n 圈,也就是 n*(b+c) 的长度。

  2. 此时快指针走过的距离是 a+n*(b+c)+b,慢指针走过的距离是 a+b。

  3. 因为快指针每次走两步,慢指针每次走一步,所以快指针走过的距离永远是慢指针的两倍,所以 a+n* (b+c)+b=2(a+b)*

  4. 上述公式可以推导出a = (n-1)*(b+c)+c,也就是a的长度是恰好是 n-1 圈环的长度加上从 D 点到相遇点的距离。


根据上面的推论,当快慢指针相遇之后,我们再申请一个指针从链表的头部开始,每次移动一个结点,同时慢指针一次移动一个结点,这两个指针最终的相遇点就是环的入口点。


答案


func detectCycle(head *ListNode) *ListNode {
    slowPoint, fastPoint := head, head
    for fastPoint != nil && fastPoint.Next != nil { // 如果快指针到达终点或者终点前的倒数第一个结点,说明没有环。
        slowPoint = slowPoint.Next
        fastPoint = fastPoint.Next.Next
        if fastPoint == slowPoint { // 如果快慢指针相等,说明有环,后面开始寻找环的入口。
            delectPoint := head
            for delectPoint != slowPoint { // 慢指针和从头开始的delectPoint指针相等,则说明当前结点就是环的入口。
                delectPoint = delectPoint.Next
                slowPoint = slowPoint.Next
            }
            return delectPoint
        }
    }
    return nil
}

寻找链表的中间结点


题目(来源Leetcode)


给定一个头结点为 head 的非空单链表,返回链表的中间结点。


如果有两个中间结点,则返回第二个中间结点。



链表的中间结点:leetcode-cn.com/problems/mi…



分析


这道题有多种解法,但是快慢指针的解法十分巧妙。我们定义两个指针:一个移动的慢叫慢指针,一个移动的快叫快指针。慢指针在链表中一次移动一个结点,快指针在链表中一次移动两个结点。因为快指针移动的距离始终是慢指针的两倍,所以当快指针移动到链表尾部时,慢指针刚好在链表中间位置。


答案


func middleNode(head *ListNode) *ListNode {
    slowPoint, fastPoint := head, head
    for fastPoint!= nil && fastPoint.Next != nil{
        slowPoint = slowPoint.Next
        fastPoint = fastPoint.Next.Next
    }
    return slowPoint
}

寻找链表的倒数第 N 个结点


题目


给你一个链表,找到链表的倒数第 n 个结点并返回。


分析


这道题有多种解法,但是快慢指针的解法十分巧妙。我们定义两个指针:两个指针每次移动一个结点。我们让其中一个指针先移动 n 个结点,先移动的这个指针我们叫它快指针,另外一个叫慢指针。然后两个指针同时移动。因为移动速度相同所以两个指针之间的距离始终是 n ,当快指针到达链表尾部时,慢指针刚好指向了链表的倒数第 n 个结点。



这里叫“快慢指针”有点牵强,感觉叫前后指针更合适,不过为了方便记忆就先归为快慢指针吧。



答案


func findNthFromEnd(head *ListNode, n int) *ListNode {
    fastPoint,slowPoint := node,node
    for i:=0; i<n+1; i++{
        fastPoint  = fastPoint.Next
    }
    for fastPoint != nil {
        fastPoint=  fastPoint.Next
        slowPoint = slowPoint.Next
    }
    return slowPoint
}

总结


本文我们介绍了快慢指针的常见用法:



  1. 利用 Floyd 判圈算法找环。

  2. 利用 Floyd 判圈算法找环的入口。

  3. 寻找链表的中间结点。

  4. 寻找链表的倒数第 n 个结点。


在 Floyd 判圈算法中,把判断环的问题抽象成两个指针围绕环运动最终会相遇的问题,通过环形跑道这个生动场景解决了问题;在寻找链表中间节点时,利用两个指针的速度差,使一个指针运动的距离始终是另外一个指针的一半,用指针的速度、路程解决了问题;在寻找链表倒数第 n 个节点时,让相同速度的两个指针始终保持 n 的相对距离,把链表问题抽象成了距离问题。


这几种方法都是把算法问题抽象成了两个指针的距离、速度问题,最终通过数学公式推导得出结论。推而广之,我们以后遇到链表的相关问题也可以采用类似的方式去抽象问题,推导解决办法。


作者:一行舟
链接:https://juejin.cn/post/7074945262292041758
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

MVVM 进阶版:MVI 架构了解一下~

MVI
前言 Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。 不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而...
继续阅读 »

前言


Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。

不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而MVI可以很好的解决一部分MVVM的痛点。

本文主要包括以下内容



  1. MVC,MVP,MVVM等经典架构介绍

  2. MVI架构到底是什么?

  3. MVI架构实战



需要重点指出的是,标题中说MVI架构是MVVM的进阶版是指MVIMVVM非常相似,并在其基础上做了一定的改良,并不是说MVI架构一定比MVVM适合你的项目

各位同学可以在分析比较各个架构后,选择合适项目场景的架构



经典架构介绍


MVC架构介绍


MVC是个古老的Android开发架构,随着MVPMVVM的流行已经逐渐退出历史舞台,我们在这里做一个简单的介绍,其架构图如下所示:



MVC架构主要分为以下几部分



  1. 视图层(View):对应于xml布局文件和java代码动态view部分

  2. 控制层(Controller):主要负责业务逻辑,在android中由Activity承担,同时因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。

  3. 模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源


由于androidxml布局的功能性太弱,Activity实际上负责了View层与Controller层两者的工作,所以在androidmvc更像是这种形式:



因此MVC架构在android平台上的主要存在以下问题:



  1. Activity同时负责ViewController层的工作,违背了单一职责原则

  2. Model层与View层存在耦合,存在互相依赖,违背了最小知识原则


MVP架构介绍


由于MVC架构在Android平台上的一些缺陷,MVP也就应运而生了,其架构图如下所示



MVP架构主要分为以下几个部分



  1. View层:对应于ActivityXML,只负责显示UI,只与Presenter层交互,与Model层没有耦合

  2. Presenter层: 主要负责处理业务逻辑,通过接口回调View

  3. Model层:主要负责网络请求,数据库处理等操作,这个没有什么变化


我们可以看到,MVP解决了MVC的两个问题,即Activity承担了两层职责与View层与Model层耦合的问题


MVP架构同样有自己的问题



  1. Presenter层通过接口与View通信,实际上持有了View的引用

  2. 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成View的接口会很庞大。


MVVM架构介绍


MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向数据绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然

MVVM架构图如下所示:



可以看出MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP


MVVM的双向数据绑定主要通过DataBinding实现,不过相信有很多人跟我一样,是不喜欢用DataBinding的,这样架构就变成了下面这样



  1. View观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性我其实并没有用到

  2. View通过调用ViewModel提供的方法来与ViewMdoel交互


小结



  1. MVC架构的主要问题在于Activity承担了ViewController两层的职责,同时View层与Model层存在耦合

  2. MVP引入Presenter层解决了MVC架构的两个问题,View只能与Presenter层交互,业务逻辑放在Presenter

  3. MVP的问题在于随着业务逻辑的增加,View的接口会很庞大,MVVM架构通过双向数据绑定可以解决这个问题

  4. MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP

  5. MVVM的双向数据绑定主要通过DataBinding实现,但有很多人(比如我)不喜欢用DataBinding,而是View通过LiveData等观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定


MVI架构到底是什么?


MVVM架构有什么不足?


要了解MVI架构,我们首先来了解下MVVM架构有什么不足

相信使用MVVM架构的同学都有如下经验,为了保证数据流的单向流动,LiveData向外暴露时需要转化成immutable的,这需要添加不少模板代码并且容易遗忘,如下所示


class TestViewModel : ViewModel() {
//为保证对外暴露的LiveData不可变,增加一个状态就要添加两个LiveData变量
private val _pageState: MutableLiveData<PageState> = MutableLiveData()
val pageState: LiveData<PageState> = _pageState
private val _state1: MutableLiveData<String> = MutableLiveData()
val state1: LiveData<String> = _state1
private val _state2: MutableLiveData<String> = MutableLiveData()
val state2: LiveData<String> = _state2
//...
}

如上所示,如果页面逻辑比较复杂,ViewModel中将会有许多全局变量的LiveData,并且每个LiveData都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM架构写比较复杂页面时最难受的点。

其次就是View层通过调用ViewModel层的方法来交互的,View层与ViewModel的交互比较分散,不成体系


小结一下,在我的使用中,MVVM架构主要有以下不足



  1. 为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘

  2. View层与ViewModel层的交互比较分散零乱,不成体系


MVI架构是什么?


MVIMVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示



其主要分为以下几部分



  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态

  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新

  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求


单向数据流


MVI强调数据的单向流动,主要分为以下几步:



  1. 用户操作以Intent的形式通知Model

  2. Model基于Intent更新State

  3. View接收到State变化刷新UI。


数据永远在一个环形结构中单向流动,不能反向流动:


上面简单的介绍了下MVI架构,下面我们一起来看下具体是怎么使用MVI架构的


MVI架构实战


总体架构图




我们使用ViewModel来承载MVIModel层,总体结构也与MVVM类似,主要区别在于ModelView层交互的部分



  1. Model层承载UI状态,并暴露出ViewStateView订阅,ViewState是个data class,包含所有页面状态

  2. View层通过Action更新ViewState,替代MVVM通过调用ViewModel方法交互的方式


MVI实例介绍


添加ViewStateViewEvent


ViewState承载页面的所有状态,ViewEvent则是一次性事件,如Toast等,如下所示


data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)  

sealed class MainViewEvent {
data class ShowSnackbar(val message: String) : MainViewEvent()
data class ShowToast(val message: String) : MainViewEvent()
}


  1. 我们这里ViewState只定义了两个,一个是请求状态,一个是页面数据

  2. ViewEvent也很简单,一个简单的密封类,显示ToastSnackbar


ViewState更新


class MainViewModel : ViewModel() {
private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData()
val viewStates = _viewStates.asLiveData()
private val _viewEvents: SingleLiveEvent<MainViewEvent> = SingleLiveEvent()
val viewEvents = _viewEvents.asLiveData()

init {
emit(MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList()))
}

private fun fabClicked() {
count++
emit(MainViewEvent.ShowToast(message = "Fab clicked count $count"))
}

private fun emit(state: MainViewState?) {
_viewStates.value = state
}

private fun emit(event: MainViewEvent?) {
_viewEvents.value = event
}
}

如上所示



  1. 我们只需定义ViewStateViewEvent两个State,后续增加状态时在data class中添加即可,不需要再写模板代码

  2. ViewEvents是一次性的,通过SingleLiveEvent实现,当然你也可以用Channel当来实现

  3. 当状态更新时,通过emit来更新状态


View监听ViewState


    private fun initViewModel() {
viewModel.viewStates.observe(this) {
renderViewState(it)
}
viewModel.viewEvents.observe(this) {
renderViewEvent(it)
}
}

如上所示,MVI 使用 ViewStateState 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。


View通过Action更新State


class MainActivity : AppCompatActivity() {
private fun initView() {
fabStar.setOnClickListener {
viewModel.dispatch(MainViewAction.FabClicked)
}
}
}
class MainViewModel : ViewModel() {
fun dispatch(action: MainViewAction) =
reduce(viewStates.value, action)

private fun reduce(state: MainViewState?, viewAction: MainViewAction) {
when (viewAction) {
is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
MainViewAction.FabClicked -> fabClicked()
MainViewAction.OnSwipeRefresh -> fetchNews(state)
MainViewAction.FetchNews -> fetchNews(state)
}
}
}

如上所示,View通过ActionViewModel交互,通过 Action 通信,有利于 ViewViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控


总结


本文主要介绍了MVC,MVP,MVVMMVI架构,目前MVVM是官方推荐的架构,但仍然有以下几个痛点



  1. MVVMMVP的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源

  2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘

  3. ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护


MVI可以比较好的解决以上痛点,它主要有以下优势



  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯

  2. 使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. ViewModel通过ViewStateAction通信,通过浏览ViewStateAciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。


当然MVI也有一些缺点,比如



  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀

  2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销


软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。

但通过以上的分析与介绍,我相信使用MVI架构代替没有使用DataBindingMVVM是一个比较好的选择~


更多


关于MVI架构更佳实践,支持局部刷新,可参见: MVI 架构更佳实践:支持 LiveData 属性监听

关于MVI架构封装,优雅实现网络请求,可参见: MVI 架构封装:快速优雅地实现网络请求


项目地址


本文所有代码可见:github.com/shenzhen201…


作者:程序员江同学
链接:https://juejin.cn/post/7022624191723601928
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

把 VS Code 带到安卓 - Code FA

注意,本篇讨论的是不基于pc的 这个是9月份初弄出来的,自己一直在使用,一直没来得及分享,前段时间在b站看到了一个差不多的方案。 背景 vs code 大部分是由 ts 编写,上层 UI 可以运行在各个系统的浏览器中,但 vs code 基于 electr...
继续阅读 »

注意,本篇讨论的是不基于pc的
这个是9月份初弄出来的,自己一直在使用,一直没来得及分享,前段时间在b站看到了一个差不多的方案。



camera.jpg


背景


vs code 大部分是由 ts 编写,上层 UI 可以运行在各个系统的浏览器中,但 vs code 基于 electron 框架,这个框架提供了对 node 的支持,一些浏览器内核中的 js 引擎没有的 api,例如 I/O,系统内核的一些交互等。
而 code-server 则是解决了脱离 electron 的问题。
目前安卓上有一个叫 aid learing 的软件,自带 VS Code ,看了一下原理差不多,并不是 linux 图形界面打开的 VS Code,也是打开的 webview 连接本地的服务,但这个玩意占磁盘内存太高,整个下载安装完就干掉6个g。


客户端框架


客户端是用 Flutter 进行的开发,而这个框架的选用并不是为了跨端,仅仅是为了快速尝试,还有基础能力的使用。


实现方法分析


code-server 在 github 发布的版本中是有 arm64 架构的,整个下载后,开终端解压执行就挂了,这个虽然是 arm64,并且带有一个 arm64 的 node,但是是为完整 linux 准备的。也就是说,node 中硬编码了 /usr /lib 等这些路径,并且附带的 node_modules 中也有大量的使用到 linux 特有节点的路径,这些安卓上都没有。
后来一想,termux 中自带的环境也是有 libllvm gcc nodejs 的,把整个 node_mudules 一删,再手动 install 一下,就行了。
所以整个流程大致分为两类。


初始尝试方案:非完整Linux



  1. 启动 termux 环境

  2. 安装 node,python,libllvm,clang

  3. 下载 code-server arm64,解压

  4. 处理兼容,删除 node_modules ,重新 yarn install

  5. 执行 bin/code-server 启动服务


经过一些测试发现,这种模式有一些问题。



  • 下载的依赖太多,由于源都在我的个人服务器,会下很久。

  • 编译太久,yarn install 的时候调用了 gcc 的编译,整个过程特别耗时。

  • 启动的 vs code 用不了搜索代码(正常情况能支持这个功能)

  • 磁盘占用太大,一阵操作下来,直接1.6g磁盘空间给干没了,主要是 npm install 拉了很多东西,还生成了一堆缓存,node_modules 嘛,比黑洞还重的东西。


不过按照以上的流程过一遍后,code-server 内的 node_modules 已经是安卓 arm64 可用的模块了,二次打包 code-server,流程就可以简化成如下



  1. 启动 termux 环境

  2. 安装 node

  3. 下载 code-server arm64,解压

  4. 执行 bin/code-server


但还是会存在编辑器无法搜索代码的 bug,node 虽然只有 20m ,但还是在个人服务器,下行带宽 5mb,大概 700kb/s ,emmm,要集成到 apk 内的话,得集成 deb ,调 dpkg 去安装,放弃。


最后使用方案:完整Linux



  1. 启动 termux 环境

  2. 下载并安装完整 Linux(30m)

  3. 下载 code-server arm64(自带node能用了)

  4. 执行 bin/code-server 启动服务


最终是选用了完整 Linux 的方式,除了安装需要的体积更小之外,还有完整源的支持,异常 bug 的避免等。
由于整个 VS Code 的启动需要的 130mb 的内存都是第一次打开需要的,所以将这些内存的占用放到服务器上,由 app 启动再下载的意义并不大,最后就全都作为资源文件集成到了 apk 内。


具体实现


启动 termux 环境


这个过程之前有现成的轮子了,只需要按照 termux-package 的编译脚本编译一个 bootstrap 集成到 apk,app 启动进行解压,然后根据符号链接格式进行恢复就行。
终端是 termare_view



bootstrap 是一个带有最小依赖的类 linux 环境,有bash,apt 等。



具体实现代码


function initApp(){
cd ${RuntimeEnvir.usrPath}/
echo 准备符号链接...
for line in `cat SYMLINKS.txt`
do
OLD_IFS="\$IFS"
IFS="←"
arr=(\$line)
IFS="\$OLD_IFS"
ln -s \${arr[0]} \${arr[3]}
done
rm -rf SYMLINKS.txt
TMPDIR=/data/data/com.nightmare.termare/files/usr/tmp
filename=bootstrap
rm -rf "\$TMPDIR/\$filename*"
rm -rf "\$TMPDIR/*"
chmod -R 0777 ${RuntimeEnvir.binPath}/*
chmod -R 0777 ${RuntimeEnvir.usrPath}/lib/* 2>/dev/null
chmod -R 0777 ${RuntimeEnvir.usrPath}/libexec/* 2>/dev/null
apt update
rm -rf $lockFile
export LD_PRELOAD=${RuntimeEnvir.usrPath}/lib/libtermux-exec.so
install_vs_code
start_vs_code
bash
}


RuntimeEnvir.usrPath 是 /data/data/$package/files/usr/bin



安装完整 Linux 和 code-server


这个我从好几个方案进行了筛选,起初用的 atlio 这个开源,整个开源依赖 python,并且有一个requirement.txt ,需要执行 python -r requirement.txt,依赖就是一大堆,后来换了 proot-distro,纯 shell,所以只需要直接集成到 apk 内就行。


1.安装 ubuntu


install_ubuntu(){
cd ~
colorEcho - 安装Ubuntu Linux
unzip proot-distro.zip >/dev/null
#cd ~/proot-distro
bash ./install.sh
apt-get install -y proot
proot-distro install ubuntu
echo '$source' > $ubuntuPath/etc/apt/sources.list
}

2.安装 code-server


install_vs_code(){
if [ ! -d "$ubuntuPath/home/code-server-$version-linux-arm64" ];then
cd $ubuntuPath/home
colorEcho - 解压 Vs Code Arm64
tar zxvf ~/code-server-$version-linux-arm64.tar.gz >/dev/null
cd code-server-$version-linux-arm64
fi
}

启动 code-server


直接用 proot-distro 启动就行,非常方便



--termux-home 参数:开启 app 沙盒的 home 挂载到 ubuntu 的 /root 下,这样 ubuntu 就能用 app 里面的文件夹了。



start_vs_code(){
install_vs_code
mkdir -p $ubuntuPath/root/.config/code-server 2>/dev/null
echo '
bind-addr: 0.0.0.0:8080
auth: none
password: none
cert: false
' > $ubuntuPath/root/.config/code-server/config.yaml
echo -e "\x1b[31m- 启动中..\x1b[0m"
proot-distro login ubuntu -- /home/code-server-$version-linux-arm64/bin/code-server
}

其实整个实现其实是没啥难度的,全都是一些 shell 脚本,也是得益于之前的 Termare 系列的支持,有兴趣的可以看下这个组织。
然后就是打开 webview 的过程了,如果觉得性能不好,你可以用局域网的电脑来进行连接。
看一下非首次的启动过程


WebView 实现方案


首先去 pub 看了一下 webview 的插件,官方目前正在维护的 webview 有这样的提示




  • Hybrid composition mode has a built-in keyboard support while Virtual displays mode has multiple keyboard issues

  • Hybrid composition mode requires Android SKD 19+ while Virtual displays mode requires Android SDK 20+

  • Hybrid composition mode has performence limitations when working on Android versions prior to Android 10 while Virtual displays is performant on all supported Android versions



也就是说开启 hybird 后,安卓10以下有性能限制,而使用虚拟显示器的话,键盘问题会很多。


实际尝试的时候,OTG 连接的键盘基本是没法用的。


再分析了下这个场景,最后还是用的原生 WebView,这里有些小坑。


必须启用项


        WebSettings mWebSettings = mWebView.getSettings();
//允许使用JS
mWebSettings.setJavaScriptEnabled(true);
mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);
mWebSettings.setUseWideViewPort(true);
mWebSettings.setAllowFileAccess(true);
// 下面这行不写不得行
mWebSettings.setDomStorageEnabled(true);
mWebSettings.setDatabaseEnabled(true);
mWebSettings.setAppCacheEnabled(true);
mWebSettings.setLoadWithOverviewMode(true);
mWebSettings.setDefaultTextEncodingName("utf-8");
mWebSettings.setLoadsImagesAutomatically(true);
mWebSettings.setSupportMultipleWindows(true);

路由重定向


有些场景 VS Code 会打开一个新的窗口,例如点击 file -> new window 的时候,不做处理,webview 会调起系统的浏览器。


        //系统默认会通过手机浏览器打开网页,为了能够直接通过WebView显示网页,必须设置
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//使用WebView加载显示url
view.loadUrl(url);
//返回true
return true;
}
});

浏览器正常跳转


例如终端输出了 xxx.xxx,ctrl + 鼠标点击,预期是会打开浏览器的。



mWebView.setWebChromeClient(webChromeClient);
WebChromeClient webChromeClient = new WebChromeClient() {

@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
WebView childView = new WebView(context);//Parent WebView cannot host it's own popup window.
childView.setBackgroundColor(Color.GREEN);
childView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
});
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(childView);//setWebView和getWebView两个方法
resultMsg.sendToTarget();
return true;
}
};

可行性探索


这个能干嘛?安卓屏幕那么小,电脑能本地用 VsCode 干嘛要连安卓的?



  • 有一个 vs code 加一个完整的 linux 环境,能 cover 住一些场景的开发了,安卓开发等除外。

  • 开发程序到 arm 板子的同学,PC 上还得弄一堆交叉编译工具链,并且每次编译调试过程也很繁琐,现在就能本地写本地编译。


正巧,买了一个平板,爱奇艺之余,也能作为程序员的一波生产力了。


screenshot.jpg


编译 C 语言


选了一个一直在学习的项目,scrcpy,一堆 c 源码,最后很顺利的编译下来了。
build_scrcpy.jpg


Web 开发


移动端的网页调试一直都是问题,作为野路子前端的我也很无奈,一般会加一些 vconsole 的组件来获取调试日志。



之前个人项目速享适配移动端 web 就是这么干的



现在,我们可以本地开发,本地调试,有 node 整个前端大部分项目都能拉下来了,真实的移动端物理环境。
试试
out.gif


写博客


本篇文章完全是在这个安卓版的 VS Code 中完成的,使用 hexo 本地调式


blog.jpg


写文档


docs.jpg


docs_code.jpg


写后台,接口测试


写一点简单的后台,如 python 的 fastapi,flask,并通过 rest client 进行接口测试


rest_client.jpg


最后


为了让其他的用户能直接使用到这个 app,我将其上架到了酷安。


看了下 vscodium 和 code-server 的开源协议都是 MIT,如果有侵权的地方辛苦评论区提醒一下鄙人。


Code FA 酷安下载地址


Code FA 个人服务器下载地址


个人软件快捷下载地址


开源地址


随便玩,有问题评论区留言,觉得不错的给个 star,文章不错的给个赞,🤔


其实还想尝试下 Flutter for web 的,折腾了半天还是失败了,能写代码,能有提示,编译会引发 dart runtime 的 crash。


作者:梦魇兽
链接:https://juejin.cn/post/7032548034638675982
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

抽丝剥茧聊Kotlin协程之深入理解协程上下文CoroutineContext

1. 前言如果你对CoroutineContext不了解,本文值得你细细品读,如果一遍看不懂,不妨多读几遍。写作该文的过程也是我对CoroutineContext理解加深的过程。CoroutineContext是协程的基础,值得投入学习Android开发者对C...
继续阅读 »

1. 前言

如果你对CoroutineContext不了解,本文值得你细细品读,如果一遍看不懂,不妨多读几遍。写作该文的过程也是我对CoroutineContext理解加深的过程。CoroutineContext是协程的基础,值得投入学习

Android开发者对Context都不陌生。在Android系统中,Context可谓神通广大,它可以获取应用资源,可以获取系统资源,可以启动Activity。Context有几个大名鼎鼎的子类,Activity、Application、Service,它们都是应用中非常重要的组件。

协程中也有个类似的概念,CoroutineContext。它是协程中的上下文,通过它我们可以控制协程在哪个线程中执行,可以设置协程的名字,可以用它来捕获协程抛出的异常等。

我们知道,通过CoroutineScope.launch方法可以启动一个协程。该方法第一个参数的类型就是CoroutineContext。默认值是EmptyCoroutineContext单例对象。 

在开始讲解CoroutineContext之前我们来看一段协程中经常会遇到的代码 

刚开始学协程的时候,我们经常会和Dispatchers.Main、Job、CoroutineName、CoroutineExceptionHandler打交道,它们都是CoroutineContext的子类。我们也很容易单独理解它们,Dispatchers.Main指把协程分发到主线程执行,Job可以管理协程的生命周期,CoroutineName可以设置协程的名字,CoroutineExceptionHandler可以捕获协程的异常。但是+操作符对大部分的Java开发者甚至Kotlin开发者而言会感觉到新鲜又难懂,在协程中CoroutineContext+到底是什么意思?

其实+操作符就是把两个CoroutineContext合并成一个链表,后文会详细讲解

2. CoroutineContext类图一览

根据类图结构我们可以把它分成四个层级:

  1. CoroutineContext 协程中所有上下文相关类的父接口。
  2. CombinedContext、Element、EmptyCoroutineContext。它们是CoroutineContext的直接子类。
  3. AbstractCoroutineContextElement、Job。这两个是Element的直接子类。
  4. CoroutineName、CoroutineExceptionHandler、CoroutineDispatcher(包含Dispatchers.Main和Dispatchers.Default)。它们是AbstractCoroutineContextElement的直接子类。

图中红框处,CombinedContext定义了size()和contains()方法,这与集合操作很像,CombinedContext是CoroutineContext对象的集合,而Element和EmptyCoroutineContext却没有定义这些方法,真正实现了集合操作的协程上下文只有CombinedContext,后文会详细讲解

3. CoroutineContext接口

CoroutineContext源码如下:  首先我们看下官方注释,我将它的作用归纳为:

Persistent context for the coroutine. It is an indexed set of [Element] instances. An indexed set is a mix between a set and a map. Every element in this set has a unique [Key].

  1. CoroutineContext是协程的上下文。
  2. CoroutineContext是element的set集合,没有重复类型的element对象。
  3. 集合中的每个element都有唯一的Key,Key可以用来检索元素。

相信大多数的人看到这样的解释时,都会心生疑惑,既然是set类型为啥不直接用HashSet来保存Element。CoroutineContext的实现原理又是什么呢?原因是考虑到协程嵌套,用链表实现更好。

接着我们来看下该接口定义的几个方法 

4. Key接口

Key是一个接口定义在CoroutineContext中的一个接口,作为接口它没有声明任何的方法,那么其实它没有任何真正有用的意义,它只是用来检索。我们先来看下,协程库中是如何使用Key接口的。  通过观察协程官方库中的例子,我们发现Element的子类都必须重写Key这个属性,而且Key的泛型类型必须和类名相同。以CoroutineName为例,Key是一个伴生对象,同时Key的泛型类型也是CoroutineName。

为了方便理解,我仿照写了MyElement类,如下:

通过对比kt类和反编译的java类我们看到 Key就是一个静态变量,而且它的实现类,其实啥也没干。它的作用与HashMap中的Key类似:

  1. 实现key-value功能,为插入和删除提供检索功能
  2. Key是static静态变量,全局唯一,为Element提供唯一性保障

Kotlin语法糖

coroutineContext.get(CoroutineName.Key)

coroutineContext.get(CoroutineName)

coroutineContext[CoroutineName]

coroutineContext[CoroutineName.Key]

写法是等价的

5. CoroutineContext.get方法

源码(整理在一起,下同) 

使用方式 

讲解

通过Key检索Element。返回值只能是Element或者null,链表节点中的元素值。

  1. Element get方法:只要Key与当前Element的Key匹配上了,返回该Element否则返回null。
  2. CombinedContext get方法:遍历链表,查询与Key相等的Element,如果没找到返回null。

6. CoroutineContext.plus方法

源码 

使用方式 

讲解

将两个CoroutineContext组合成一个CoroutineContext,如果是两个类型相同的Element会返回一个新的Element。如果是两个不同类型的Element会返回一个CombinedContext。如果是多个不同类型的Element会返回一条CombinedContext链表。

我将上述算法总结成了5种场景,不过在介绍这5种场景前,我们先讲解CombinedContext的数据结构。

7. CombinedContext分析

因为CombinedContext是CoroutineContext的子类,left也是CoroutineContext类型的,所以它的数据结构是链表。我们经常用next来表示链表的下一个节点。那么为什么这里取名叫left呢?我甚至怀疑写这段代码的是个左撇子。真正的原因是,协程可以启动子协程,子协程又可以启动孙协程。父协程在左边,子协程在右边

嵌套启动协程  越是外层的协程的Context越在左边,大概示意图如下 (真实并非如此,比这更复杂) 

链表的两个知识点在此都有体现。CoroutineContext.plus方法中使用的是头插法。CombinedContext的toString方法采用的是链表倒序打印法。

8. 五种plus场景

根据plus源码,我总结出会覆盖到五种场景。 

  1. plus EmptyCoroutineContext
  2. plus 相同类型的Element
  3. plus方法的调用方没有Dispatcher相关的Element
  4. plus方法的调用方只有Dispatcher相关的Element
  5. plus方法的调用方是包含Dispatcher相关Element的链表

结果如下:

  1. Dispatchers.Main + EmptyCoroutineContext 结果:Dispatchers.Main
  2. CoroutineName("c1") + CoroutineName("c2")结果: CoroutineName("c2")。相同类型的直接替换掉。
  3. CoroutineName("c1") + Job()结果:CoroutineName("c1") <- Job。头插法被plus的(Job)放在链表头部
  4. Dispatchers.Main + Job()结果:Job <- Dispatchers.Main。虽然是头插法,但是ContinuationInterceptor必须在链表头部。
  5. Dispatchers.Main + Job() + CoroutineName("c5")结果:Job <- CoroutineName("c5") <- Dispatchers.Main。Dispatchers.Main在链表头部,其它的采用头插法。

如果不考虑Dispatchers.Main的情况。我们可以把+<-代替。CoroutineName("c1") + Job()等价于CoroutineName("c1") <- Job

9. CoroutineContext的minusKey方法

源码 

讲解

  1. Element minusKey方法:如果Key与当前element的Key相等,返回EmptyCoroutineContext,否则相当于没减成功,返回当前element
  2. CombinedContext minusKey方法:删除链表中符合条件的节点,分三种情况。

三种情况以下面链表为例

Job <- CoroutineName("c5") <-Dispatchers.Main

  1. 没找到节点:minusKey(MyElement)。在Job节点处走newLeft === left分支,依此类推,在CoroutineName处走同样的分支,在Dispatchers.Main处走同样的分支。

  2. 节点在尾部:minusKey(Job)。在CoroutineName("c5")节点走newLeft === EmptyCoroutineContext分支,依此往头部递归

  3. 节点不在尾部:minusKey(CoroutineName)。在Dispatchers.Main节点处走else分支

10. 总结

学习CoroutineContext首先要搞清楚各类之间的继承关系,其次,CombinedContext各具体Element的集合,它的数据结构是链表,如果读者对链表增删改查操作熟悉的话,那么很容易就能搞懂CoroutineContext原理,否则想要搞懂CoroutineContext那简直如盲人摸象。


作者:字节小站
链接:https://juejin.cn/post/7037256376887803912
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

flutter 项目中json转dart模型

前言 实际开发项目中,后端返回的数据大多数是json格式,例如上一篇 Flutter 自定义数据选择器 中显示的数据都是dart模型的数据,由于dart本身数据格式并没有json格式,所以需要转化成dart模型数据。 解决方案 1. 小项目手动序列化 dart...
继续阅读 »

前言


实际开发项目中,后端返回的数据大多数是json格式,例如上一篇 Flutter 自定义数据选择器 中显示的数据都是dart模型的数据,由于dart本身数据格式并没有json格式,所以需要转化成dart模型数据。


解决方案


1. 小项目手动序列化


dart:convert 中内置的JSON解码器 它将原始JSON字符串传递给JSON.decode() 方法,然后在返回的Map<String, dynamic>中查找所需的值。 它没有外部依赖或其它的设置,对于小项目很方便


2.对于大中型项目 使用 json to Dart 网站生成model类


3. 使用插件 json_serializable package


实际应用


以方案1 和方案2 在实际项目中展示,还是以上一篇的[Flutter 自定义数据选择器] 为例


由于json to Dart 这个转换已经支持空安全,所以如果需要改动


pubspec.yaml文件


environment:
sdk: ">=2.12.0 <3.0.0" #如果需要支持空安全,只需要这里改一下版本 >=2.7.0 改成2.12.0

执行:


flutter clean

flutter pub get

area-data-jsonString.dart 文件


定义一个json 字符串数据(由于dart 没有json数据类型, 本地模拟数据就用json数据组装成 字符串格式,'''XXX ''' 包裹起来, 注意是 三个单引号)


image.png


进入 json to Dart
粘贴 ,定义类型


image.png


转化的模型数据如下:


area-data-json.dart


class AreaDataToJson {
String? id;
String? name;
String? center;
int? level;
List<Children>? children;

AreaDataToJson({this.id, this.name, this.center, this.level, this.children});

AreaDataToJson.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
center = json['center'];
level = json['level'];
if (json['children'] != null) {
children = <Children>[];
json['children'].forEach((v) {
children!.add(new Children.fromJson(v));
});
}
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['name'] = this.name;
data['center'] = this.center;
data['level'] = this.level;
if (this.children != null) {
data['children'] = this.children!.map((v) => v.toJson()).toList();
}
return data;
}
}

class Children {
String? id;
String? name;
String? center;
int? level;

Children({this.id, this.name, this.center, this.level});

Children.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
center = json['center'];
level = json['level'];
}

Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['name'] = this.name;
data['center'] = this.center;
data['level'] = this.level;
return data;
}
}

仔细对比发现这里生成数据有问题,省市区是有三层数据,这里只有两层(chilren下面下面可能还存在一层,这里后面再研究一下)


custom-pick.dart 这里就不贴出来,跟上一篇[Flutter 自定义数据选择器] 一样,去上面查看或者查看github源码


如下调用:
json-picker-page.dart



import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:personal_app/components/custom-picker.dart';
import 'package:personal_app/data/area-data-jsonString.dart';
import 'package:personal_app/Modal/area-data-json.dart';

class JsonPickerPage extends StatefulWidget {

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

class _JsonPickerPageState extends State<JsonPickerPage> {
// 方案一
List<dynamic> provAreaDataIdxs = [];//省市区的选择数据下标集合
String provAreaName = '';// 省市区名称
dynamic provAreaValue; //省市区选的数据id
List<Map<String, dynamic>> resetAreaData = []; //省市区数据转换后的数据

// 方案二
List<dynamic> provAreaDataIdxs2 = [];//省市区的选择数据下标集合
String provAreaName2 = '';// 省市区名称
dynamic provAreaValue2; //省市区选的数据id
List<Map<String, dynamic>> resetAreaData2 = []; //省市区数据转换后的数据


@override
void initState() {
//方式1,因为数据是本地的数据,dart本身是没有dart数据格式,
// 所以在定义json数据格式时,需要加上'''XXX''' 先包裹成字符串
//所以需要解析成List ,真实的接口请求不需要这么处理
List<dynamic> AreaDataJson = json.decode(areaDataJsonSting);//Json 改成小写,升级原因
//需要注意的是 areaData 数据字段不是label 和value; 需要转化一下
resetAreaData =recursionDataHandle(AreaDataJson);

//方式 2
List<dynamic> AreaDataJson2 = json.decode(areaDataJsonSting);
List<dynamic> AreaDataDart2 = AreaDataJson2.map((item) =>
new AreaDataToJson.fromJson(item)).toList();
print(AreaDataDart2.toString()); //这里打印才发现AreaDataToJson类有问题,该网站不能对于这种树形结构的数据生成还存在问题,下面放开2层 即 省市是没问题的

// // List<Map<String, dynamic>> AreaDataDart = AreaDataToJson.fromJson(AreaDataJson);
//需要注意的是 areaData 数据字段不是label 和value; 需要转化一下
resetAreaData2 =recursionDataHandle(AreaDataDart2);
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("自定义数据选择器"),
),
body: Center(
child: GestureDetector(
onTap: (){
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
customPicker(context,
{"indexs":provAreaDataIdxs, "initData": resetAreaData, "colNum":3},
(opt) {
setState(() {
provAreaDataIdxs = opt['indexs'];
List names = opt['names'];
provAreaName = '';
for(int i = 0; i< names.length; i++) {
provAreaName += names[i]['label'] != '' ? i== 0 ? names[i]['label'] : '/' + names[i]['label'] : '';
}
provAreaValue = names[names.length-1]['value'];//value 这里逻辑只需要取最后一个
});
});
},
child: Row(
children: [
Text('省市区选择方案1'),

],
),
),
Text(provAreaName),
Container(
height: 26.0,
),
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
customPicker(context,
{"indexs":provAreaDataIdxs2, "initData": resetAreaData2, "colNum":2},
(opt) {
setState(() {
provAreaDataIdxs2 = opt['indexs'];
List names = opt['names'];
provAreaName2 = '';
for(int i = 0; i< names.length; i++) {
provAreaName2 += names[i]['label'] != '' ? i== 0 ? names[i]['label'] : '/' + names[i]['label'] : '';
}
provAreaValue2 = names[names.length-1]['value'];//value 这里逻辑只需要取最后一个
});
});
},
child: Row(
children: [
Text('省市区选择方案2'),
],
),
),
Text(provAreaName2),
Container(
height: 26.0,
),
]
),
),
),
);
}

//数据格式转换
recursionDataHandle(data) {
List<Map<String, dynamic>> resetData = [];
if(data?.length > 0) {
for (var i = 0; i <data?.length; i++) {
Map<String, dynamic> tmpData;
try {
tmpData = data?[i].toJson();
} catch(e) {
tmpData = data?[i];
}

resetData.add({
'value': tmpData['id'],
'label': tmpData['name'],
'center': tmpData['center'],
'level': tmpData['level'],
// 'children': data[i]['children'] ? recursionDataHandle(data[i]['children']): []
});
if(tmpData.containsKey('children')) { //是否包含key值children
if(tmpData['children']?.length > 0) {
resetData[i]['children'] = recursionDataHandle(tmpData['children']);
} else {
resetData[i]['children'] = [];
}
}
}
}
return resetData;
}
}

方式1 和方式2 分别对应着上面的方案1 和方案2


效果:


image.png


问题


需要研究一下 json to Dart 类似多层树形结构数据存在问题?


作者:杰森陈
链接:https://juejin.cn/post/7071939995233157134
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

为什么IDEA不推荐你使用@Autowired ?

@Autowired注解相信每个Spring开发者都不陌生了!在DD的Spring Boot基础教程和Spring Cloud基础教程中也都经常会出现。 但是当我们使用IDEA写代码的时候,经常会发现@Autowired注解下面是有小黄线的,我们把小鼠标悬停在...
继续阅读 »

@Autowired注解相信每个Spring开发者都不陌生了!在DD的Spring Boot基础教程Spring Cloud基础教程中也都经常会出现。


但是当我们使用IDEA写代码的时候,经常会发现@Autowired注解下面是有小黄线的,我们把小鼠标悬停在上面,可以看到这个如下图所示的警告信息:


IDEA警告:Field injection is not recommended


那么为什么IDEA会给出Field injection is not recommended这样的警告呢?


下面带着这样的问题,一起来全面的了解下Spring中的三种注入方式以及他们之间在各方面的优劣。


Spring中的三种依赖注入方式


Field Injection


@Autowired注解的一大使用场景就是Field Injection


具体形式如下:


@Controller
public class UserController {

@Autowired
private UserService userService;

}

这种注入方式通过Java的反射机制实现,所以private的成员也可以被注入具体的对象。


Constructor Injection


Constructor Injection是构造器注入,是我们日常最为推荐的一种使用方式。


具体形式如下:


@Controller
public class UserController {

private final UserService userService;

public UserController(UserService userService){
this.userService = userService;
}

}

这种注入方式很直接,通过对象构建的时候建立关系,所以这种方式对对象创建的顺序会有要求,当然Spring会为你搞定这样的先后顺序,除非你出现循环依赖,然后就会抛出异常。


Setter Injection


Setter Injection也会用到@Autowired注解,但使用方式与Field Injection有所不同,Field Injection是用在成员变量上,而Setter Injection的时候,是用在成员变量的Setter函数上。


具体形式如下:


@Controller
public class UserController {

private UserService userService;

@Autowired
public void setUserService(UserService userService){
this.userService = userService;
}
}

这种注入方式也很好理解,就是通过调用成员变量的set方法来注入想要使用的依赖对象。


三种依赖注入的对比


在知道了Spring提供的三种依赖注入方式之后,我们继续回到本文开头说到的问题:IDEA为什么不推荐使用Field Injection呢?


我们可以从多个开发测试的考察角度来对比一下它们之间的优劣:


可靠性


从对象构建过程和使用过程,看对象在各阶段的使用是否可靠来评判:



  • Field Injection:不可靠

  • Constructor Injection:可靠

  • Setter Injection:不可靠


由于构造函数有严格的构建顺序和不可变性,一旦构建就可用,且不会被更改。


可维护性


主要从更容易阅读、分析依赖关系的角度来评判:



  • Field Injection:差

  • Constructor Injection:好

  • Setter Injection:差


还是由于依赖关键的明确,从构造函数中可以显现的分析出依赖关系,对于我们如何去读懂关系和维护关系更友好。


可测试性


当在复杂依赖关系的情况下,考察程序是否更容易编写单元测试来评判



  • Field Injection:差

  • Constructor Injection:好

  • Setter Injection:好


Constructor InjectionSetter Injection的方式更容易Mock和注入对象,所以更容易实现单元测试。


灵活性


主要根据开发实现时候的编码灵活性来判断:



  • Field Injection:很灵活

  • Constructor Injection:不灵活

  • Setter Injection:很灵活


由于Constructor Injection对Bean的依赖关系设计有严格的顺序要求,所以这种注入方式不太灵活。相反Field InjectionSetter Injection就非常灵活,但也因为灵活带来了局面的混乱,也是一把双刃剑。


循环关系的检测


对于Bean之间是否存在循环依赖关系的检测能力:



  • Field Injection:不检测

  • Constructor Injection:自动检测

  • Setter Injection:不检测


性能表现


不同的注入方式,对性能的影响



  • Field Injection:启动快

  • Constructor Injection:启动慢

  • Setter Injection:启动快


主要影响就是启动时间,由于Constructor Injection有严格的顺序要求,所以会拉长启动时间。


所以,综合上面各方面的比较,可以获得如下表格:


三种依赖注入的对比


结果一目了然,Constructor Injection在很多方面都是优于其他两种方式的,所以Constructor Injection通常都是首选方案!


Setter Injection比起Field Injection来说,大部分都一样,但因为可测试性更好,所以当你要用@Autowired的时候,推荐使用Setter Injection的方式,这样IDEA也不会给出警告了。同时,也侧面也反映了,可测试性的重要地位啊!


总结


最后,对于今天的问题讨论,我们给出两个结论,方便大家记忆:



  1. 依赖注入的使用上,Constructor Injection是首选。

  2. 使用@Autowired注解的时候,要使用Setter Injection方式,这样代码更容易编写单元测试。


好了,今天的学习就到这里!如果您学习过程中如遇困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!


作者:程序猿DD
链接:https://juejin.cn/post/7026916446370267172
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

LeetCode 动态规划之杨辉三角

题目 杨辉三角 给定一个非负整数 numRows, 生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中,每个数是它左上方和右上方的数的和。 示例 1: 输入: numRows = 5 输出: [[1],[1,1],[1,2,1...
继续阅读 »

题目


杨辉三角

给定一个非负整数 numRows 生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。



示例 1:


输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

示例 2:


输入: numRows = 1
输出: [[1]]

提示:



  • 1 <= numRows <= 30


题解


解题分析


杨辉三角介绍:


杨辉三角,是二项式系数在三角形中的一种几何排列,南宋时期数学家杨辉在 1261年 所著《详解九章算法》中出现。

在欧洲,帕斯卡(1623-1662)在1654年发现这一数学规律,所以这个又叫做帕斯卡三角形。帕斯卡的发现比 杨辉 要迟393年,比 贾宪 迟600年。
杨辉三角是中国数学史上的一个伟大成就。


杨辉三角形的规律如题目中所示。


题解思路



  1. 定义一个集合存储结果集

  2. 定义一个两重循环,分别进行运算,row 数为 numRows, 列数的值为 0 -> i 其中 i 表示当前是第几行.

  3. 如果当前 row 的列是第一列或最后一个列的当前的值为 1。

  4. 当前行其他的列的其他数据, 等于 re.get(i-1).get(j-) + re.get(i -1).get(j) 之和


复杂度分析



  • 时间复杂度:O(num * numRow ^2)

  • 空间复杂度:O(1)


解题代码


题解代码如下(代码中有详细的注释说明):


class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> re = new ArrayList<>();
for (int i =0; i< numRows; i++) {
List<Integer> row = new ArrayList<>();
for (int j=0; j<=i; j++) {
// 行中的第一数据和最后一个数据的值为 1
if (j == 0 || j == i) {
row.add(1);
} else {
// 其他数据, 等于 re.get(i-1).get(j-) + re.get(i -1).get(j) 之和
row.add(re.get(i -1).get(j -1) + re.get(i -1).get(j));
}
}
re.add(row);
}
return re;
}
}

提交后反馈结果:


image.png


作者:心城以北
链接:https://juejin.cn/post/7073856300282560549
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

官方推荐 Flow 取代 LiveData,有必要吗?

前言 打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如Room,DataStore, Paging3,DataBinding 等都支持了Flow Google开发者账号最近也发布了几篇使用Flow的文章,比如:从 LiveDat...
继续阅读 »

前言


打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如RoomDataStore, Paging3,DataBinding 等都支持了Flow

Google开发者账号最近也发布了几篇使用Flow的文章,比如:从 LiveData 迁移到 Kotlin 数据流

看起来官方在大力推荐使用Flow取代LiveData,那么问题来了,有必要吗?

LiveData用得好好的,有必要再学Flow吗?本文主要回答这个问题,具体包括以下内容

1.LiveData有什么不足?

2.Flow介绍以及为什么会有Flow

3.SharedFlowStateFlow的介绍与它们之间的区别


本文具体目录如下所示:


1. LiveData有什么不足?


1.1 为什么引入LiveData?


要了解LiveData的不足,我们先了解下LiveData为什么被引入



LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样两者结合的优势就发挥出来了



可以看出,LiveData就是一个简单易用的,具备感知生命周期能力的观察者模式

它使用起来非常简单,这是它的优点,也是它的不足,因为它面对比较复杂的交互数据流场景时,处理起来比较麻烦


1.2 LiveData的不足


我们上文说过LiveData结构简单,但是不够强大,它有以下不足

1.LiveData只能在主线程更新数据

2.LiveData的操作符不够强大,在处理复杂数据流时有些捉襟见肘


关于LiveData只能在主线程更新数据,有的同学可能要问,不是有postValue吗?其实postValue也是需要切换到到主线程的,如下图所示:



这意味着当我们想要更新LiveData对象时,我们会经常更改线程(工作线程→主线程),如果在修改LiveData后又要切换回到工作线程那就更麻烦了,同时postValue可能会有丢数据的问题。


2. Flow介绍


Flow 就是 Kotlin 协程与响应式编程模型结合的产物,你会发现它与 RxJava 非常像,二者之间也有相互转换的 API,使用起来非常方便。


2.1 为什么引入Flow


为什么引入Flow,我们可以从Flow解决了什么问题的角度切入



  1. LiveData不支持线程切换,所有数据转换都将在主线程上完成,有时需要频繁更改线程,面对复杂数据流时处理起来比较麻烦

  2. RxJava又有些过于麻烦了,有许多让人傻傻分不清的操作符,入门门槛较高,同时需要自己处理生命周期,在生命周期结束时取消订阅


可以看出,Flow是介于LiveDataRxJava之间的一个解决方案,它有以下特点



  • Flow 支持线程切换、背压

  • Flow 入门的门槛很低,没有那么多傻傻分不清楚的操作符

  • 简单的数据转换与操作符,如 map 等等

  • 冷数据流,不消费则不生产数据,这一点与LiveData不同:LiveData的发送端并不依赖于接收端。

  • 属于kotlin协程的一部分,可以很好的与协程基础设施结合


关于Flow的使用,比较简单,有兴趣的同学可参阅文档:Flow文档


3. SharedFlow介绍


我们上面介绍过,Flow 是冷流,什么是冷流?



  • 冷流 :只有订阅者订阅时,才开始执行发射数据流的代码。并且冷流订阅者只能是一对一的关系,当有多个不同的订阅者时,消息是重新完整发送的。也就是说对冷流而言,有多个订阅者的时候,他们各自的事件是独立的。

  • 热流:无论有没有订阅者订阅,事件始终都会发生。当 热流有多个订阅者时,热流订阅者们的关系是一对多的关系,可以与多个订阅者共享信息。


3.1 为什么引入SharedFlow


上面其实已经说得很清楚了,冷流订阅者只能是一对一的关系,当我们要实现一个流,多个订阅者的需求时(这在开发中是很常见的),就需要热流

从命名上也很容易理解,SharedFlow即共享的Flow,可以实现一对多关系,SharedFlow是一种热流


3.2 SharedFlow的使用


我们来看看SharedFlow的构造函数


public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

其主要有3个参数

1.replay表示当新的订阅者Collect时,发送几个已经发送过的数据给它,默认为0,即默认新订阅者不会获取以前的数据

2.extraBufferCapacity表示减去replayMutableSharedFlow还缓存多少数据,默认为0

3.onBufferOverflow表示缓存策略,即缓冲区满了之后Flow如何处理,默认为挂起


简单使用如下:


//ViewModel
val sharedFlow=MutableSharedFlow<String>()

viewModelScope.launch{
sharedFlow.emit("Hello")
sharedFlow.emit("SharedFlow")
}

//Activity
lifecycleScope.launch{
viewMode.sharedFlow.collect {
print(it)
}
}

3.3 将冷流转化为SharedFlow


普通flow可使用shareIn扩展方法,转化成SharedFlow


    val sharedFlow by lazy {
flow<Int> {
//...
}.shareIn(viewModelScope, WhileSubscribed(500), 0)
}

shareIn主要也有三个参数:



@param scope 共享开始时所在的协程作用域范围

@param started 控制共享的开始和结束的策略

@param replay 状态流的重播个数



started 接受以下的三个值:

1.Lazily: 当首个订阅者出现时开始,在scope指定的作用域被结束时终止。

2.Eagerly: 立即开始,而在scope指定的作用域被结束时终止。

3.WhileSubscribed: 这种情况有些复杂,后面会详细讲解


对于那些只执行一次的操作,您可以使用Lazily或者Eagerly。然而,如果您需要观察其他的流,就应该使用WhileSubscribed来实现细微但又重要的优化工作


3.4 Whilesubscribed策略


WhileSubscribed策略会在没有收集器的情况下取消上游数据流,通过shareIn运算符创建的SharedFlow会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。

让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程。


public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)

如上所示,它支持两个参数:



  • 1.stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止).这个值非常有用,因为您可能并不想因为视图有几秒钟不再监听就结束上游流。这种情况非常常见——比如当用户旋转设备时,原来的视图会先被销毁,然后数秒钟内重建。

  • 2.replayExpirationMillis表示数据重播的过时时间,如果用户离开应用太久,此时您不想让用户看到陈旧的数据,你可以用到这个参数


4. StateFlow介绍


4.1 为什么引入StateFlow


我们前面刚刚看了SharedFlow,为什么又冒出个StateFlow?

StateFlowSharedFlow 的一个比较特殊的变种,StateFlowLiveData 是最接近的,因为:



  • 1.它始终是有值的。

  • 2.它的值是唯一的。

  • 3.它允许被多个观察者共用 (因此是共享的数据流)。

  • 4.它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。


可以看出,StateFlowLiveData是比较接近的,可以获取当前的值,可以想像之所以引入StateFlow就是为了替换LiveData

总结如下:

1.StateFlow继承于SharedFlow,是SharedFlow的一个特殊变种

2.StateFlowLiveData比较相近,相信之所以推出就是为了替换LiveData


4.2 StateFlow的简单使用


我们先来看看构造函数:


public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

1.StateFlow构造函数较为简单,只需要传入一个默认值

2.StateFlow本质上是一个replay为1,并且没有缓冲区的SharedFlow,因此第一次订阅时会先获得默认值

3.StateFlow仅在值已更新,并且值发生了变化时才会返回,即如果更新后的值没有变化,也没会回调Collect方法,这点与LiveData不同


SharedFlow类似,我们也可以用stateIn将普通流转化成StateFlow


val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)

shareIn类似,唯一不同的时需要传入一个默认值

同时之所以WhileSubscribed中传入了5000,是为了实现等待5秒后仍然没有订阅者存在就终止协程的功能,这个方法有以下功能



  • 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。

  • 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。

  • 订阅将被重启,新数据会填充进来,当数据可用时更新视图。

  • 在屏幕旋转时,因为重新订阅的时间在5s内,因此上游流不会中止


4.3 在页面中观察StateFlow


LiveData类似,我们也需要经常在页面中观察StateFlow

观察StateFlow需要在协程中,因此我们需要协程构建器,一般我们会使用下面几种



  1. lifecycleScope.launch : 立即启动协程,并且在本 ActivityFragment 销毁时结束协程。

  2. LaunchWhenStartedLaunchWhenResumed,它会在lifecycleOwner进入X状态之前一直等待,又在离开X状态时挂起协程




如上图所示:

1.使用launch是不安全的,在应用在后台时也会接收数据更新,可能会导致应用崩溃

2.使用launchWhenStartedlaunchWhenResumed会好一些,在后台时不会接收数据更新,但是,上游数据流会在应用后台运行期间保持活跃,因此可能浪费一定的资源


这么说来,我们使用WhileSubscribed进行的配置岂不是无效了吗?订阅者一直存在,只有页面关闭时才会取消订阅

官方推荐repeatOnLifecycle来构建协程

在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程,如下图所示。



比如在某个Fragment的代码中:


onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}

当这个Fragment处于STARTED状态时会开始收集流,并且在RESUMED状态时保持收集,最终在Fragment进入STOPPED状态时结束收集过程。

结合使用repeatOnLifecycle APIWhileSubscribed,可以帮助您的应用妥善利用设备资源的同时,发挥最佳性能


4.4 页面中观察Flow的最佳方式


通过ViewModel暴露数据,并在页面中获取的最佳方式是:



  • ✔️ 使用带超时参数的 WhileSubscribed 策略暴露 Flow示例 1

  • ✔️ 使用 repeatOnLifecycle 来收集数据更新。示例 2




最佳实践如上图所示,如果采用其他方式,上游数据流会被一直保持活跃,导致资源浪费

当然,如果您并不需要使用到Kotlin Flow的强大功能,就用LiveData好了 :)


5 StateFlowSharedFlow有什么区别?


从上文其实可以看出,StateFlowSharedFlow其实是挺像的,让人有些傻傻分不清,有时候也挺难选择该用哪个的


我们总结一下,它们的区别如下:



  1. SharedFlow配置更为灵活,支持配置replay,缓冲区大小等,StateFlowSharedFlow的特化版本,replay固定为1,缓冲区大小默认为0

  2. StateFlowLiveData类似,支持通过myFlow.value获取当前状态,如果有这个需求,必须使用StateFlow

  3. SharedFlow支持发出和收集重复值,而StateFlowvalue重复时,不会回调collect

  4. 对于新的订阅者,StateFlow只会重播当前最新值,SharedFlow可配置重播元素个数(默认为0,即不重播)


可以看出,StateFlow为我们做了一些默认的配置,在SharedFlow上添加了一些默认约束,这些配置可能并不符合我们的要求



  1. 它忽略重复的值,并且是不可配置的。这会带来一些问题,比如当往List中添加元素并更新时,StateFlow会认为是重复的值并忽略

  2. 它需要一个初始值,并且在开始订阅时会回调初始值,这有可能不是我们想要的

  3. 它默认是粘性的,新用户订阅会获得当前的最新值,而且是不可配置的,而SharedFlow可以修改replay


StateFlow施加在SharedFlow上的约束可能不是最适合您,如果不需要访问myFlow.value,并且享受SharedFlow的灵活性,可以选择考虑使用SharedFlow


总结


简单往往意味着不够强大,而强大又常常意味着复杂,两者往往不能兼得,软件开发过程中常常面临这种取舍。

LiveData的简单并不是它的缺点,而是它的特点。StateFlowSharedFlow更加强大,但是学习成本也显著的更高.

我们应该根据自己的需求合理选择组件的使用



  1. 如果你的数据流比较简单,不需要进行线程切换与复杂的数据变换,LiveData对你来说相信已经足够了

  2. 如果你的数据流比较复杂,需要切换线程等操作,不需要发送重复值,需要获取myFlow.valueStateFlow对你来说是个好的选择

  3. 如果你的数据流比较复杂,同时不需要获取myFlow.value,需要配置新用户订阅重播无素的个数,或者需要发送重复的值,可以考虑使用SharedFlow

作者:程序员江同学
链接:https://juejin.cn/post/6986265488275800072
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 线程间通信 - Java 层 - MessagePool

线程间通信在 Android 系统中应用十分广泛,本文是一个系列文章,主要梳理了Android Java层的线程间通信机制-Handler 还是以一个简单的Handler示例开头 public void egHandler() { Looper.pr...
继续阅读 »

线程间通信在 Android 系统中应用十分广泛,本文是一个系列文章,主要梳理了Android Java层的线程间通信机制-Handler




还是以一个简单的Handler示例开头


public void egHandler() {
Looper.prepare();
Looper.loop();

Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
// TODO; 处理 Event
}
};

new Thread() {
@Override
public void run() {
Message msg = Message.obtain(); // 注意: 此处和上篇文章中的示例不同
msg.what = 0;
handler.sendMessage(msg);
}
}.start();
}

为了避免重复的创建以及销毁 Message 对象带来的系统开销,Google 团队在Message.java中搞了一个MessagePool,在MessagePool中缓存了定量的Message对象


上面示例中使用的Message.obtain()是从Pool中拿到一个message对象。


MessagePool


源码路径:frameworks/base/core/java/android/os/Message.java


在Message.java中定义了四个成员变量


// MessagePool 同步锁
public static final Object sPoolSync = new Object();
// MessagePool Node节点
private static Message sPool;
// MessagePool Node个数
private static int sPoolSize = 0;
// MessagePool 最大容量
private static final int MAX_POOL_SIZE = 50;

从MessagePool中获取一个message对象,需要调用obtain方法,该方法重载了多个


obtain message


Message.obtain()


public static Message obtain() {
// 上锁
synchronized (sPoolSync) {
if (sPool != null) {
// 从sPool链表拿出头节点
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}

Message.obtain(Message orig)


public static Message obtain(Message orig) {
// 获取一个message
Message m = obtain();
// 将m和形参orig同步
m.what = orig.what;
m.arg1 = orig.arg1;
m.arg2 = orig.arg2;
m.obj = orig.obj;
m.replyTo = orig.replyTo;
m.sendingUid = orig.sendingUid;
m.workSourceUid = orig.workSourceUid;
if (orig.data != null) {
m.data = new Bundle(orig.data);
}
m.target = orig.target;
m.callback = orig.callback;

return m;
}

Message.obtain(Handler h)


public static Message obtain(Handler h) {
Message m = obtain();
// 单独为新msg设置handler
m.target = h;

return m;
}

另外还有一些构造方法,整体实现思路差不多。


recycle message


Message.recycle


public void recycle() {
if (isInUse()) {
// InUse中被释放会抛出异常
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
// 释放
recycleUnchecked();
}

Message.recycleUnchecked


@UnsupportedAppUsage
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
// 初始化flag,标记为InUse
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;

// 上锁,将需要释放的msg插入到链表头
synchronized (sPoolSync) {
// 注意这里,回收的过程中会判断当前链表的长度是否大于最大长度
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}

recycle与recycleUnchecked的区别在于:



  • recycleUnchecked 不会检查当前msg是否在使用中,而是会直接回收掉该msg,放在链表的头部

  • recycle 会判断当前的msg是否在使用,在使用中会抛出异常,非使用中直接调用recycleUnchecked进行回收


另外,recycleUnchecked不支持app是否,App中只能是否recycle


Message.isInUse


// Message 中两个成员变量
/*package*/ static final int FLAG_IN_USE = 1 << 0;

/** If set message is asynchronous */
/*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1;

/*package*/ boolean isInUse() {
return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
}

Message.markInUse


@UnsupportedAppUsage
/*package*/ void markInUse() {
flags |= FLAG_IN_USE;
}

这个flag的判断方式我有点不理解,还不如直接用数字判断,例如flag = 1就是InUse,难道是为了判断的更快一点采用|、&的方式?


理解为什么这么做的大佬在下面评论一下,不吝指出。


作者:lightingsui
链接:https://juejin.cn/post/7072737035357782023
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

❤️Android 快别用Toast了,来试试Snackbar❤️

🔥 应用场景 Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下: Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT); ...
继续阅读 »

🔥 应用场景


Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下:


        Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();

运行在在Android 12上无法显示,查看Logcat提示如下:


Toast: setGravity() shouldn't be called on text toasts, the values won't be used

意思就是:你不能使用toast调用setGravity,调用无效。哎呀,看给牛气的,咱看看源码找找原因


🔥 源码


💥 Toast.setGravity()


    /**
* 设置Toast出现在屏幕上的位置。
*
* 警告:从 Android R 开始,对于面向 API 级别 R 或更高级别的应用程序,此方法在文本 toast 上调用时无效。
*/
public void setGravity(int gravity, int xOffset, int yOffset) {
if (isSystemRenderedTextToast()) {
Log.e(TAG, "setGravity() shouldn't be called on text toasts, the values won't be used");
}
mTN.mGravity = gravity;
mTN.mX = xOffset;
mTN.mY = yOffset;
}

妥了,人家就告诉你了 版本>=Android R(30),调用该方法无效。无效就无效呗,还不给显示了,过分。


Logcat的提示居然是在这里提示的,来都来了,咱们看看isSystemRenderedTextToast()方法。


💥 Toast.isSystemRenderedTextToast()


    /**
*Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现,因此应用程序无法绕过后台自定义 Toast 限制。
*/
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
private static final long CHANGE_TEXT_TOASTS_IN_THE_SYSTEM = 147798919L;

private boolean isSystemRenderedTextToast() {
return Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM) && mNextView == null;
}

重点了。Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现。


清晰明了,可以这样玩,但是你级别不够,不给你玩。


事情整明白了,再想想解决解决方案。他说了Text Toast 将由 SystemUI 呈现,那我不用 Text 不就行了。


🔥 Toast 提供的方法


先看看Tast提供的方法:



有这几个方法。咱们实践一下。保险起见看看源码


💥 Toast.setView() 源码


    /**
* 设置显示的View
* @deprecated 自定义 Toast 视图已弃用。 应用程序可以使用 makeText 方法创建标准文本 toast,
* 或使用 Snackbar
*/
@Deprecated
public void setView(View view) {
mNextView = view;
}

这个更狠,直接弃用。




  • 要么老老实实的用默认的Toast。




  • 要么使用 Snackbar。




🔥 Snackbar


Snackbar 就是一个类似Toast的快速弹出消息提示的控件(我是刚知道,哈哈)。


与Toast相比:




  • 一次只能显示一个




  • 与用户交互



    • 在右侧设置按钮来添加事件,根据 Material Design 的设计原则,只显示 1 个按钮 (添加多个,以最后的为准)




  • 提供Snackbar显示和关闭的监听事件



    • BaseTransientBottomBar.addCallback(BaseCallback)




💥 代码实现


    showMessage(findViewById(android.R.id.content), str, Snackbar.LENGTH_INDEFINITE);

public static void showMessage(View view, String str, int length) {
Snackbar snackbar = Snackbar.make(view, str, length);

View snackbarView = snackbar.getView();
//设置布局居中
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(snackbarView.getLayoutParams().width, snackbarView.getLayoutParams().height);
params.gravity = Gravity.CENTER;
snackbarView.setLayoutParams(params);
//文字居中
TextView message = (TextView) snackbarView.findViewById(R.id.snackbar_text);
//View.setTextAlignment需要SDK>=17
message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
message.setGravity(Gravity.CENTER);
message.setMaxLines(1);
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
super.onDismissed(transientBottomBar, event);
//Snackbar关闭
}

@Override
public void onShown(Snackbar transientBottomBar) {
super.onShown(transientBottomBar);
//Snackbar显示
}
});
snackbar.setAction("取消", new View.OnClickListener() {
@Override
public void onClick(View v) {
//显示一个默认的Snackbar。
Snackbar.make(view, "我先走", BaseTransientBottomBar.LENGTH_LONG).show();
}
});
snackbar.show();
}

Snackbar.make的三个参数:



  • View:从View中找出当前窗口最外层视图,然后在其底部显示。

  • 第二个参数(text)

    • CharSequence

    • StringRes



  • duration(显示时长)

    • Snackbar.LENGTH_INDEFINITE 从 show()开始显示,直到它被关闭或显示另一个 Snackbar

    • Snackbar.LENGTH_SHORT 短时间

    • Snackbar.LENGTH_LONG 长时间

    • 自定义持续时间 以毫秒为单位




💥 效果


Android 12



Android 5.1



💥 工具类


如果觉得设置麻烦可以看看下面这边文章,然后整合一套适合自己的。


作者:Android帅次
链接:https://juejin.cn/post/7029216153469714445
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

装了这几个IDEA插件,基本上一站式开发了!

前言 前几天有社区小伙伴私聊我,问我都用哪些IDEA插件,我的IDEA的主题看起来不错。 作为一个开源作者,每周要code大量的代码,提升日常工作效率是我一直追求的,在众多的IDEA插件中,我独钟爱这几款。这期就整理一些我日常中经常使用的IDEA插件,这些插件...
继续阅读 »

前言


前几天有社区小伙伴私聊我,问我都用哪些IDEA插件,我的IDEA的主题看起来不错。


作为一个开源作者,每周要code大量的代码,提升日常工作效率是我一直追求的,在众多的IDEA插件中,我独钟爱这几款。这期就整理一些我日常中经常使用的IDEA插件,这些插件有些挺小众,但是的确非常提升效率,推荐给大家。


Vuesion Theme


首先推荐是一款皮肤,每天对着看IDEA,默认的皮肤黑白两色,我个人总觉得白色太刺眼,黑色的有点太黑了,代码高亮也不好看,长时间难免看着有点审美疲劳。


颜值是生产力的第一要素,主题整好了,整个心情也好一点,心情好,自然bug就少点。。。是这个道理么?


在众多的IDEA的主题中,我钟爱这一款。非常适中的UI颜色,漂亮的代码高亮主题。看了半年多了,都没有审美疲劳。



废话不多说,直接看代码主题效果:



我知道每个人审美有所不同,有的小伙伴会说,我就是喜欢默认的暗黑色。okay啦,我只代表个人喜好。这里不杠。


Atom Material ICons


第二款推荐的是一款ICON插件,相信也有很多小伙伴也有用。


其实这个Icon虽然不难看,但是我也没觉得多好看。那我为什么还要特意推荐??


因为这款ICon插件附加了一个buff。。。这是我当时如何也想不通的。😂



部分效果如下:




其实不难看,那我就要说说这个icon插件附带的buff了。


idea在macOs下,无论是我用2018款的Macbook pro还是现在的Macbook pro m1版本,总感觉在拖动滚动条或是鼠标中键滚屏时有点卡顿,并不是电脑性能的问题,我在网上看到有其他小伙伴也遇到了这种情况。应该是idea对MacOs系统的优化问题。


我尝试过增大Idea的jvm缓存,尝试过优化参数。都无果,后来偶然一次机会在某个论坛上看到有一个人说,装了这个Icon插件之后就变的丝滑无比了,但不知道为啥。我抱着怀疑的态度装了下,卧槽,瞬间丝滑了。虽然我也不懂这是为什么,但是解决问题了之后这个Icon插件就变成必备插件了。如果有小伙伴遇到我想同的问题的话,那么请尝试。


这个buff是不是很强大呢。


File Expander


有了这个插件,有些小伙伴平时用的Jad工具就可以扔了,它能在Idea里直接打开Jar包,并且反编译代码查看。甚至于能打开tar.gz,zip等压缩格式。


这里补充下,你项目里之所以不需要装插件就能看jar包里的代码,是因为jar在你的classpath内。如果单独打开一个jar包,不装插件是看不了的。





GitToolBox


这款插件现在我几乎离不开它。


他能在项目上提示你还有多少文件没提交,远程还有多少文件没更新下来。还能在每一行代码上提示上次提交的时间。查版本提交问题的时候尤其方便。





Maven Helper


这个我想应该是所有使用Idea开发者的标配插件了吧。


我经常使用到的功能便是可视化依赖书,可以清晰的知道,哪个Jar包传递依赖了什么,哪个jar包什么版本和什么版本冲突了。


排查Jar包依赖等问题用这个简直是神器。这个插件也提供了一些其他的快捷命令,右键直接唤起maven命令,颇为方便。




Translation


源码中很多注解都是英文,有时候看着有点费劲。这款翻译插件基本上与Idea一体化,从集成度和方便程度来说,可以吊打其他的第三方翻译软件了。不需要你切换窗口,直接一个快捷键就可以翻译整段文本了。


关键是这个插件的翻译引擎可以与多个翻译接口集成对接,支持google翻译,有道翻译,百度翻译,阿里翻译。实时进行精准快速的翻译,自动识别语言。帮助你在阅读源码里的英文时理解的更加透彻。





arthas idea


Arthas是阿里开源的一款强大的java在线诊断工具,做java开发的小伙伴一定很熟悉。


这个工具几乎已经成为诊断线上java应用的必备工具了。


但是每次需要输入很长一段命令,有些命令遗忘的话,还要去翻看Arthas的命令文档,然后还要复制代码中类或方法的全路径,很是不方便。而这款arthas的插件就可以让你完全摆脱这些苦恼。生产力大大提升。



使用起来非常方便,进入代码片段,选择你要诊断的类或者方法上面,右击打开Arthas命令,选择一项,即可自动生成命令,省去你敲打命令的时间。



Search In Repository


平时我们如果要依赖一个第三方jar包,但是不知道它的maven/gradle的坐标。我们该怎么做?


搓点的做法基本上就是baidu了,稍微高级点的就是到中央仓库去查下,最新版本的坐标是什么。然后复制下来,贴到pom里去。


这款插件,就无需你来回跳转,直接把中央仓库的查找集成到了Idea里面。你只需要打开这款插件,输入jar包的名字或者gav关键字,就能查到到这个jar包所有的版本,然后可以直接复制gav坐标。方便又快捷,干净又卫生!




VisualGC


不知道大家去诊断JVM堆栈用什么工具呢,是不是大部分都是用jdk的原生工具呢。


这里推荐大家一个Idea堆栈的可视化工具,和Idea深度集成。直接显示所有进程,双击即可打开JVM的堆栈可视化界面。堆栈和垃圾收集情况一目了然!




Zoolytic


一款zookeeper节点的查看分析插件。其实第三方也有一些zk的节点信息查看工具,但是我都觉得不够方便,直到我发现了这款插件。


idea里面直接可以看zookeeper的节点信息,非常方便。




最后


以上这10款Idea插件是我平时中用的非常多且经过筛选的,因为有些大家耳熟能详就不介绍了,相信小伙伴们都有装。


希望大家能从以上插件中找到适合自己的那几款,或者有更好更效率的插件,也可以评论里留言。


我是铂赛东,是开一个开源作者和内容博主,热爱生活和分享。如果你对我的内容感兴趣,请关注元人部落。


作者:铂赛东🐶
链接:https://juejin.cn/post/7028025194048405534
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

算法题每日一练:位运算

一、前言 学习目标: 掌握 原码 反码 补码 基本运算以及转换 熟练应用 与 或 非 同或 异或 应用 对于 移位运算 在题目中熟练应用,后面会出位运算的题目 二、概述 计算机最主要的功能是处理数值、文字、声音、图形图像等信息。 在计算机内部,各种信息都必...
继续阅读 »

一、前言


学习目标:



  • 掌握 原码 反码 补码 基本运算以及转换

  • 熟练应用 与 或 非 同或 异或 应用

  • 对于 移位运算 在题目中熟练应用,后面会出位运算的题目


二、概述


计算机最主要的功能是处理数值、文字、声音、图形图像等信息。


在计算机内部,各种信息都必须经过数字化编码后才能被传送、存储和处理,所有的数据以二进制的形式存储在设备中,即 0、1 这两种状态。


计算机对二进制数据进行的运算(+、-、*、/)都是叫位运算,例如下面的计算:


int a=74;
int b=58;
int c=a+b;

a 74 : 1 0 0 1 0 1 0
b 58 : 1 1 1 0 1 0
c 132 : 1 0 0 0 0 1 0 0

十进制数字转换成底层的二进制数字之后,二进制逐位相加,满2进1。


三、原码 反码 补码


1.原码


在计算机的运算中,计算机只能做加法,减法、乘除都没法计算。原码是最简单的机器数表示法,用最高位表示符号位,其他位存放该数的二进制的绝对值。


2.png


首位的0表示正数、1表示负数。


特点



  • 表示直观易懂,正负数区分清晰

  • 加减法运算复杂,要先判断符号是正号还是负号、相同还是相反


2.反码


正数的反码还是等于原码,负数的反码就是它的原码除符号位外,按位取反。


1.png


特点



  • 反码的表示范围与原码的表示范围相同

  • 反码表示在计算机中往往作为数码变换的中间环节


3.补码


正数的补码等于它的原码,负数的补码等于反码+1


3.png


特点:



  • 在计算机运算时,都是以补码的方式运算的,下面的位运算也是补码形式计算


四、基本运算


1.与


符号:&


运算规则:两个二进制位都为1时才为1,否则为0


示例:1001&1111=1001


2.或


符号:|


运算规则:两个二进制位都为0时才为0,否则为1


示例:1001&1100=1101


3.非


符号:~


运算规则:0变成1,1变成0


示例:~1001 = 0110


4.同或


符号:~


运算规则:数字相同时为1,相反为0


示例:1001~1100=1010


5.异或


符号:^


运算规则:两个二进制位相反为1,相同为0


示例:1001^0111=1110


五、移位运算


1.左移


符号:<<


运算规则:符号位不变,低位补0


示例


a<<b 代表十进制数字a向左移动b个进位
/* 左移:
* 左移1位,相当于原数值 * 2
* 左移2位,相当于原数值 * 4
* 左移n位,相当于原数值 * 2^n
*/
计算 10 << 1
10的补码:0000 1010
-----------------------
结果补码:0001 0100 ==> 正数,即 10*2=20

计算 10 << 2
10的补码:0000 1010
-----------------------
结果补码:0010 1000 ==> 正数,即 10*2^2=40

计算 10 << 3
10的补码:0000 1010
-----------------------
结果补码:0101 0000 ==> 正数,即 10*2^3=80

计算 12 << 1
12的补码:0000 1100
-----------------------
结果补码:0001 1000 ==> 正数,即 12*2=24

2.右移


符号:>>


运算规则:低位溢出,符号位不变,并用符号位补溢出的高位


示例


a>>b 代表十进制数字a向右移动b个进位
/* 右移:
* 右移1位,相当于原数值 / 2
* 右移2位,相当于原数值 / 4
* 右移3位,相当于原数值 / 2^n
* 结果没有小数(向下取整)
*/
计算 80 >> 1
80的补码:0101 0000
-----------------------
结果补码:0010 1000 ==> 正数,即 80/2=40

计算 80 >> 2
80的补码:0101 0000
-----------------------
结果补码:0001 01000 ==> 正数,即 80/2^2=20

计算 80 >> 3
80的补码:0101 0000
-----------------------
结果补码:0000 1010 ==> 正数,即 80/2^3=10

计算 24 >> 1
12的补码:0001 1000
-----------------------
结果补码:0000 1100 ==> 正数,即 24/2=12

作者:知心宝贝
链接:https://juejin.cn/post/7072141949787963400
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

?Flutter 那些花里胡哨的底部菜单? 进来绝不后悔

前言 前段时间,学习到了Flutter动画,正愁不知道写个项目练习巩固,突然有一天产品在群里发了一个链接【ios中的动画标签】(下面有例图),我心里直呼"好家伙",要是产品都要求做成这样,产品经理和程序员又又又又又又得打起来! 还好只是让我们参考,刚好可以拿来...
继续阅读 »

前言


前段时间,学习到了Flutter动画,正愁不知道写个项目练习巩固,突然有一天产品在群里发了一个链接【ios中的动画标签】(下面有例图),我心里直呼"好家伙",要是产品都要求做成这样,产品经理和程序员又又又又又又得打起来! 还好只是让我们参考,刚好可以拿来练习。


GitHub地址:github.com/longer96/fl…


t01.png


我们每天都会看到底部导航菜单,它们在应用程序内引导用户,允许他们在不同的tag之间快速切换。但是谁说切换标签就应该很无聊?
让我们一起探索标签栏中有趣的动画。虽然你在应用程序中可能不会使用到,但看看它的实现可能会给你提供一些灵感、设计参考。


如果恰好能给你带来一点点帮助,那是再好不过啦~ 路过的帅逼帮忙点个 star


先上几张花里胡哨的底部菜单 参考图


s01.gif


s04.gif


s03.gif


s02.gif


效果分析


咳咳,有的动效确实挺难的,需要设计师的鼎力支持,我只好选软的柿子捏


p00.png


首先我们观察,它是由文字和指示器组成的。点击之后指示器切换,文字缩放。



  • 每个tag 均分了屏幕宽度

  • 点击之后,指示器从之前的tag中部位置拉长到选中tag的中部位置

  • 指示器到达选中tag之后,长度立马向选中tag位置收缩


稍微复杂一点的是指示器的动画,看上去有3个变量:左边距、右边距、指示器宽度。
但变量越多,越不方便控制,细心想一下 我们发现其实只需要控制: 左、右边距就可以了,指示器宽度设置成自适应(或者只控制左边距和指示器宽度)


实现效果


p11.gif


其实很多类似底部菜单都可以如法炮制,指示器位于tag后面,根据不同的条件调整位置和尺寸。


d00.gif


d01.gif


d02.gif


实现一款底部菜单


常见的还有另一种展开类似的菜单,比如这样
x00.gif


咱们还是先简单分析一下



  • 由一个按钮、多个tag按钮组成

  • 点击之后,tag呈扇状展开或收缩


看上去只有2步,还是很简单的嘛


第一步:我们用帧布局叠放按钮和tag


Stack(
children: [
// tag菜单

// 菜单/关闭 按钮
]
)

第二步:管理好tag的位置
简单介绍一下Flow,Flutter中Flow是一个对子组件尺寸以及位置调整非常高效的控件。



Flow用转换矩阵在对子组件进行位置调整的时候进行了优化:在Flow定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate中的paintChildren()方法中调用context.paintChild 进行重绘,而context.paintChild在重绘时使用了转换矩阵,并没有实际调整组件位置。



使用起来也很简单,只需要实现FlowDelegate的paintChildren()方法,就可以自定义布局策略。所以我们需要计算好每一个tag的轨迹位置。


经过你的细心观察,你发现tag的轨迹呈半圆状展开,对 没错 就是需要翻出三角函数


sjhs.jpg


f00.png


经过你的又一次细心观察,你发现有5个tag,半圆实际可以放7个,但是为了有更好的显示效果,可以将需要展示的tag放在中间位置(过滤掉第一个和最后一个)


所以我们可以列出简单的计算


final total = context.childCount + 1;

for (int i = 0; i < childCount; i++) {
x = cos(pi * (total - i - 1) / total) * Radius;
y = sin(pi * (total - i - 1) / total) * Radius;
}

你发现太规整的圆其实并不是那么好看,优化一下



  • 将x轴半径设置为 父级约束宽度的一半

  • 将Y轴半径设置为 父级约束高度

  • 给动画加上曲线,让tag有类似回弹效果

  • 注意y轴得转换为负数,因为我们的坐标点位于下方


a003.gif


微调一下,好啦 恭喜你!
3句代码,让产品经理给你点了18杯茶


b001.png


class FlowAnimatedCircle extends FlowDelegate {
final Animation<double> animation;

/// icon 尺寸
final double iconSize = 48.0;

/// 菜单左右边距
final paddingHorizontal = 8.0;

FlowAnimatedCircle(this.animation) : super(repaint: animation);

@override
void paintChildren(FlowPaintingContext context) {
// 进度等于0,也就是收起来的时候不绘制
final progress = animation.value;
if (progress == 0) return;

final xRadius = context.size.width / 2 - paddingHorizontal;
final yRadius = context.size.height - iconSize;

// 开始(0,0)在父组件的中心
double x = 0;
double y = 0;

final total = context.childCount + 1;

for (int i = 0; i < context.childCount; i++) {
x = progress * cos(pi * (total - i - 1) / total) * xRadius;
y = progress * sin(pi * (total - i - 1) / total) * yRadius;

// 使用Matrix定位每个子组件
context.paintChild(
i,
transform: Matrix4.translationValues(
x, -y + (context.size.height / 2) - (iconSize / 2), 0),
);
}
}

@override
bool shouldRepaint(FlowAnimatedCircle oldDelegate) => false;
}

只要理解到了上面的实现,下面这3种也能很轻松完成


b000.png


b002.gif


b003.gif


最后


收集、参考实现了几个底部导航,当然可能很多地方需要优化,大家不要喷我哦



  • 有很棒的底部菜单希望推荐

  • 需要使用的,建议大家clone下来,直接引入,具体需求(如未读消息)自己添加

  • 欢迎Fork & pr贡献您的代码,大家共同学习❤

  • Android 体验下载 d.cc53.cn/sn6c

  • Web在线体验 footer.eeaarr.cn

作者:longer96
链接:https://juejin.cn/post/6992127843740155918
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

用 Markdown 做 PPT,就是这么简单!

相信绝大多数朋友做 PPT(幻灯片 / Slides / Deck 等各种称呼了)都是用的 PowerPoint 或者 KeyNote 吧?功能是比较强大,但你有没有遇到过这样的痛点: 各种标题、段落的格式不统一,比如字体大小、行间距等等各个页面不太一样,然...
继续阅读 »

相信绝大多数朋友做 PPT(幻灯片 / Slides / Deck 等各种称呼了)都是用的 PowerPoint 或者 KeyNote 吧?功能是比较强大,但你有没有遇到过这样的痛点:



  • 各种标题、段落的格式不统一,比如字体大小、行间距等等各个页面不太一样,然后得用格式刷来挨个刷一下。

  • 想给 PPT 做版本控制,然后就保存了各种复制版本,比如“一版”、“二版”、“终版”、“最终版”、“最终不改版”、“最终稳定不改版”等等,想必大家都见过类似这样的场景吧。

  • 想插入代码,但是插入之后发现格式全乱了或者高亮全没了,然后不得不截图插入进去。

  • 想插入个公式,然后发现 PPT、Keynote 对 Latex 兼容不太好或者配置稍微麻烦,就只能自己重新敲一遍或者贴截图。

  • 想插入一个酷炫的交互组件,比如嵌入一个微博的网页页面实时访问、插入一个可以交互的组件、插入一个音乐播放器组件,原生的 PPT 功能几乎都不支持,这全得依赖于 PowerPoint 或者 KeyNote 来支持才行。


如果你遇到这些痛点,那请你一定要看下去。如果你没有遇到,那也请你看下去吧(拜托。


好,说回正题,我列举了那么多痛点,那这些痛点咋解决呢?


能!甚至解决方案更加轻量级,那就是用 Markdown 来做 PPT!


你试过用 Markdown 写 PPT 吗?没有吧,试试吧,试过之后你就发现上面的功能简直易如反掌。


具体怎么实现呢?


接下来,就有请今天的主角登场了!它就是 Slidev。


什么是 Slidev?


简而言之,Slidev 就是可以让我们用 Markdown 写 PPT 的工具库,基于 Node.js、Vue.js 开发。


利用它我们可以简单地把 Markdown 转化成 PPT,而且它可以支持各种好看的主题、代码高亮、公式、流程图、自定义的网页交互组件,还可以方便地导出成 pdf 或者直接部署成一个网页使用。


官方主页:sli.dev/


GitHub:github.com/slidevjs/sl…


安装和启动


下面我们就来了解下它的基本使用啦。


首先我们需要先安装好 Node.js,推荐 14.x 及以上版本,安装方法见 setup.scrape.center/nodejs


接着,我们就可以使用 npm 这个命令了。


然后我们可以初始化一个仓库,运行命令如下:


npm init slidev@latest

这个命令就是初始化一个 Slidev 的仓库,运行之后它会让我们输入和选择一些选项,如图所示:



比如上图就是先输入项目文件夹的名称,比如这里我取名叫做 slidevtest。


总之一些选项完成之后,Slidev 会在本地 3000 端口上启动,如图所示:



接着,我们就可以打开浏览器 http://localhost:3000 来查看一个 HelloWorld 版本的 PPT 了,如图所示:



我们可以点击空格进行翻页,第二页展示了一张常规的 PPT 的样式,包括标题、正文、列表等,如图所示:



那这一页的 Markdown 是什么样的呢?其实就是非常常规的 Markdown 文章的写法,内容如下:


# What is Slidev?

Slidev is a slides maker and presenter designed for developers, consist of the following features

- 📝 **Text-based** - focus on the content with Markdown, and then style them later
- 🎨 **Themable** - theme can be shared and used with npm packages
- 🧑‍💻 **Developer Friendly** - code highlighting, live coding with autocompletion
- 🤹 **Interactive** - embedding Vue components to enhance your expressions
- 🎥 **Recording** - built-in recording and camera view
- 📤 **Portable** - export into PDF, PNGs, or even a hostable SPA
- 🛠 **Hackable** - anything possible on a webpage

<br>
<br>

Read more about [Why Slidev?](https://sli.dev/guide/why)

是不是?我们只需要用同样格式的 Markdown 语法就可以轻松将其转化为 PPT 了。


快捷键操作


再下一页介绍了各种快捷键的操作,这个就很常规了,比如点击空格、上下左右键来进行页面切换,如图所示:



更多快捷键的操作可以看这里的说明:sli.dev/guide/navig…,一些简单的快捷键列举如下:



  • f:切换全屏

  • right / space:下一动画或幻灯片

  • left:上一动画或幻灯片

  • up:上一张幻灯片

  • down:下一张幻灯片

  • o:切换幻灯片总览

  • d:切换暗黑模式

  • g:显示“前往...”


代码高亮


接下来就是代码环节了,因为 Markdown 对代码编写非常友好,所以展示自然也不是问题了,比如代码高亮、代码对齐等都是常规操作,如图所示:



那左边的代码定义就直接这么写就行了:


# Code

Use code snippets and get the highlighting directly![^1]

```ts {all|2|1-6|9|all}
interface User {
id: number
firstName: string
lastName: string
role: string
}

function updateUser(id: number, update: User) {
const user = getUser(id)
const newUser = {...user, ...update}
saveUser(id, newUser)
}
```

由于是 Markdown,所以我们可以指定是什么语言,比如 TypeScript、Python 等等。


网页组件


接下来就是非常酷炫的环节了,我们还可以自定义一些网页组件,然后展示出来。


比如我们看下面的一张图。左边就呈现了一个数字计数器,点击左侧数字就会减 1,点击右侧数字就会加 1;另外图的右侧还嵌入了一个组件,这里显示了一个推特的消息,通过一个卡片的形式呈现了出来,不仅仅可以看内容,甚至我们还可以点击下方的喜欢、回复、复制等按钮来进行一些交互。


这些功能在网页里面并不稀奇,但是如果能做到 PPT 里面,那感觉就挺酷的。



那这一页怎么做到的呢?这个其实是引入了一些基于 Vue.js 的组件,本节对应的 Markdown 代码如下:


# Components

<div grid="~ cols-2 gap-4">
<div>

You can use Vue components directly inside your slides.

We have provided a few built-in components like `<Tweet/>` and `<Youtube/>` that you can use directly. And adding your custom components is also super easy.

```html
<Counter :count="10" />
```

<!-- ./components/Counter.vue -->
<Counter :count="10" m="t-4" />

Check out [the guides](https://sli.dev/builtin/components.html) for more.

</div>
<div>

```html
<Tweet id="1390115482657726468" />
```

<Tweet id="1390115482657726468" scale="0.65" />

</div>
</div>

这里我们可以看到,这里引入了 Counter、Tweet 组件,而这个 Counter 就是 Vue.js 的组件,代码如下:


<script setup lang="ts">
import { ref } from 'vue'

const props = defineProps({
count: {
default: 0,
},
})

const counter = ref(props.count)
</script>

<template>
<div flex="~" w="min" border="~ gray-400 opacity-50 rounded-md">
<button
border="r gray-400 opacity-50"
p="2"
font="mono"
outline="!none"
hover:bg="gray-400 opacity-20"
@click="counter -= 1"
>
-
</button>
<span m="auto" p="2">{{ counter }}</span>
<button
border="l gray-400 opacity-50"
p="2"
font="mono"
outline="!none"
hover:bg="gray-400 opacity-20"
@click="counter += 1"
>
+
</button>
</div>
</template>

这就是一个标准的基于 Vue.js 3.x 的组件,都是标准的 Vue.js 语法,所以如果我们要添加想要的组件,直接自己写就行了,什么都能实现,只要网页能支持的,统统都能写!


主题定义


当然,一些主题定制也是非常方便的,我们可以在 Markdown 文件直接更改一些配置就好了,比如就把 theme 换个名字,整个主题样式就变了,看如下的对比图:



上面就是一些内置主题,当然我们也可以去官方文档查看一些别人已经写好的主题,见:sli.dev/themes/gall…


另外我们自己写主题也是可以的,所有的主题样式都可以通过 CSS 等配置好,想要什么就可以有什么,见:sli.dev/themes/writ…


公式和图表


接下来就是一个非常强大实用的功能,公式和图表,支持 Latex、流程图,如图所示:




比如上面的 Latex 的源代码就是这样的:


Inline $\sqrt{3x-1}+(1+x)^2$

Block
$$
\begin{array}{c}

\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
= \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\

\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\

\nabla \cdot \vec{\mathbf{B}} & = 0

\end{array}
$$

其语法也是和 Latex 一样的。


其背后是怎么实现的呢?其实是因为 Slidev 默认集成了 Katex 这个库,见:katex.org/,有了 Katex 的加持,所有公式的显示都不是事。


页面分隔


有的朋友就好奇了,既然是用 Markdown 写 PPT,那么每一页之间是怎么分割的呢?


其实很简单,最常规的,用三条横线分割就好了,比如:


---
layout: cover
---

# 第 1 页

This is the cover page.

---

# 第 2 页

The second page

当然,除了使用三横线,我们还可以使用更丰富的定义模式,可以给每一页制定一些具体信息,就是使用两层三横线。


比如这样:


---
theme: seriph
layout: cover
background: 'https://source.unsplash.com/1600x900/?nature,water'
---

上面这样的配置可以替代三横线,是另一种可以用作页面分隔的写法,借助这种写法我们可以定义更多页面的具体信息。


备注


当然我们肯定也想给 PPT 添加备注,这个也非常简单,通过注释的形式写到 Markdown 源文件就好了:


---
layout: cover
---

# 第 1 页

This is the cover page.

<!-- 这是一条备注 -->

这里可以看到其实就是用了注释的特定语法。


演讲者头像


当然还有很多酷炫的功能,比如说,我们在讲 PPT 的时候,可能想同时自己也出镜,Slidev 也可以支持。


因为开的是网页,而网页又有捕捉摄像头的功能,所以最终效果可以是这样子:



是的没错!右下角就是演讲者的个人头像,它被嵌入到了 PPT 中!是不是非常酷!


演讲录制


当然,Slidev 还支持演讲录制功能,因为它背后集成了 WebRTC 和 RecordRTC 的 API,一些录制配置如下所示:



所以,演讲过程的录制完全不是问题。


具体的操作可以查看:sli.dev/guide/recor…


部署


当然用 Slidev 写的 PPT 还可以支持部署,因为这毕竟就是一个网页。


而且部署非常简单和轻量级,因为这就是一些纯静态的 HTML、JavaScript 文件,我们可以轻松把它部署到 GitHub Pages、Netlify 等站点上。


试想这么一个场景:别人在演讲之前还在各种拷贝 PPT,而你打开了一个浏览器直接输入了一个网址,PPT 就出来了,众人惊叹,就问你装不装逼?


具体的部署操作可以查看:sli.dev/guide/hosti…


让我们看几个别人已经部署好的 PPT,直接网页打开就行了:



就是这么简单方便。


版本控制


什么?你想实现版本控制,那再简单不过了。


Markdown 嘛,配合下专业版本管理工具 Git,版本控制再也不是难题。


总结


以上就是对 Slidev 的简单介绍,确实不得不说有些功能真的非常实用,而且我本身特别喜欢 Markdown 和网页开发,所以这个简直对我来说太方便了。


作者:崔庆才丨静觅
链接:https://juejin.cn/post/7062979350651469831
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter 保证数据操作原子性

Flutter 保证数据操作原子性 Flutter 是单线程架构,按道理理说,Flutter 不会出现 Java 的多线程相关的问题。 但在我使用 Flutter 过程中,却发现 Flutter 依然会存在数据操作原子性的问题。 其实 Flutter 中存在...
继续阅读 »

Flutter 保证数据操作原子性


Flutter 是单线程架构,按道理理说,Flutter 不会出现 Java 的多线程相关的问题。


但在我使用 Flutter 过程中,却发现 Flutter 依然会存在数据操作原子性的问题。



其实 Flutter 中存在多线程的(Isolate 隔离池),只是 Flutter 中的多线程更像 Java 中的多进程,因为 Flutter 中线程不能像 Java 一样,可以两个线程去操作同一个对象。


我们一般将计算任务放在 Flutter 单独的线程中,例如一大段 Json 数据的解析,可以将解析计算放在单独的线程中,然后将解析完后的 Map<String, dynamic> 返回到主线程来用。



Flutter单例模式


在 Java 中,我们一般喜欢用单例模式来理解 Java 多线程问题。这里我们也以单例来举例,我们先来一个正常的:


class FlutterSingleton {
static FlutterSingleton? _instance;

/// 将构造方法声明成私有的
FlutterSingleton._();

static FlutterSingleton getInstance() {
if (_instance == null) {
_instance = FlutterSingleton._();
}
return _instance!;
}
}

由于 Flutter 是单线程架构的, 所以上述代码是没有问题的。


问题示例


但是, 和 Java 不同的是, Flutter 中存在异步方法。


做 App 开发肯定会涉及到数据持久化,Android 开发应该都熟悉 SharedPreferences,Flutter 中也存在 SharedPreferences 库,我们就以此来举例。同样实现单例模式,只是这次无可避免的需要使用 Flutter 中的异步:


class SPSingleton {
static SPSingleton? _instance;

String? data;

/// 将构造方法声明成私有的
SPSingleton._fromMap(Map<String, dynamic> map) : data = map['data'];

static Future<SPSingleton> _fromSharedPreferences() async {
// 模拟从 SharedPreferences 中读取数据, 并以此来初始化当前对象
Map<String, String> map = {'data': 'mockData'};
await Future.delayed(Duration(milliseconds: 10));
return SPSingleton._fromMap(map);
}

static Future<SPSingleton> getInstance() async {
if (_instance == null) {
_instance = await SPSingleton._fromSharedPreferences();
}
return _instance!;
}
}

void main() async {
SPSingleton.getInstance().then((value) {
print('instance1.hashcode = ${value.hashCode}');
});
SPSingleton.getInstance().then((value) {
print('instance2.hashcode = ${value.hashCode}');
});
}

运行上面的代码,打印日志如下:


instance1.hashcode = 428834223
instance2.hashcode = 324692380

可以发现,我们两次调用 SPSingleton.getInstance() 方法,分别创建了两个对象,说明上面的单例模式实现有问题。


我们来分析一下 getInstance() 方法:


static Future<SPSingleton> getInstance() async {
if (_instance == null) { // 1
_instance = await SPSingleton._fromSharedPreferences(); //2
}
return _instance!;
}

当第一次调用 getInstance() 方法时,代码在运行到 1 处时,发现 _instance 为 null, 就会进入 if 语句里面执行 2 处, 并因为 await 关键字挂起, 并交出代码的执行权, 直到被 await 的 Future 执行完毕,最后将创建的 SPSingleton 对象赋值给 _instance 并返回。


当第二次调用 getInstance() 方法时,代码在运行到 1 处时,可能会发现 _instance 还是为 null (因为 await SPSingleton._fromSharedPreferences() 需要 10ms 才能返回结果), 然后和第一次调用 getInstance() 方法类似, 创建新的 SPSingleton 对象赋值给 _instance。


最后导致两次调用 getInstance() 方法, 分别创建了两个对象。


解决办法


问题原因知道了,那么该怎样解决这个问题呢?


究其本质,就是 getInstance() 方法的执行不具有原子性,即:在一次 getInstance() 方法执行结束前,不能执行下一次 getInstance() 方法。


幸运的是, 我们可以借助 Completer 来将异步操作原子化,下面是借助 Completer 改造后的代码:


import 'dart:async';

class SPSingleton {
static SPSingleton? _instance;
static Completer<bool>? _monitor;

String? data;

/// 将构造方法声明成私有的
SPSingleton._fromMap(Map<String, dynamic> map) : data = map['data'];

static Future<SPSingleton> _fromSharedPreferences() async {
// 模拟从 SharedPreferences 中读取数据, 并以此来初始化当前对象
Map<String, String> map = {'data': 'mockData'};
await Future.delayed(Duration(milliseconds: 10));
return SPSingleton._fromMap(map);
}

static Future<SPSingleton> getInstance() async {
if (_instance == null) {
if (_monitor == null) {
_monitor = Completer<bool>();
_instance = await SPSingleton._fromSharedPreferences();
_monitor!.complete(true);
} else {
// Flutter 的 Future 支持被多次 await
await _monitor!.future;
_monitor = null;
}
}
return _instance!;
}
}

void main() async {
SPSingleton.getInstance().then((value) {
print('instance1.hashcode = ${value.hashCode}');
});
SPSingleton.getInstance().then((value) {
print('instance2.hashcode = ${value.hashCode}');
});
}

我们再次分析一下 getInstance() 方法:


static Future<SPSingleton> getInstance() async {
if (_instance == null) { // 1
if (_monitor == null) { // 2
_monitor = Completer<bool>(); // 3
_instance = await SPSingleton._fromSharedPreferences(); // 4
_monitor!.complete(true); // 5
} else {
// Flutter 的 Future 支持被多次 await
await _monitor!.future; //6
_monitor = null;
}
}
return _instance!; // 7
}

当第一次调用 getInstance() 方法时, 1 处和 2 处都会判定为 true, 然后进入执行到 3 处创建一个的 Completer 对象, 然后在 4 的 await 处挂起, 并交出代码的执行权, 直到被 await 的 Future 执行完毕。


此时第二次调用的 getInstance() 方法开始执行,1 处同样会判定为 true, 但是到 2 处时会判定为 false, 从而进入到 else, 并因为 6 处的 await 挂起, 并交出代码的执行权;


此时, 第一次调用 getInstance() 时的 4 处执行完毕, 并执行到 5, 并通过 Completer 通知第二次调用的 getInstance() 方法可以等待获取代码执行权了。


最后,两次调用 getInstance() 方法都会返回同一个 SPSingleton 对象,以下是打印日志:


instance1.hashcode = 786567983
instance2.hashcode = 786567983


由于 Flutter 的 Future 是支持多次 await 的, 所以即便是连续 n 次调用 getInstance() 方法, 从第 2 到 n 次调用会 await 同一个 Completer.future, 最后也能返回同一个对象。



Flutter任务队列


虽然我们经常拿单例模式来解释说明 Java 多线程问题,可这并不代表着 Java 只有在单例模式时才有多线程问题。


同样的,也并不代表着 Flutter 只有在单例模式下才有原子操作问题。


问题示例


我们同样以数据持久化来举例,只是这次我们以数据库操作来举例。


我们在操作数据库时,经常会有这样的需求:如果数据库表中存在这条数据,就更新这条数据,否则就插入这条数据。


为了实现这样的需求,我们可能会先从数据库表中查询数据,查询到了就更新,没查询到就插入,代码如下:


class Item {
int id;
String data;
Item({
required this.id,
required this.data,
});
}

class DBTest {
DBTest._();
static DBTest instance = DBTest._();
bool _existsData = false;
Future<void> insert(String data) async {
// 模拟数据库插入操作,10毫秒过后,数据库中才有数据
await Future.delayed(Duration(milliseconds: 10));
_existsData = true;
print('执行了插入');
}

Future<void> update(String data) async {
// 模拟数据库更新操作
await Future.delayed(Duration(milliseconds: 10));
print('执行了更新');
}

Future<Item?> selected(int id) async {
// 模拟数据库查询操作
await Future.delayed(Duration(milliseconds: 10));
if (_existsData) {
// 数据库中有数据才返回
return Item(id: 1, data: 'mockData');
} else {
// 数据库没有数据时,返回null
return null;
}
}

/// 先从数据库表中查询数据,查询到了就更新,没查询到就插入
Future<void> insertOrUpdate(int id, String data) async {
Item? item = await selected(id);
if (item == null) {
await insert(data);
} else {
await update(data);
}
}
}

void main() async {
DBTest.instance.insertOrUpdate(1, 'data');
DBTest.instance.insertOrUpdate(1, 'data');
}

我们期望的输出日志为:


执行了插入
执行了更新

但不幸的是, 输出的日志为:


执行了插入
执行了插入

原因也是异步方法操作数据, 不是原子操作, 导致逻辑异常。


也许我们也可以效仿单例模式的实现,利用 Completer 将 insertOrUpdate() 方法原子化。


但对于数据库操作是不合适的,因为我们可能还有其它需求,比如说:调用插入数据的方法,然后立即从数据库中查询这条数据,发现找不到。


如果强行使用 Completer,那么到最后,可能这个类中会出现一大堆的 Completer ,代码难以维护。


解决办法


其实我们想要的效果是,当有异步方法在操作数据库时,别的操作数据的异步方法应该阻塞住,也就是同一时间只能有一个方法来操作数据库。我们其实可以使用任务队列来实现数据库操作的需求。


我这里利用 Completer 实现了一个任务队列:


import 'dart:async';
import 'dart:collection';

/// TaskQueue 不支持 submit await submit, 以下代码就存在问题
///
/// TaskQueue taskQueue = TaskQueue();
/// Future<void> task1(String arg)async{
/// await Future.delayed(Duration(milliseconds: 100));
/// }
/// Future<void> task2(String arg)async{
/// 在这里submit时, 任务会被添加到队尾, 且当前方法任务不会结束
/// 添加到队尾的任务必须等到当前方法任务执行完毕后, 才能继续执行
/// 而队尾的任务必须等当前任务执行完毕后, 才能执行
/// 这就导致相互等待, 使任务无法进行下去
/// 解决办法是, 移除当前的 await, 让当前任务结束
/// await taskQueue.submit(task1, arg);
/// }
///
/// taskQueue.submit(task2, arg);
///
/// 总结:
/// 被 submit 的方法的内部如果调用 submit 方法, 此方法不能 await, 否则任务队列会被阻塞住
///
/// 如何避免此操作, 可以借鉴以下思想:
/// 以数据库操作举例, 有个save方法的逻辑是插入或者更新(先查询数据库select,再进行下一步操作);
/// sava方法内部submit,并且select也submit, 就容易出现submit await submit的情况
///
/// 我们可以这样操作,假设当前类为 DBHelper:
/// 将数据库的增,删,查,改操作封装成私有的 async 方法, 且私有方法不能使用submit
/// DBHelper的公有方法, 可以调用自己的私有 async 方法, 但不能调用自己的公有方法, 公有方法可以使用submit
/// 这样就不会存在submit await submit的情况了
class TaskQueue {
/// 提交任务
Future<O> submit<A, O>(Function fun, A? arg) async {
if (!_isEnable) {
throw Exception('current TaskQueue is recycled.');
}
Completer<O> result = new Completer<O>();

if (!_isStartLoop) {
_isStartLoop = true;
_startLoop();
}

_queue.addLast(_Runnable<A, O>(
fun: fun,
arg: arg,
completer: result,
));
if (!(_emptyMonitor?.isCompleted ?? true)) {
_emptyMonitor?.complete();
}

return result.future;
}

/// 回收 TaskQueue
void recycle() {
_isEnable = false;
if (!(_emptyMonitor?.isCompleted ?? true)) {
_emptyMonitor?.complete();
}
_queue.clear();
}

Queue<_Runnable> _queue = Queue<_Runnable>();
Completer? _emptyMonitor;
bool _isStartLoop = false;
bool _isEnable = true;

Future<void> _startLoop() async {
while (_isEnable) {
if (_queue.isEmpty) {
_emptyMonitor = new Completer();
await _emptyMonitor!.future;
_emptyMonitor = null;
}

if (!_isEnable) {
// 当前TaskQueue不可用时, 跳出循环
return;
}

_Runnable runnable = _queue.removeFirst();
try {
dynamic result = await runnable.fun(runnable.arg);
runnable.completer.complete(result);
} catch (e) {
runnable.completer.completeError(e);
}
}
}
}

class _Runnable<A, O> {
final Completer<O> completer;
final Function fun;
final A? arg;

_Runnable({
required this.completer,
required this.fun,
this.arg,
});
}


由于 Flutter 中的 future 不支持暂停操作, 一旦开始执行, 就只能等待执行完。


所以这里的任务队列实现是基于方法的延迟调用来实现的。



TaskQueue 的用法示例如下:


void main() async {
Future<void> test1(String data) async {
await Future.delayed(Duration(milliseconds: 20));
print('执行了test1');
}

Future<String> test2(Map<String, dynamic> args) async {
await Future.delayed(Duration(milliseconds: 10));
print('执行了test2');
return 'mockResult';
}

TaskQueue taskQueue = TaskQueue();
taskQueue.submit(test1, '1');
taskQueue.submit(test2, {
'data1': 1,
'data2': '2',
}).then((value) {
print('test2返回结果:${value}');
});

await Future.delayed(Duration(milliseconds: 200));
taskQueue.recycle();
}
/*
执行输出结果如下:

执行了test1
执行了test2
test2返回结果:mockResult
*/


值得注意的是: 这里的 TaskQueue 不支持 submit await submit, 原因及示例代码已在注释中说明,这里不再赘述。



为了避免出现 submit await submit 的情况,我代码注释中也做出了建议(假设当前类为 DBHelper):




  • 将数据库的增、删、查、改操作封装成私有的异步方法, 且私有异步方法不能使用 submit;




  • DBHelper 的公有方法, 可以调用自己的私有异步方法, 但不能调用自己的公有异步方法, 公有异步方法可以使用 submit;




这样就不会出现 submit await submit 的情况了。


于是,上述的数据库操作示例代码就变成了以下的样子:


class Item {
int id;
String data;
Item({
required this.id,
required this.data,
});
}

class DBTest {
DBTest._();
static DBTest instance = DBTest._();
TaskQueue _taskQueue = TaskQueue();
bool _existsData = false;
Future<void> _insert(String data) async {
// 模拟数据库插入操作,10毫秒过后,数据库才有数据
await Future.delayed(Duration(milliseconds: 10));
_existsData = true;
print('执行了插入');
}

Future<void> insert(String data) async {
await _taskQueue.submit(_insert, data);
}

Future<void> _update(String data) async {
// 模拟数据库更新操作
await Future.delayed(Duration(milliseconds: 10));
print('执行了更新');
}

Future<void> update(String data) async {
await _taskQueue.submit(_update, data);
}

Future<Item?> _selected(int id) async {
// 模拟数据库查询操作
await Future.delayed(Duration(milliseconds: 10));
if (_existsData) {
// 数据库中有数据才返回
return Item(id: 1, data: 'mockData');
} else {
// 数据库没有数据时,返回null
return null;
}
}

Future<Item?> selected(int id) async {
return await _taskQueue.submit(_selected, id);
}

/// 先从数据库表中查询数据,查询到了就更新,没查询到就插入
Future<void> _insertOrUpdate(Map<String, dynamic> args) async {
int id = args['id'];
String data = args['data'];
Item? item = await _selected(id);
if (item == null) {
await _insert(data);
} else {
await _update(data);
}
}

Future<Item?> insertOrUpdate(int id, String data) async {
return await _taskQueue.submit(_insertOrUpdate, {
'id': id,
'data': data,
});
}
}

void main() async {
DBTest.instance.insertOrUpdate(1, 'data');
DBTest.instance.insertOrUpdate(1, 'data');
}

输出日志也变成了我们期望的样子:


执行了插入
执行了更新

总结




  • Flutter 异步方法修改数据时, 一定要注意数据操作的原子性, 不能因为 Flutter 是单线程架构,就忽略多个异步方法竞争导致数据异常的问题。




  • Flutter 保证数据操作的原子性,也有可行办法,当逻辑比较简单时,可直接使用 Completer,当逻辑比较复杂时,可以考虑使用任务队列。




另外,本文中的任务队列实现有很大的缺陷,不支持 submit await submit,否则整个任务队列会被阻塞住。


如果诸位有其它的任务队列实现方式,或者保证数据操作原子性的方法,欢迎留言。


作者:水花DX
链接:https://juejin.cn/post/7070071427864477710
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Mybatis的where标签,竟然还有这么多不知道的!

背景 在上篇文章,我们系统地学习了where 1=1 相关的知识点,大家可以回看《不要再用where 1=1了!有更好的写法!》这篇文章。文章中涉及到了Mybatis的替代方案,有好学的朋友在评论区有朋友问了基于Mybatis写法的问题。 于是,就有了这篇文章...
继续阅读 »

背景


在上篇文章,我们系统地学习了where 1=1 相关的知识点,大家可以回看《不要再用where 1=1了!有更好的写法!》这篇文章。文章中涉及到了Mybatis的替代方案,有好学的朋友在评论区有朋友问了基于Mybatis写法的问题。


于是,就有了这篇文章。本篇文章会将Mybatis中where标签的基本使用形式、小技巧以及容易踩到的坑进行总结梳理,方便大家更好地实践运用d


原始的手动拼接


在不使用Mybatis的where标签时,我们通常是根据查询条件进行手动拼接,也就是用到了上面提到的where 1=1的方式,示例如下:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  where 1=1
  <if test="username != null and username != ''">
    and username = #{username}
  </if>
  <if test="idNo != null and idNo != ''">
    and id_no = #{idNo}
  </if>
</select>

这种方式主要就是为了避免语句拼接错误,出现类似如下的错误SQL:


select * from t_user where and username = 'Tom' and id = '1001';
select * from t_user where and id = '1001';

当添加上1=1时,SQL语句便是正确的了:


select * from t_user where 1=1 and username = 'Tom' and id = '1001';
select * from t_user where 1=1 and id = '1001';

这个我们之前已经提到过,多少对MySQL数据库的有一定的压力。因为1=1条件的优化过滤是需要MySQL做的。如果能够将这部分放到应用程序来做,就减少了MySQL的压力。毕竟,应用程序是可以轻易地横向扩展的。


Mybatis where标签的使用


为了能达到MySQL性能的调优,我们可以基于Mybatis的where标签来进行实现。where标签是顶层的遍历标签,需要配合if标签使用,单独使用无意义。通常有下面两种实现形式。


方式一:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      and id_no = #{idNo}
    </if>
  </where>
</select>

方式二:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      and username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      and id_no = #{idNo}
    </if>
  </where>
</select>

仔细观察会发现,这两种方式的区别在于第一if条件中的SQL语句是否有and


这里就涉及到where标签的两个特性:



  • 第一,只有if标签有内容的情况下才会插入where子句;

  • 第二,若子句的开通为 “AND” 或 “OR”,where标签会将它替换去除;


所以说,上面的两种写法都是可以了,Mybatis的where标签会替我们做一些事情。


但需要注意的是:where标签只会 智能的去除(忽略)首个满足条件语句的前缀。所以建议在使用where标签时,每个语句都最好写上 and 前缀或者 or 前缀,否则像以下写法就会出现问题:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      id_no = #{idNo}
    </if>
  </where>
</select>

生成的SQL语句如下:


select * from t_user      WHERE username = ?  id_no = ?

很显然,语法是错误的。


因此,在使用where标签时,建议将所有条件都添加上and或or


进阶:自定义trim标签


上面使用where标签可以达到拼接条件语句时,自动去掉首个条件的and或or,那么如果是其他自定义的关键字是否也能去掉呢?


此时,where标签就无能为力了,该trim标签上场了,它也可以实现where标签的功能。


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <trim prefix="where" prefixOverrides="and | or ">
    <if test="username != null and username != ''">
      and username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      and id_no = #{idNo}
    </if>
  </trim>
</select>

将上面基于where标签的写改写为trim标签,发现执行效果完全一样。而且trim标签具有了更加灵活的自定义性。


where语句的坑


另外,在使用where语句或其他语句时一定要注意一个地方,那就是:注释的使用。


先来看例子:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      and username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      /* and id_no = #{idNo}*/
      and id_no = #{idNo}
    </if>
  </where>
</select>

上述SQL语句中添加了 /**/的注释,生成的SQL语句为:


select * from t_user WHERE username = ? /* and id_no = ?*/ and id_no = ? 

执行时,直接报错。


还有一个示例:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      -- and username = #{username}
      and username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      and id_no = #{idNo}
    </if>
  </where>
</select>

生成的SQL语句为:


select * from t_user WHERE -- and username = ? and username = ? and id_no = ? 

同样会导致报错。


这是因为我们使用 XML 方式配置 SQL 时,如果在 where 标签之后添加了注释,那么当有子元素满足条件时,除了 < !-- --> 注释会被 where 忽略解析以外,其它注释例如 // 或 /**/ 或 -- 等都会被 where 当成首个子句元素处理,导致后续真正的首个 AND 子句元素或 OR 子句元素没能被成功替换掉前缀,从而引起语法错误。


同时,个人在实践中也经常发现因为在XML中使用注释不当导致SQL语法错误或执行出错误的结果。强烈建议,非必要,不要在XML中注释掉SQL,可以通过版本管理工具来追溯历史记录和修改。


小结


本文基于Mybatis中where标签的使用,展开讲了它的使用方式、特性以及拓展到trim标签的替代作用,同时,也提到了在使用时可能会出现的坑。内容虽然简单,但如果能够很好地实践、避免踩坑也是能力的体现。


作者:程序新视界
链接:https://juejin.cn/post/7070614699892539428
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

算法题每日一练:汉明距离

一、问题描述 两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。 给你两个整数 x 和 y,计算并返回它们之间的汉明距离。 题目链接:汉明距离。 二、题目要求 样例 1 输入: x = 1...
继续阅读 »

一、问题描述


两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。


给你两个整数 x 和 y,计算并返回它们之间的汉明距离。


题目链接:汉明距离


二、题目要求


样例 1


输入: x = 1, y = 4
输出: 2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。

样例 2


输入: x = 3, y = 1
输出: 1

考察


1.位运算简单题型
2.建议用时5~20min

三、问题分析


本题是位运算的第3题,没了解过位运算相关知识点可以看这一篇文章,讲解比较详细:


算法题每日一练---第45天:位运算


什么是汉明距离,简单来讲就是将两个10进制数字转换成2进制数字之后,统计不同位置上面1的个数,那如何将这题逐渐向位运算靠拢呢?


不同位置,啥叫不同位置,不就是如果相同位置都是1或0,那么就不计数。只有当相同位置一个为1,另一个为0时才开始计数,这不就是位运算的异或计算吗?


7.png


四、编码实现


class Solution {
public:
int hammingDistance(int x, int y) {
int i,ans=0;//初始化数据
for(i=0;i<32;i++)//32位循环判断
{
if((x^y)&1<<i)//与计算并且开始查询1的个数
{
ans++;//计数器++
}
}
return ans;//输出结果
}
};

五、测试结果


1.png


2.png


作者:知心宝贝
链接:https://juejin.cn/post/7072515878218104840
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

细说Android apk四代签名:APK v1、APK v2、APK v3、APK v4

简介 大部分开发者对apk签名还停留在APK v2,对APK v3和APK v4了解很少,而且网上大部分文章讲解的含糊不清,所以根据官网文档重新整理一份。 apk签名从APK v1到APK v2改动很大,是颠覆性的,而APK v3只是对APK v2的一次升级,...
继续阅读 »

简介


大部分开发者对apk签名还停留在APK v2,对APK v3和APK v4了解很少,而且网上大部分文章讲解的含糊不清,所以根据官网文档重新整理一份。


apk签名从APK v1到APK v2改动很大,是颠覆性的,而APK v3只是对APK v2的一次升级,APK v4则是一个补充。


本篇文章主要参考Android各版本改动:
developer.android.google.cn/about/versi…


APK v1


就是jar签名,apk最初的签名方式,大家都很熟悉了,签名完之后是META-INF 目录下的三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。


MANIFEST.MF

MANIFEST.MF中是apk种每个文件名称和摘要SHA1(或者 SHA256),如果是目录则只有名称


CERT.SF

CERT.SF则是对MANIFEST.MF的摘要,包括三个部分:



  • SHA1-Digest-Manifest-Main-Attributes:对 MANIFEST.MF 头部的块做 SHA1(或者SHA256)后再用 Base64 编码

  • SHA1-Digest-Manifest:对整个 MANIFEST.MF 文件做 SHA1(或者 SHA256)后再用 Base64 编码

  • SHA1-Digest:对 MANIFEST.MF 的各个条目做 SHA1(或者 SHA256)后再用 Base64 编码


CERT.RSA

CERT.RSA是将CERT.SF通过私钥签名,然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存


通过这三层校验来确保apk中的每个文件都不被改动。


APK v2


官方说明:source.android.google.cn/security/ap…


APK 签名方案 v2 是在 Android 7.0 (Nougat) 中引入的。为了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。


APK v1的缺点就是META-INF目录下的文件并不在校验范围内,所以之前多渠道打包等都是通过在这个目录下添加文件来实现的。


APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。


使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APK 签名分块”内,v2 签名和签名者身份信息会存储在 APK 签名方案 v2 分块中。


image.png


通俗点说就是签名信息不再以文件的形式存储,而是将其转成二进制数据直接写在apk文件中,这样就避免了APK v1的META-INF目录的问题。


在 Android 7.0 及更高版本中,可以根据 APK 签名方案 v2+ 或 JAR 签名(v1 方案)验证 APK。更低版本的平台会忽略 v2 签名,仅验证 v1 签名。


image.png


APK v3


官方说明:source.android.google.cn/security/ap…


APK 签名方案 v3 是在 Android 9 中引入的。


Android 9 支持 APK 密钥轮替,这使应用能够在 APK 更新过程中更改其签名密钥。为了实现轮替,APK 必须指示新旧签名密钥之间的信任级别。为了支持密钥轮替,我们将 APK 签名方案从 v2 更新为 v3,以允许使用新旧密钥。v3 在 APK 签名分块中添加了有关受支持的 SDK 版本和 proof-of-rotation 结构的信息。


简单来说APK v3就是为了Andorid9的APK 密钥轮替功能而出现的,就是在v2的基础上增加两个数据块来存储APK 密钥轮替所需要的一些信息,所以可以看成是v2的升级。具体结构见官网说明即可。


APK 密钥轮替功能可以参考:developer.android.google.cn/about/versi…



具有密钥轮转的 APK 签名方案


Android 9 新增了对 APK Signature Scheme v3 的支持。该架构提供的选择可以在其签名块中为每个签名证书加入一条轮转证据记录。利用此功能,应用可以通过将 APK 文件过去的签名证书链接到现在签署应用时使用的证书,从而使用新签名证书来签署应用。


developer.android.google.cn/about/versi…



注:运行 Android 8.1(API 级别 27)或更低版本的设备不支持更改签名证书。如果应用的 minSdkVersion 为 27 或更低,除了新签名之外,可使用旧签名证书来签署应用。


详细了解如何使用 apksigner 轮转密钥参考:developer.android.google.cn/studio/comm…


在 Android 9 及更高版本中,可以根据 APK 签名方案 v3、v2 或 v1 验证 APK。较旧的平台会忽略 v3 签名而尝试验证 v2 签名,然后尝试验证 v1 签名。


image.png


APK v4


官方说明:source.android.google.cn/security/ap…


APK 签名方案 v4 是在 Android 11 中引入的。


Android 11 通过 APK 签名方案 v4 支持与流式传输兼容的签名方案。v4 签名基于根据 APK 的所有字节计算得出的 Merkle 哈希树。它完全遵循 fs-verity 哈希树的结构(例如,对salt进行零填充,以及对最后一个分块进行零填充。)Android 11 将签名存储在单独的 .apk.idsig 文件中。v4 签名需要 v2 或 v3 签名作为补充。


APK v4同样是为了新功能而出现的,这个新功能就是ADB 增量 APK 安装,可以参考Android11 功能和 API 概览:
developer.android.google.cn/about/versi…



ADB 增量 APK 安装


在设备上安装大型(2GB 以上)APK 可能需要很长的时间,即使应用只是稍作更改也是如此。ADB(Android 调试桥)增量 APK 安装可以安装足够的 APK 以启动应用,同时在后台流式传输剩余数据,从而加速这一过程。如果设备支持该功能,并且您安装了最新的 SDK 平台工具,adb install 将自动使用此功能。如果不支持,系统会自动使用默认安装方法。


developer.android.google.cn/about/versi…




运行以下 adb 命令以使用该功能。如果设备不支持增量安装,该命令将会失败并输出详细的解释。


adb install --incremental


在运行 ADB 增量 APK 安装之前,您必须先为 APK 签名并创建一个 APK 签名方案 v4 文件。必须将 v4 签名文件放在 APK 旁边,才能使此功能正常运行。


developer.android.google.cn/about/versi…



因为需要流式传输,所以需要将文件分块,对每一块进行签名以便校验,使用的方式就是Merkle 哈希树(http://www.kernel.org/doc/html/la… v4就是做这部分功能的。所以APK v4与APK v2或APK v3可以算是并行的,所以APK v4签名后还需要 v2 或 v3 签名作为补充。


运行 adb install --incremental 命令时,adb 会要求 .apk.idsig 文件存在于 .apk 旁边(所以APK v4的签名文件.apk.idsig并不会打包进apk文件中


默认情况下,它还会使用 .idsig 文件尝试进行增量安装;如果此文件缺失或无效,该命令会回退到常规安装。


image.png


总结


综上,可以看到APK v4是面向ADB即开发调试的,而如果我们没有签名变动的需求也可以不考虑APK v3,所以目前国内大部分还停留在APK v2。


作者:BennuCTech
链接:https://juejin.cn/post/7068079232290652197
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用了这么久Java,我竟不知道Java是值传递

泪目,想不到我用了这么久的Java编程语言,竟然使用的是值传递。本篇文章我们将带大家搞清楚Java值传递的特性。前言是不是有人会这样认为Java在传递参数时,参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。如果是这样认为那就大错特错了。下面我们一...
继续阅读 »

泪目,想不到我用了这么久的Java编程语言,竟然使用的是值传递。本篇文章我们将带大家搞清楚Java值传递的特性。

前言

是不是有人会这样认为Java在传递参数时,参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。

如果是这样认为那就大错特错了。

下面我们一起看一下Java的值传递特性。

Java 真的是值传递

检验是值传递最好的方法就是交换方法,我们在交换方法中将两个对象的引用互换,再看一下原来的值会不会被影响。

我们可以看如下例子:

  • 首先创建两个用户一个张三,一个李四。
  • 然后我们调用身份互换方法swap
  • 在身份互换方法中确认两人是否身份互换成功(可以看到结果是成功的)
  • 如果是引用传递的话我们创建的两个用户已经完成了身份互换
  • 但实际结果是两个用户
package com.zhj.interview;

public class Test15 {

   public static void main(String[] args) {
       User ZhangSan = new User("张三");
       User LiSi = new User("李四");
       System.out.println("开始交换---------------------------------");
       swap(ZhangSan, LiSi);
       System.out.println("交换结束---------------------------------");
       System.out.println("交换后ZhangSan的名字:" + ZhangSan.name);
       System.out.println("交换后LiSi的名字:" + LiSi.name);
  }

   private static void swap(User ZhangSan, User LiSi){
       User user;
       user = ZhangSan;
       ZhangSan = LiSi;
       LiSi = user;
       System.out.println("交换后ZhangSan的名字:" + ZhangSan.name);
       System.out.println("交换后LiSi的名字:" + LiSi.name);
  }
}
class User{
   String name;
   User(String name) {
       this.name = name;
  }
}

运行结果:

开始交换---------------------------------
交换后ZhangSan的名字:李四
交换后LiSi的名字:张三
交换结束---------------------------------
交换后ZhangSan的名字:张三
交换后LiSi的名字:李四

如下图所示,值传递的意思是将引用进行值传递,希望下图能对大家理解上有所帮助。

image.png

造成错觉的原因

造成我们认为Java在传递参数时,参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递的原因就是,我们将传入的对象修改后,开始创建的对象内容也会改变。

最好的理解方式就是:

我们传入的参数只是钥匙,值传递就是配两把钥匙给方法,引用传递就是把自己的钥匙给方法;

如果把自己的钥匙给方法,方法内交换了钥匙之后,我们自己的钥匙也就被掉包了(引用传递);

如果是额外配两把钥匙给对方,方法内交换的钥匙是新配的钥匙,不会影响我们自己的钥匙(值传递);

需要注意的是,无论是使用自己的钥匙,还是后配的钥匙,打开房门,修改房屋内结构,这个变化是不受影响钥匙影响的,因为改变的不是钥匙。

在JVM中对象引用与对象信息的体现,如下图所示。

image.png


作者:CoderJie
链接:https://juejin.cn/post/7070442746971947016
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

神奇的二进制

一. 前言 太极生两仪,两仪生四相,四相生八卦,八卦定吉凶 中国的八卦与西方的二进制其实原理上有极其相似的一面. 但是演化和推算方面, 八卦更胜一筹. 阴: 0, 阳: 1; 老阴: 00, ...
继续阅读 »

一. 前言


太极生两仪,两仪生四相,四相生八卦,八卦定吉凶


中国的八卦与西方的二进制其实原理上有极其相似的一面. 但是演化和推算方面, 八卦更胜一筹.


阴: 0, 阳: 1;


老阴: 00, 少阳: 10, 少阴: 01, 老阳: 11;


坤: 000, 艮: 100, 坎: 010,巽 :110, ,震: 001, 离:101, 兑: 011, 乾: 111;


image.png


二.正文.


计算机就是一个二进制的逻辑机器


计算机三大件:主板,CPU,内存; 而最主要的就是cpu, cpu的主要组成就是可以理解为晶体管, 晶体管也就相当于是个开关, 即打电报中的._. 这种二进制的简单也造就了计算机快的特性.


**计算机只会二进制, 那怎样才能让计算机有用呢? **


那就是约定大于配置


先是字符集



  • ASCII字符集

  • ISO 8859-1字符集

  • GB2312字符集

  • GBK字符集

  • utf8字符集


数字, 以int为例子:



















十进制二进制
2013140000 0000 0000 0011 0001 0010 0110 0010
-2013141000 0000 0000 0011 0001 0010 0110 0010

二进制在源码的巧妙使用(我们以java示例)


很多源码中使用了二进制来区分各个状态, 而且状态可以组合的形式.
主要使用的是&|的计算来实现的.


0&0 = 0;           0|0 = 0;
0&1 = 0; 0|1 = 1;
1&0 = 0; 1|0 = 1;
1&1 = 1; 1|1 = 1;

例如:


Streams
public int characteristics() {
// 既包含所有的特性
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED |
Spliterator.IMMUTABLE | Spliterator.NONNULL |
Spliterator.DISTINCT | Spliterator.SORTED;
}

public int characteristics() {
if (beforeSplit) {
// 两个共同都有的特性: aSpliterator.characteristics() & bSpliterator.characteristics()
return aSpliterator.characteristics() & bSpliterator.characteristics()
& ~(Spliterator.DISTINCT | Spliterator.SORTED
| (unsized ? Spliterator.SIZED | Spliterator.SUBSIZED : 0));
}
else {
return bSpliterator.characteristics();
}
}

public interface Spliterator<T> {
public static final int ORDERED = 0x00000010;

public static final int DISTINCT = 0x00000001;

public static final int SORTED = 0x00000004;

public static final int SIZED = 0x00000040;

public static final int NONNULL = 0x00000100;

public static final int IMMUTABLE = 0x00000400;

public static final int CONCURRENT = 0x00001000;

public static final int SUBSIZED = 0x00004000;
}

二进制在一些记录中的头信息巧妙使用


mysql中 COMPACT行格式 中一条记录.
image.png


JVM中一个对象的二进制的巧妙使用
image.png


二进制解决一些有趣的问题



有1000桶酒,其中1桶有毒。而一旦吃了,毒性会在1周后发作。现在我们用小老鼠做实验,要在1周后找出那桶毒酒,问最少需要多少老鼠。



这个问题其实大家都见到过. 解题思路也巧妙地使用了二进制来完成.




  • 把10只老鼠依次排队为:0 - 9,关在笼子里




  • 把1000瓶药依次编号,并换算成二进制,如 8 = 1000,根据二进制中出现1的位数对相应位置小鼠喝药。 因为我们选择的是10只


    小鼠,2^10 = 1024 > 1000,能够保证所有的编号的酒都能被喂小鼠而不会遗漏被转为二进制的1的位.




  • 长时间的等待,等待小鼠的死亡




  • 把死亡小鼠的依次记录,然后10位的二进制中, 死亡老鼠编号对应10位中的二进制数为1, 没死的老鼠编号对应10为中的二进制数为0, 即知毒药的编号.


作者:DamonXue
链接:https://juejin.cn/post/7072328677652070431
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

二叉搜索树怎么这么难做呢?

首先我们需要去了解一下, 二叉搜索树的性质:对于 BST的每一个节点 node,左子树节点的值都比 node的值要小,右子树的值都要比node的值大。对于BST的每一个节点node, 它的左侧和右侧都是 BST...
继续阅读 »

首先我们需要去了解一下, 二叉搜索树的性质:

  1. 对于 BST的每一个节点 node,左子树节点的值都比 node的值要小,右子树的值都要比node的值大。
  2. 对于BST的每一个节点node, 它的左侧和右侧都是 BST

这里需要说明一下的是,从刷算法的角度来讲,还有一个重要的性质: BST的中序遍历的结果是有序的(升序)

那么我们开始吧, 最近一直拖更,很不好意思.

230. 二叉搜索树中第K个小的元素

image.png

思路梳理 如果这么理解, 产生一个升序的序列(数组),那么我们可以根据第k个小的元素 1,2,3,4,5这里第1个小的元素在哪里?那不就是1嘛.刚好借助于BST中序遍历的结果。是不是就很巧了呢。

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} k
* @return {number}
*/
// BST 的两个性质 左边的比右边的大
// 中序遍历的结果是 升序的

var kthSmallest = function(root, k) {
// 返回结果 记录结果
let res = 0;
// 记录升序之后的排名
let count = 0;
function traverse(root, k) {
// base case
if (root === null) return

traverse(root.left, k)
//中序
count++;
if (count === k) {
res = root.val;
return res;
}
traverse(root.right, k)
}

//定义函数
traverse(root, k)
return res;
}

538. 把二叉搜索树转换为累加树

image.png

image.png

思路梳理

其实这道题需要需要一个反过来的想法, BST的中序遍历的结果是升序的, 如果我们稍微作为一下修改呢?

function traverse(root) {

traverse(root.right)
// 中序遍历的结果是不是就成了 逆序(降序)的方式排列呢
// 这里做累加的结果
traverse(root.left)
}
traverse(root)

通过逆向的思考方式, 我从 8开始,也就是右子树开始依次去右中左的方式去遍历和累加和,是不是会更好一点呢,你可以思考一下,仔细去看一下那颗实例树:

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/

var convertBST = function(root) {
// 升序变为降序
// sum 记录累加和
let sum = 0;

function traverse(root) {
// base case
if (root === null) return

traverse(root.right)
// 维护累加值
sum += root.val;
root.val = sum;
traverse(root.left)
}
traverse(root)
return root;
}

1038. 从搜索树到更大的和树

image.png

image.png

思路梳理

这道题和上题完全一样的思路和写法这里就不做赘述了

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var bstToGst = function(root) {
// 升序变降序
// 记录累加和
let sum = 0;

function traverse(root) {
if (root === null) return;

traverse(root.right)
// 累加和
sum += root.val;
root.val = sum;
traverse(root.left)
}
traverse(root)
return root;
}

654. 最大二叉树

image.png

image.png

image.png

思路梳理

其实对于构建一颗二叉树最关键的是把root也就是把根节点找出来就好了。 那么我们就需要去遍历去找出数组中最大的值maxVal,把根节点root找出来了。 那么就可以把 maxVal左边和右边的数组进行递归,作为root的左右子树

// 伪代码
var constructMaximumBinaryTree = function([3,2,1,6,0,5]) {
// 找出数组中的最大值
let root = TreeNode(6)

let root.left = constructMaximumBinaryTree([3,2,1])
let root.right = constructMaximumBinaryTree([0,5])
return root;
}

这里我们需要的注意的是如何去构建一个递归函数: 参数是如何要求的? 因为需要 分离左子树和右子树 我们需要不断的确认子树的开始和结束的位置

function build (nums, start, end) {
// base case
if (left > right) return;

let maxValue = -1, index = -1; // index 是最大值的索引值是重要的分离条件
// find
for (let i = start; i < end; i++) {
if (nums[i] > maxValue) {
maxValue = nums[i];
index = i;
}
}
// 此时去构建树 root;
let root = new TreeNode(maxValue);

root.left = build(nums, start, index - 1)
root.right = build(nums, index + 1, end)
// 别忘了返回
return root;
}

这里其实我们已经写出来的本题的核心代码了,需要我们自己耐心的组合一下就好了啊

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} nums
* @return {TreeNode}
*/
var constructMaximumBinaryTree = function(nums) {
let root = build(nums, 0, nums.length-1)
return root;
}

function build(nums, start, end) {
// base case
if (left > right) return;

let maxVal = -1, index = -1;
for (let i = start; i < end; i++) {
if (nums[i] > maxVal) {
maxVal = nums[i]
index = i;
}
}

// 制作树
let root = new TreeNode(maxVal)

root.left = build(nums, start, index - 1)
root.right = build(nums, index + 1, end)
return root;
}

98. 验证二叉搜索树

image.png

思路分析

BST类似的代码逻辑就是利用 左小右大的特性

function BST(root, target) {
if (root.val === target) {
// 找到目标做点什么呢
}
if (root.val < target) {
// 右 比 root 大
BST(root.right, target)
}
if (root.val > target) {
// 左 比 root 小
BST(root.left, target)
}
}

那么对于我们去验证一棵树是不是合法的是需要注意一些事情的:

  1. 对于每一个节点 root代码值都需要去检查它的左右孩子节点是否都是符合左小右大的原则
  2. 从 BST的定义出发的话, root的整个左子树都要小于 root.val, 整个右子树都要大于 root.val

但是就会产生一个问题,就是对于某个节点,它只能管得了自己的左右子节点,如何去把约束关系传递给左右子树呢?

left.val < root.val < right.val

是不是可以借助于这种关系去约束他们呢?

主函数的定义

function isValidBST(root, null, null)

对于左子树来说 每一个左子树的 val 都需要满足于 min.val < val < root.val

isValidBST(root.left, min, root) 

那么同样的对于 每一个右子树的 val 都需要满足于 root.val < val < max.val

isValidBST(root.right, root, max)

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isValidBST = function(root) {
return isValid(root, null, null)
}

function isValid(root, min, max) {
// 怎么就合法了呢? 找完还一直没报false 不就是满足条件的吗
if (root === null) return true;
// 比最小的还小 不合法
if (min !== null && root.val <= min.val) return false;
// 比最大的还大。不合法
if (max !== null && root.val >= max.val) return false;

return isvalid(root.left, min, root) && isValid(root.right, root, max)
}

700. 二叉搜索树中的搜索

image.png

思路分析

其实,你看像不像我们上面提到的二叉树的思维模版呢? 题目要求是 找到对应的val的节点, 并返回以该节点为根的子树 其实就是可以理解为 返回当前节点就好了 会带有以 该节点为根的一颗子树的。

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} val
* @return {TreeNode}
*/

var searchBST = function(root, val) {
// base case
if (root === null) return null;

if (root.val === val) {
return root;
}

if (root.val < val) {
searchBST(root.right)
}
if (root.val > val) {
searchBST(root.left)
}

}

701. 二叉搜索树中的插入操作

image.png

image.png

思路分析

这里 需要注意的一点就是你你怎么样才能插入呢?如果是 target === val说明不是空位置, 那就插不进去啊, 所以我们要插入的位置肯定是一个空位置, 你如果认真分析过 这个过程 你对 base case 有没有一个全新的认识呢? 就是这里

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} val
* @return {TreeNode}
*/
var insertIntoBST = function(root, val) {
// base case
if (root === null) // 插入
return new TreeNode(val)

// if (root === val) 不需要了
if (root.val < val) {
insertIntoBST(root.right)
}
if (root.val > val) {
insertIntoBST(root.left)
}
}

450. 删除二叉搜索树中的节点

image.png

思路分析

先上一个基本的模版:

var deleteNode = function(root, key) {
// 基本的摹本
if (root === null) return null;

if (root.val === key) {
// 删除操作
}

if (root.val < key) {
deleteNode(root.right, val)
}
if (root.val > key) {
deleteNode(root.left, val)
}
}

当 root.val === key的时候 需要我们去执行一个删除的逻辑了

case1: 没有子孩子

if (root.left === null && root.right) reture null;

case2: 只有一个非空节点的情况,那么需要这个非空的节点接替自己的位置

if (root.left === null) return root.right;
if (root.right === null) return root.left;

case3: 如果有两个节点就麻烦了,我们就需要把 左子树中最大的或者是右子树中最小的元素来接替自己, 我们采用第二种方式

if (root.left !== null && root.right !== null) {
// 找到右子树中的最小的节点
let minNode = getMin(root.right)
// 把当前的值 替换为最小的值
root.val = minNode;
// 我们还需要把右子树中最小的值 删除掉
root.right = deleteNode(root.right, minNode.val)
}

获取右子树中最小的元素

// 其实这里的最小的就是 左子树
function getMin(node) {
while (node.left !== null) 继续找下面的左子树
node = node.left;
return node; // 没有发现和前端的原型链好像啊
}

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} key
* @return {TreeNode}
*/
var deleteNode = function(root, key) {
// 基本的摹本
if (root === null) return null;

if (root.val === key) {
// 删除操作
// case1, 2
if (root.left === null) return root.right;
if (root.right === null) return root.left;
// case 3
let minNode = getMin(root.right);
root.val = minNode.val; // 一加
root.right = deleteNode(root.right, minNode); // 一减
}

if (root.val < key) {
deleteNode(root.right, val)
}
if (root.val > key) {
deleteNode(root.left, val)
}

function getMin(node) {
while(node.left !== null) node = node.left;
return node;
}
}


作者:酒窝yun过去了
链接:https://juejin.cn/post/7070012823794876452
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

学不好Lambda,能学好Kotlin吗

嗯,当然 不能 进入正题,Kotlin中,高阶函数的身影无处不在,听上去高端大气上档次的高阶函数,简化一点讲,其实就是Lambda + 函数。 如果,Lambda学不好,就会导致高阶函数学不好,就会导致协程等等一系列的Kotlin核心学不好,Kotlin自然就...
继续阅读 »

嗯,当然


不能


进入正题,Kotlin中,高阶函数的身影无处不在,听上去高端大气上档次的高阶函数,简化一点讲,其实就是Lambda + 函数。


如果,Lambda学不好,就会导致高阶函数学不好,就会导致协程等等一系列的Kotlin核心学不好,Kotlin自然就一知半解了。所以,下面,一起来学习吧。


开始一个稍微复杂一点的实现


需求如下:传入一个参数,打印该参数,并且返回该参数


分析


乍看需求,这还不简单,一个print加一个return不就完事了,但是如果用Lambda,该怎么写呢?


val myPrint = { str: String ->
print("str is $str")
str
}


  • 这里划第一个重点,Lambda的最后一行作为返回值输出。此时,如果直接打印myPrint,是可以直接输出的


fun main() {
println(myPrint("this is kotlin"))
}

image.png


结果和预想一致。如果对这种函数的写法结构有什么疑惑的,可以查看juejin.cn/post/701173…


String.()


一脸懵逼,这是啥玩意?(此处应有表情 尼克杨问号脸)


先写个例子看看


val testStr : String.() -> Unit = {
print(this)
}


  • 官方一点解释,在.和()之间没有函数名,所以这是给String增加了一个匿名的扩展函数,这个函数的功能是打印String。在括号内,也就是Lambda体中,会持有String本身,也就是this。怎么调用呢?如下:


fun main() {
"hello kotlin".testStr()
}
// 执行结果:hello kotlin


  • 此外这里还有一个重点:扩展函数是可以全局调用的

  • 扩展函数有啥用?举个例子,如果对Glide提供的方法不满意,可以直接扩展一个Glide.xxx函数供自己调用,在xxx函数内部,可以取到this,也就是Glide本身。

  • 有兴趣可以看一下Compose的源码,原来扩展函数还可以这么用


终极形态


先看代码


val addInt : Int.(Int) -> String = {
"两数相加的结果是${this + it}"
}

用已有的知识分析一下:



  • Int.():匿名的扩展函数

  • this:当前的Int,也就是调用这个扩展函数的对象

  • "两数相加的结果是${this + it}" : Lambda的最后一行,也就返回值


如何调用


一般有如下两种调用方式:


fun main() {
println(addInt(1,2))
println(1.addInt(2))
}


  • 第二种更加符合规范,之所以可以有第一种写法,是因为this会默认作为第一个参数

  • 此处可以记住一个知识点,扩展了某一个函数,扩展函数内部的this就是被扩展的函数本身


Kotlin函数返回值那些事


在Kotlin函数中,如果不指定函数的返回值类型,则默认为Unit


fun output() {println("helle kotlin")}


  • 上述函数的返回值为Unit类型


当函数体中出现return的时候,则需要手动为函数指定类型


fun output2() : Int {
return 0
}


  • 返回Int类型的0,需要手动指定函数的返回值类型,否则报错


如果是以下的函数,那么返回值为?


fun output3() = {}


  • 此处的返回值为() -> Unit,可省略,写全了,就是如下的样子:


fun output3() : () -> Unit = {}


  • 此处函数返回函数,已经是高阶函数的范畴了


如果函数接着套一个函数呢,比如


fun output4() = run { println("hello kotlin") }


  • 虽说run是一个函数,但是此处的返回值就不是() -> Unit

  • 此处的返回就是run的返回值,但是run是什么?


@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}


  • run的作用就是执行内部的函数,在这里就是println方法。

  • run的返回自是R,也就是泛型,具体一点就是println的返回值,这里println的返回值是Unit,所以可以得出上面的output4的返回值就是Unit。

  • 这里如果不是很懂的话,可以看一个简单一点的例子


fun output5() = run {true}


  • 此处,函数的返回值就是true的类型,Boolen


函数中套一个函数怎么传参呢


刚刚的例子中,知道了怎么写一个函数中套函数,那么其中嵌套得函数怎么传参呢


fun output6() = {a: Int ->  println("this is $a")}


  • a为参数,函数是println,所以output6的返回值类型为(Int) -> Unit

  • 如果需要调用的话,需要这么写:


output6()(1)

最后一个重点:在写Lambda的时候,记住换行


几种函数写法的区别


fun a()


常见的函数


val a = {}


a是一个变量,只不过是一个接受了匿名函数的变量,可以执行这个函数,和第一种现象一致。


这里的a还可以赋值给另一个变量 val a2 = a,但是函数本身不能直接赋给一个变量,可以使用::,让函数本身变成函数的引用


--end---


作者:鸣乔
链接:https://juejin.cn/post/7018184770873983007
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

已开源!Flutter 流畅度优化组件 Keframe

列表流畅度优化这是一个通用的流畅度优化方案,通过分帧渲染优化由构建导致的卡顿,例如页面切换或者复杂列表快速滚动的场景。代码中 example 运行在 VIVO X23(骁龙 660),在相同的滚动操作下优化前后 200 帧采集数据指标对比(录屏在文章最后):优...
继续阅读 »

列表流畅度优化

这是一个通用的流畅度优化方案,通过分帧渲染优化由构建导致的卡顿,例如页面切换或者复杂列表快速滚动的场景。

代码中 example 运行在 VIVO X23(骁龙 660),在相同的滚动操作下优化前后 200 帧采集数据指标对比(录屏在文章最后):

优化前优化后
优化前优化后

监控工具来自:fps_monitor,指标详细信息:页面流畅度不再是谜!调试神器开箱即用,Flutter FPS检测工具

  • 流畅:一帧耗时低于 18ms
  • 良好:一帧耗时在 18ms-33ms 之间
  • 轻微卡顿:一帧耗时在 33ms-67ms 之间
  • 卡顿:一帧耗时大于 66.7ms

采用分帧优化后,卡顿次数从 平均 33.3 帧出现了一帧,降低到 200 帧中仅出现了一帧,峰值也从 188ms 降低到 90ms。卡顿现象大幅减轻,流畅帧占比显著提升,整体表现更流畅。下方是详细数据。

优化前优化后
平均多少帧出现一帧卡顿33.3200
平均多少帧出现一帧轻微卡顿8.666.7
最大耗时188.0ms90.0ms
平均耗时27.0ms19.4ms
流畅帧占比40%64.5%

页面切换流畅度提升

在打开一个页面或者 Tab 切换时,系统会渲染整个页面并结合动画完成页面切换。对于复杂页面,同样会出现卡顿掉帧。借助分帧组件,将页面的构建逐帧拆解,通过 DevTools 中的性能工具查看。切换过程的峰值由 112.5ms 降低到 30.2 ms,整体切换过程更加流畅。

image.pngimage.png

实际优化合集:Keframe 流畅度优化实践合集

如何使用?

项目依赖:

在 pubspec.yaml 中添加 keframe 依赖

dependencies:
keframe: version

组件仅区分非空安全与空安全版本

非空安全使用: 1.0.2

空安全版本使用: 2.0.2

github 地址:github.com/LianjiaTech…

pub 查看:pub.dev/packages/ke…

Dont forget star ~

快速上手:

如下图所示

image.png

假如现在页面由 A、B、C、D 四部分组成,每部分耗时 10ms,在页面时构建为 40ms。使用分帧组件 FrameSeparateWidget 嵌套每一个部分。页面构建时会在第一帧渲染简单的占位,在后续四帧内分别渲染 A、B、C、D。

对于列表,在每一个 item 中嵌套 FrameSeparateWidget,并将 ListView 嵌套在 SizeCacheWidget 内即可。

image.png


构造函数说明

FrameSeparateWidget :分帧组件,将嵌套的 widget 单独一帧渲染

类型参数名是否必填含义
Keykey
intindex分帧组件 id,使用 SizeCacheWidget 的场景必传,SizeCacheWidget 中维护了 index 对应的 Size 信息
Widgetchild实际需要渲染的 widget
WidgetplaceHolder占位 widget,尽量设置简单的占位,不传默认是 Container()

SizeCacheWidget:缓存子节点中,分帧组件嵌套的实际 widget 的尺寸信息

类型参数名是否必填含义
Keykey
Widgetchild子节点中如果包含分帧组件,则缓存实际的 widget 尺寸
intestimateCount预估屏幕上子节点的数量,提高快速滚动时的响应速度

方案设计与分析:

卡顿的本质,就是 单帧的绘制时间过长。基于此自然衍生出两种思路解决:

1、减少一帧的绘制耗时,因为导致耗时过长的原因有很多,比如不合理的刷新,或者绘制时间过长,都有可能,需要具体问题具体分析,后面我会分享一些我的优化经验。

2、在不对耗时优化下,将一帧的任务拆分到多帧内,保证每一帧都不超时。这也是本组件的设计思路,分帧渲染。

如下图所示:

image.png

原理并不复杂,问题在于如何在 Flutter 中实践这一机制。

因为涉及到帧与系统的调度,自然联想到看 SchedulerBinding 中有无现成的 API。

发现了 scheduleTask 方法,这是系统提供的一个执行任务的方法,但这个方法存在两个问题:

  • 1、其中的渲染任务是优先级进行堆排序,而堆排序是不稳定排序,这会导致任务的执行顺序并非 FIFO。从效果上来看,就是列表不会按照顺序渲染,而是会出现跳动渲染的情况

  • 2、这个方法本身存在调度问题,我已经提交 issue 与 pr,不过一直卡在单元测试上,如果感兴趣可以以在这里交流谈论。

fix: Tasks scheduled through 'SchedulerBinding.instance.scheduleTask'… #82781

最终,参考这个设计结合 endOfFrame 方法的使用,完成了分帧队列。整个渲染流程变为下图所示:

image.png

对于列表构建场景来说,假设屏幕上能显示五个 item。首先在第一帧的时候,列表会渲染 5 个占位的 Widget,同时添加 5 个高优先级任务到队列中,这里的任务可以是简单的将占位 Widget 和实际 item进行替换,也可通过渐变等动画提升体验。在后续的五帧中占位 Widget 依次被替换成实际的列表 item。

在 ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案 这篇文章中有更加详细的分析。


一些展示效果(Example 说明请查看 Github

卡顿的页面往往都是由多个复杂 widget 同时渲染导致。通过为复杂的 widget 嵌套分帧组件 FrameSeparateWidget。渲染时,分帧组件会在第一帧同时渲染多个 palceHolder,之后连续的多帧内依次渲染复杂子项,以此提升页面流畅度。

例如 example 中的优化前示例:

ListView.builder(
itemCount: childCount,
itemBuilder: (c, i) => CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
)

其中 CellWidget 高度为 60,内部嵌套了三个 TextField 的组件(整体构建耗时在 9ms 左右)。

优化仅需为每一个 item 嵌套分帧组件,并为其设置 placeHolder(placeHolder 尽量简单,样式与实际 item 接近即可)。

在列表情况下,给 ListView 嵌套 SizeCacheWidget,同时建议将预加载范围 cacheExtent 设置大一点,例如 500(该属性默认为 250),提升慢速滑动时候的体验。

Screenrecording_20210611_194905.gif (占位与实际列表项不一致时,首次渲染抖动,二次渲染正常)

此外,也可以给 item 嵌套透明度/位移等动画,优化视觉上的效果。

效果如下图:

Screenrecording_20210315_133310.gifScreenrecording_20210315_133848.gif

分帧的成本

当然分帧方案也非十全十美,在我看来主要有两点成本:

1、额外的构建开销:整个构建过程的构建消耗由「n * widget消耗 」变成了「n *( widget + 占位)消耗 + 系统调度 n 帧消耗」。可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,测试后发现整体构建耗时大概提升在 15 % 左右。这种额外开销对于当下的移动设备而言,成本几乎可以不计。

2、视觉上的变化:如同上面的演示,组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。但其实由于列表存在缓存区域(建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。


优化前后对比演示

注:gif 帧率只有20

优化前优化后
优化前优化后

最后:一点点思考

列表优化篇到此告一段落,在整个开源实践过程中,有两点感触较深:

「点」与「面」的关系

我们在思考技术方案的时候可以由「点」到「面」,站在一个较高视野去想问题的本质。

而在执行的时候则需要由「面」到「点」的进行逐级拆分,抓住问题的关键节点,并且拟定进度计划,逐步破解。

很多时候,这种向上和向下的逻辑思维才是我们的核心竞争力

以不变应万变

对于未知的东西,我们往往会过度的将它想复杂。在一开始分析列表构建原理的时候,我也苦于无从下手,走了很多弯路。但其实对于 Flutter 这套 「UI」 框架而言,核心仍然在于三棵树的构建机制

在这套体系内,抓住不变的东西,无论是生命周期、路由等等问题都可以从里面找到答案。我之前也有过总结:Flutter 核心渲染机制 与 Flutter路由设计与源码解析 。


作者:Nayuta
链接:https://juejin.cn/post/6979781997568884766
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

如何用charts_flutter创建Flutter图表

应用程序中的图表提供了数据的图形显示或图画表示,跨越了行业和应用程序。像Mint这样的移动应用程序使用饼状图来监测消费习惯,像Strava这样的健身应用程序使用线状图和条状图来分析步幅、心率和海拔高度。在构建Flutter应用程序时,开发者可以使用由谷歌维护的...
继续阅读 »

应用程序中的图表提供了数据的图形显示或图画表示,跨越了行业和应用程序。像Mint这样的移动应用程序使用饼状图来监测消费习惯,像Strava这样的健身应用程序使用线状图和条状图来分析步幅、心率和海拔高度。

在构建Flutter应用程序时,开发者可以使用由谷歌维护的官方charts_flutter 库来创建这些类型的图表。

在本教程中,我们将学习如何使用charts_flutter 创建一些最常见的图表--线形图、饼图和条形图。

我们将用这些图表来显示一个虚构的Flutter图表开发者社区五年来的增长情况。虽然本教程中的数据是捏造的,但本教程可以很容易地利用真实数据。

前提条件

要学习本教程,您必须具备以下条件。

创建并设置一个Flutter项目charts_flutter

要创建一个新的Flutter项目,运行以下命令。

flutter create projectName

接下来,在您的代码编辑器中打开这个新项目。如上所述,我们将使用[chart_flutter](https://pub.dev/packages/charts_flutter) ,Flutter的官方库

要将chart_flutter 导入您的项目,请打开pubspec.yaml 文件并将其添加到依赖项下。

dependencies:
flutter:
sdk: flutter

charts_flutter: ^0.11.0

构建应用程序的脚手架

现在我们有了新的Flutter应用程序所附带的基本代码:一个记录按钮被按下多少次的计数器。

由于我们的条形图应用程序中不需要这个,继续删除在main.dart 页面中发现的代码。删除所有的内容,除了下面的内容。

import ‘package:flutter/material.dart’;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//TODO: implement build
Return MaterialApp(
);
}
}

现在,在我们的构建部件中返回MaterialApp 类,以便我们可以使用Material Design。

创建一个主页

要为我们的应用程序创建一个主页,请导航到lib 文件夹,并创建一个名为home.dart 的新页面。

import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ,
),
);
}
}

通过import 'package:flutter/material.dart' ,我们就可以导入Material Design。

然后,HomePage 类扩展了statelessWidget ,因为在这个页面上没有状态变化。

BuildContext widget里面,我们返回Scaffold 类,给我们一个基本的Material Design布局结构。我们的条形图将放在子参数的位置,我们将把它放在屏幕主体的中心。

所有这些现在都成为我们应用程序的支架。

随着主页的完成,我们可以在我们的main.dart 文件中指定HomePage ,因为main.dart 将我们应用程序中的所有功能集中在一起。

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(), //This is where we specify our homepage
);
}
}

有了这段代码,main.dart 就知道每当应用加载时首先显示哪一页。

请注意,将debugShowCheckedModeBanner 设置为false ,就可以从我们的应用程序中删除调试标记。

创建一个Flutter图表应用程序

系列和模型

在我们创建图表应用之前,让我们熟悉一下Flutter图表常用的两个术语:系列和模型。

系列是一组(或系列)的信息,我们可以用它来绘制我们的图表。一个模型是我们的信息的格式,它规定了使用该模型的每个数据项必须具有的属性。

创建一个条形图

为柱状图数据创建一个模型

首先,我们将创建一个柱状图,以显示在过去五年中新增的虚构Flutter图表开发者的数量。换句话说,我们要跟踪虚构的Flutter图表社区的增长情况。

我们的模型,定义了我们的数据格式,包括我们要看的年份,那一年加入Flutter图表社区的开发者数量,以及相应条形图的颜色。

lib 文件夹中,创建一个名为developer_series.dart 的文件。下面,实现我们模型的代码。

import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter/foundation.dart';

class DeveloperSeries {
final int year;
final int developers;
final charts.Color barColor;

DeveloperSeries(
{
@required this.year,
@required this.developers,
@required this.barColor
}
);
}

我们将模型命名为DeveloperSeries ,并指定了每个系列项目必须具备的属性(year,developers, 和barColor )。

为了防止在创建一个类的对象时,该类的参数为空,我们使用了@required 注释,如上面的代码块中所示。

要使用@required 关键字,我们必须导入foundation.dart 包。

为柱状图创建数据

现在我们有了一个条形图数据的模型,让我们继续实际创建一些数据。在主页上,通过添加以下内容为柱状图生成数据。

 import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:;lib/developer_series.dart';

class HomePage extends StatelessWidget {
final List<DeveloperSeries> data = [

DeveloperSeries(
year: "2017",
developers: 40000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
DeveloperSeries(
year: "2018",
developers: 5000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
DeveloperSeries(
year: "2019",
developers: 40000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
DeveloperSeries(
year: "2020",
developers: 35000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
DeveloperSeries(
year: "2021",
developers: 45000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
];

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ,
),
);
}
}

这是一个名为data 的简单列表。列表中的每一个项目都是按照DeveloperSeries 的模型制作的,也就是说每一个项目都有一个年份(year)、开发者的数量(developers)和条形图的颜色(barColor)的属性。

请注意,上面的数据是真实的,所以请随意操作这些数字和颜色。

构建柱状图

我们已经成功地为我们的柱状图创建了数据。现在,让我们来创建条形图本身。为了使我们的项目有条理,我们将把柱状图的代码放在一个单独的文件中。

lib ,创建一个developer_chart.dart 文件。

import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:lib/developer_series.dart';

class DeveloperChart extends StatelessWidget {
final List<DeveloperSeries> data;

DeveloperChart({@required this.data});
@override
Widget build(BuildContext context) {
List<charts.Series<DeveloperSeries, String>> series = [
charts.Series(
id: "developers",
data: data,
domainFn: (DeveloperSeries series, _) => series.year,
measureFn: (DeveloperSeries series, _) => series.developers,
colorFn: (DeveloperSeries series, _) => series.barColor
)
];

Return charts.Barchart(series, animate: true);
}

}

通过final List<DeveloperSeries> data ,我们定义了一个名为data 的列表,它是我们之前创建的DeveloperSeries 模型形式的数据项的List 。

列表中的每一个数据项都带有相应的年份、开发人员的数量和条形颜色。

类中的DeveloperChart 构造函数确保在使用条形图类的任何地方,它所需要的数据总是被提供;这是用@required 关键字完成的。

实际的柱状图是在我们的构建部件中创建的。如你所知,所有的柱状图都有几组数据相互对照(在我们的例子中,过去五年和Flutter图表社区获得的开发者数量)。

这些数据组在一起,被称为系列。系列告诉我们Flutter要把哪一组数据放在我们条形图的水平面,哪一组放在垂直面。

然后,我们先前创建的数据列表插入到我们的系列中,并由Flutter适当地使用。

通过List<charts.Series<DeveloperSeries, String>> series ,我们创建了一个名为series 的列表。这个列表的类型为charts.Series ;charts 将Flutter导入我们的项目,Series 函数为Flutter中的柱状图创建系列。

我们刚刚创建的系列是以我们的DeveloperSeries 模型为模型。

我们将在系列中指定的参数包括:id,data,domainFn,measureFn, 和colorFN 。

  • id 标识了图表
  • data 指向要在柱状图上绘制的项目列表
  • domainFn 指向柱状图水平方向上的数值。
  • measureFn 指向垂直方向上的数值的数量
  • colorFN 指的是条形图的颜色

通过domainFn,measureFn, 和colorFN 函数,我们创建了以Subscriber 系列为参数的函数,创建了它的实例,然后使用这些实例来访问它的不同属性。

developer_chart.dart 文件中的下划线标志着第二个参数是不需要的。

在将我们的系列指向它所需要的所有数据后,我们再使用Flutter的BarChart 函数来创建我们的柱状图。

我们还可以通过简单地将animate 设置为true 来添加一个动画,以获得视觉上的吸引力,这将使图表呈现出一个漂亮的动画。

将柱状图添加到主页上

现在,我们可以将新创建的柱状图添加到我们的主页上并显示。

import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:lib/developer_series.dart';
import 'package:lib/developer_chart.dart';

class HomePage extends StatelessWidget {
// ...

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: DeveloperChart(
data: data,
)
),
);
}
}

在这里,我们只需在我们的页面主体内调用DeveloperChart 类,并将其指向我们要使用的数据。

为了确保我们的图表能够很好地在屏幕上显示,我们将把它放在一个Card ,在它周围包裹一个容器,并给它设置一个高度和一些填充。



class DeveloperChart extends StatelessWidget {
final List<DeveloperSeries> data;

DeveloperChart({@required this.data});
@override
Widget build(BuildContext context) {
List<charts.Series<DeveloperSeries, String>> series = [
charts.Series(
id: "developers",
data: data,
domainFn: (DeveloperSeries series, _) => series.year,
measureFn: (DeveloperSeries series, _) => series.developers,
colorFn: (DeveloperSeries series, _) => series.barColor
)
];

return Container(
height: 300,
padding: EdgeInsets.all(25),
child: Card(
child: Padding(
padding: const EdgeInsets.all(9.0),
child: Column(
children: <Widget>[
Text(
"Yearly Growth in the Flutter Community",
style: Theme.of(context).textTheme.body2,
),
Expanded(
child: charts.BarChart(series, animate: true),
)
],
),
),
),
);
}

}

通过使用expanded widget,我们将我们的柱状图很好地扩展到Card 。上面的Text widget给了我们的柱状图一个标题,让人们知道它是关于什么的。

而且,通过Theme.of(context).textTheme.body2 ,我们将Material Design默认的正文样式应用于我们的标题。

通过padding: const EdgeInsets.all(9.0) ,我们给容纳我们的条形图的卡片在所有边上加了9px的填充。最后,我们将Card 包裹在一个容器中,并给这个容器一个300px的高度和25px的边距。

现在,我们的条形图应该能很好地呈现在我们的屏幕上。

Flutter Bar Chart, Showing Growth Of The Flutter Chart Community Over Five Years With Five Green Bars, With Dates Ranging From 2017 To 2021

创建饼状图

我们也可以使用charts_flutter 包来创建饼图。事实上,我们上面遵循的程序和我们编写的代码可以创建饼图。

要将我们创建的条形图改为饼图,只需将charts.BarChart(series, animate: true) 改为child:( charts.PieChart(series, animate: true) 。

然后我们就可以在饼图上添加标签。

Expanded(
child: charts.PieChart(series,
defaultRenderer: charts.ArcRendererConfig(
arcRendererDecorators: [
charts.ArcLabelDecorator(
labelPosition: charts.ArcLabelPosition.inside)
])
animate: true),
)

ArcRendererConfig 函数可以配置饼图的外观,我们可以使用ArcLabelDecorator 函数为饼图添加标签。

labelPosition 指定将标签放在哪里,是放在里面还是外面;在这种情况下,我们指定标签应该放在外面。

Flutter Pie Chart Shows Flutter Chart Community Growth Over Five Years In Green Chart With Dates Ranging From 2017 To 2021

创建折线图

我们可以用创建其他两个图表的同样方法来创建一个折线图。我们只需对我们的数据配置做一个小小的调整。

在我们的系列列表中,List<charts.Series<DeveloperSeries, String>> 变成List<charts.Series<DeveloperSeries, num>> ,因为折线图只对数字起作用。

List<charts.Series<DeveloperSeries, num>> series = [
charts.Series(
id: "developers",
data: data,
domainFn: (DeveloperSeries series, _) => series.year,
measureFn: (DeveloperSeries series, _) => series.developers,
colorFn: (DeveloperSeries series, _) => series.barColor
)
];

现在我们可以把charts.PieChart 改为charts.Linechart ,从而得到我们的折线图。默认情况下,折线图是从原点零开始的。但是我们所关注的年份是从2016年到2021年。因此,这里是如何使我们的折线图跨越这个范围的。

Expanded(
child: charts.LineChart(series,
domainAxis: const charts.NumericAxisSpec(
tickProviderSpec:
charts.BasicNumericTickProviderSpec(zeroBound: false),
viewport: charts.NumericExtents(2016.0, 2022.0),
),
animate: true),
)

NumericAxisSpec 函数为图表中的轴设置规格。通过BasicNumericTickProviderSpec 函数,我们将zeroBound 设置为false ,这样我们的图表就不会从原点零开始。

最后,通过NumericExtents 函数,我们设置了我们希望我们的坐标轴所跨越的范围。

Flutter Line Chart With Community Growth Over The Years 2017 To 2021 Indicated By Green Line

总结

本教程的目的是向Flutter开发者展示如何在其应用程序中实现不同的图表。使用谷歌创建的强大的charts_flutter 包,我们能够实现一个柱状图、一个饼状图和一个线形图。


作者:前端小工
链接:https://juejin.cn/post/7061863199301173278
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Java并发-ThreadLocal

Java并发-ThreadLocal ThreadLocal简介: 多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。 而Threa...
继续阅读 »

Java并发-ThreadLocal


ThreadLocal简介:


多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。


而ThreadLocal是除了加锁这种同步方式之外的另一种保证一种规避多线程访问出现线程不安全的方法,线程并发的安全问题在于多个线程同时操作同一个变量的时候,不加以额外限制会存在线程争着写入导致数据写入不符合预期,如果我们在创建一个变量后,每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。


ThreadLocal的典型适用场景:


典型场景1:


每一个线程需要有一个独享的对象(通常是工具类,典型比如SimpleDateFormat,Random)。


以代码为例,通过SimpleDateFormat实现时间戳转换为格式化时间串的功能:


public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
String result = toDate(1000L + finalI);
}
});
}
threadPool.shutdown();
}

public static String toDate(long seconds){
Date currentDate = new Date(seconds *1000);
SimpleDateFormat formator = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return formator.format(currentDate);
}

上面的代码其实是没有线程安全问题的,但是存在的不足是我们调用了1000次,创建了1000次SimpleDateFormat对象,为了结局这个问题,我们可以把SimpleDateFormat对象从toDate中抽离出来,成为一个全局的变量:


static SimpleDateFormat formator = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

public static String toDate(long seconds){
Date currentDate = new Date(seconds *1000);
return formator.format(currentDate);
}
// output:
1970-01-01 08:33:06
1970-01-01 08:33:07
1970-01-01 08:32:47
1970-01-01 08:32:47
1970-01-01 08:33:10
1970-01-01 08:33:11

很容易发现,全局唯一的formator对象,因为没有加锁,是有线程安全问题的,那么我们可以通过加锁修复:


public static String toDate(long seconds){
Date currentDate = new Date(seconds *1000);
synchronized (formator){
return formator.format(currentDate);
}
}

虽然修复了线程安全问题,但是随之而来的,synchronized关键字导致各个线程需要频繁的申请锁资源,等待锁资源释放,释放锁资源,这并不划算,而利用ThreadLocal这个工具类,可以很方便的解决问题:


ThreadLocal改造如下:


class FormatorThreadLocalGetter {
public static ThreadLocal<SimpleDateFormat> formator = new ThreadLocal<>() {
@Override
protected SimpleDateFormat initialValue() {
return new SSimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};

// or use Lambda
public static ThreadLocal<SimpleDateFormat> formator2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

public class ThreadLocalTest {
// 这里我们使用COW容器记录下每一个SimpleDateFormator的hashcode
static CopyOnWriteArraySet<String> hashSet = new CopyOnWriteArraySet<String>();

public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println(toDate(1000L + finalI));
}
});
}
threadPool.shutdown();
Thread.sleep(5000);
// 延迟5s,确保所有的输出都执行完毕,然后看看我们创建了多少个formator对象。
System.out.println(hashSet.size());
hashSet.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
}

public static String toDate(long seconds) {
Date currentDate = new Date(seconds * 1000);
SimpleDateFormat formator = FormatorThreadLocalGetter.formator.get();
// 将当前线程的formator的hashcode记录下来,看看最终有多少个hashCode
hashSet.add(String.valueOf(formator.hashCode()));
// 通过ThreadLocal去get一个formator。
return FormatorThreadLocalGetter.formator.get().format(currentDate);
}
}

// 这里我们需要override一下hashCode函数,因为默认的hashCode生成规则是
// 调用构造函数入参pattern这个String对象的hashCode,因为所有的formator
// 的pattern都一样,不重写一下会发现hashCode都一样。
class SSimpleDateFormat extends SimpleDateFormat {
private int hashCode = 0;
SSimpleDateFormat(String pattern) {
super(pattern);
}

@Override
public int hashCode() {
if (hashCode > 0) {
return hashCode;
}
hashCode = UUID.randomUUID().hashCode();
return hashCode;
}
}
// output:
1970-01-01 08:33:15
1970-01-01 08:33:10
1970-01-01 08:33:18
23 // 一千次任务总共创建了23个formator对象
-674481611
-424833271
-2124230669
411606156
-1600493931
900910308
540382160
-1054803206
...

因为线程池执行1000次任务并不是只创建了10个线程,其中仍然包括线程的销毁和新建,因此通常而言是不止10个formator对象被创建,符合预期。


典型场景2:


每个线程内需要有保存自己线程内部全局变量的地方,可以让不同的方法直接使用,避免参数传递麻烦,同时规避线程不安全行为。


典型场景2其实对于客户端来说比较少见,但是可以作为ThreadLocal的另外用法的演示,在使用场景1中,我们用到的是在ThreadLocal对象构造的时候主动去初始化我们希望通过它去保存的线程独有对象。


下面的场景是用来演示主动给ThreadLocal赋值:


举个例子如下图所示,每一个请求都是在一个thread中被处理的,然后通过层层Handler去传递和处理user信息。


image-20220223005746413.png


这些信息在同一个线程内都是相同的,但是不同的线程使用的业务内容user是不同的,这个时候我们不能简单通过一个全局的变量去存储,因为这个全局变量是线程间都可见的,为此,我们可以声明一个map结构,去保存每一个线程所独有的user信息,key是这个线程,value是我们要保存的内容,为了线程安全,我们可以采取两种方式:



  1. 给map的操作加锁(synchronized等)。

  2. 借助线程安全的map数据结构来实现这个map,比如ConcurrentHashMap。


但是无论加锁还是CHM去实现,都免不得会面临线程同步互斥的压力,而这个场景下,ThreadLocal就是一个非常好的解决方式,无需同步互斥机制,在不影响性能的前提下,user参数也不需要通过函数入参的方式去层层传递,就可以达到保存当前线程(request)对应的用户信息的目的。


简单实现如下:


class UserContextHolder{
public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class Handler1{
public void handle(){
User user = new User();
user.name = "UserInfo" + user.hashCode();
// handler1通过set方法给当前线程的ThreadLoca<User>赋值
UserContextHolder.holder.set(user);
}
}

class Handler2{
public void handle(){
// handler2通过get方法获取到当前线程对应的user信息。
System.out.println("UserInfo:" + UserContextHolder.holder.get());
}
}

通过上面的例子,我们可以总结出ThreadLocal几个好处:



  1. 线程安全的存储方式,因为每一个线程都会有自己独有的一份数据,不存在并发安全性。

  2. 不需要加锁,执行效率肯定是比加锁同步的方式更高的。

  3. 可以更高效利用内存,节省内存开销,见场景1,几遍有1000个任务,相比于原始的创建1000个SimpleDateFormator对象或者加锁,显然ThreadLocal是更好的方案。

  4. 从场景2,我们也能看出,在某些场景下,可以简化我们传参的繁琐流程,降低代码的耦合程度。


ThreadLocal原理分析:


理解ThreadLocal需要先知道Thread,ThreadLocal,ThreadLocalMap三者之间的关系,如下图所示:


image-20220223012629697.png


每一个Thread对象内部都有一个ThreadLocalMap变量,这个Map是一个散列表的数据结构,而Map的Entry的key是ThreadLocal对象,Value就是ThreadLocal对象要保存的Value对象。


ThreadLocalMap本身是一个数组结构的散列表,并非传统定义的Map结构,ThreadLocalMap在遇到hash冲突的时候,采用的是线性探测法去解决冲突 ,数组存放的Entry是ThreadLocal和Value对象。


static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

**尤其需要注意,ThreadLocal工作机制的核心是线程持有的ThreadLocalMap这个数据结构,而不是ThreadLocal自身。**有点绕,可以看下文的分析。


核心API解析:


initialValue:


该方法会返回当前线程对应的数据的初始值,并且这是一个延迟初始化的方法,不会在ThreadLocal对象构造的时候调用,而是在线程调用ThreadLocal#get方法的时候调用到。


get:


得到这个线程对应的Value值,如果首次调用,会通过initialize这个方法初始化。


set:


为这个线程设置一个新的Value。


remove:


删除这个线程设置的Value,后续再get,就会再次initialValue。


源码如下:


public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// JDK源码中上面两行代码其实等价于:
// ThreadLocalMap map = t.threadLocals
if (map != null) {
// 获取到当前线程的ThreadLocalMap中存放的Entry
// Entry的key其实就是this(ThreadLocal本身)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
// 返回ThreadLocal存放的对象本身。
return result;
}
}
// 如果上面没有找到,那么就会初始化
return setInitialValue();
}

setInitialValue实现如下:


private T setInitialValue() {
// 调用initialValue初始化ThreadLocal要保存的对象。
T value = initialValue();
Thread t = Thread.currentThread();
// 拿到当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// Thread#ThreadLocalMap的初始值是null
if (map != null) {
// 如果map有了,set存
map.set(this, value);
} else {
// 否则就给当前Thread的threadLocals(即ThreadLocalMap)赋值并存入
// 上面创建的Value。
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
// 将Value返回,作为get的返回值。
return value;
}

再来看下set操作实现,其实就是做一件事,如果线程已经有了ThreadLocalMap,那么就直接存Value,如果线程没有ThreadLocalMap,就创建Map并且存Value。


    public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

在这里我们也可以看到如果是通过initialValue将Value的初始值写入,那么就会调用setInitialValue,如果是通过set写入初始值,那么不会调用到setInitialValue。


同时,需要注意,initialValue通常是只会调用一次的,同一个线程重复get并不会触发多次init操作,但是如果通过remove这个API,主动移除Value,后续再get,还是会触发到initialValue这个方法的。


public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

如果我们不主动重写initialValue这个方法,默认是返回null的,一般使用匿名内部类的方法来重写initialValue方法,这样方便在后续的使用中,可以直接使用,但是要注意,initialValue除非主动remove,否则是只会调用一次的,即仍然需要做空值确认。


ThreadLocal内存泄露:


ThreadLocal被讨论的最多的就是它可能导致内存泄露的问题。


我们看下ThreadLocalMap#Entry的定义:


static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

ThreadLocalMap对Entry的引用是强引用,Entry对ThreadLocal的引用是弱引用,但是对Value的引用是强引用,这就可能会导致内存泄露。


正常情况下,当线程终止的时候,会将threadLocals置空,这看起来没有问题。


but:


如果线程不能终止,或者线程的存活时间比较久,那么Value对象将始终得不到回收,而如果Value对象再持有其它对象,比如Android当中的Activity,就会导致Activity的内存泄露,(Activity被销毁了,但是因为Value绑定的Thread还在运行状态,将导致Activity对象无法被GC回收)。


这个时候引用链就变成了如下:


Thread->ThreadLocalMap->Entry(key是null,Value不为空)->Value->Activity。


当然JDK其实已经考虑了这个问题,ThreadLocalMap在set,remove,rehash等方法中,都会主动扫描key为null的Entry,然后把对应的Value设置为null,这样原来Value对应的对象就可以被回收。


以resize为例:


private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (Entry e : oldTab) {
if (e != null) {
ThreadLocal<?> k = e.get();
// 遍历到key为null的时候就将value也设置为null。
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}

但是但是,还是有问题:


如果Thread一直在运行,但是其所持有的ThreadLocalMap又没被用到了,此事上面那些set,remove,rehash方法都不会被调用,那还是存在内存泄露的问题......


按照阿里Java规范,ThreadLocal的最佳实践需要在ThreadLocal用完之后,主动去remove,回到典型场景2的代码,我们需要在Handler2的末尾,执行ThreadLoca.remove操作,或者在Handler链路过程中,如果逻辑无法运行到Handler2末尾,相应的异常处也需要处理remove。


装箱拆箱的NPE问题:


如果使用ThreadLocal去保存基本数据类型,需要注意空指针异常,因为ThreadLocal保存的只能是封箱之后的Object类型,在做拆箱操作的时候需要兼容空指针,如下代码所示:


public class ThreadLocalNPE {
static ThreadLocal<Integer> intHolder = new ThreadLocal<>();
static int getV(){
return intHolder.get();
}
public static void main(String[] args) {
getV();// 抛异常
}
}

原因是我们在get之前没有主动set去赋值,getV中intHolder.get先拿到一个Integer的null值,null值转换为基本数据类型,当然报错,将getV的返回值修改为Integer即可。


作者:AaronXYZ
链接:https://juejin.cn/post/7067727693126565901
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

撸一下ThreadPoolExecutor核心思路

ThreadPoolExecutor中知识点很多,本文只是从7个构造参数入手,看看其运转的核心思路。重点不是扣代码,是体会设计思想哈! 欢迎纠错和沟通。 ThreadPoolExecutor 以下是构造ThreadPoolExecutor的7大参数。 publ...
继续阅读 »

ThreadPoolExecutor中知识点很多,本文只是从7个构造参数入手,看看其运转的核心思路。重点不是扣代码,是体会设计思想哈!
欢迎纠错和沟通。


ThreadPoolExecutor


以下是构造ThreadPoolExecutor的7大参数。


public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {

corePoolSize 和 maximumPoolSize 以及 workQueue(BlockingQueue)共同决定了线程池中线程的数量。
下面几条是我总结的观点:



  1. 核心线程数小于等corePoolSize。

  2. 普通线程在workQueue满了后才会创建。

  3. 普通线程在任务结束后存活时长为:keepAliveTime*unit

  4. 任务总数如果超过了workQueue的容量+普通线程数,会触发 RejectedExecutionHandler

  5. 最好自定义ThreadFactory来创建线程,方便标识线程名等。

  6. ThreadPoolExecutor提供了hook的方法beforeExecute()afterExecute()

  7. shutdown()不会立即停止线程池中的未完成的任务,shutdownNow()会。


这里有个设计上的亮点:它使用了一个AtomicInteger类型的ctl来同时记录线程池状态和线程池中线程数。ctl中高3位记录状态,低29位代表线程数量。
这种方法值得学习,除了节省变量,也减少了线程池状态和当前线程数量同步问题。


@ReachabilitySensitive
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

核心代码区域execute():


public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
//如果当前worker数量小于corePoolSize则创建新的核心Worker
if (addWorker(command, true))
return;
c = ctl.get();
}
//线程池正在运行且可以正常添加任务(即workQueue还没有满),此时等待任务执行即可。注意此处已经将新的Runnable存储到了workQueue里了。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
//二次确认,此时线程池不属于运行状态,且刚添加进去的任务还没有执行,则reject
reject(command);
else if (workerCountOf(recheck) == 0)
//线程池showdown中,后续应该什么都不会做直接return
addWorker(null, false);
}
//workerQueue中满了,创建非核心worker,如果不成功则reject
else if (!addWorker(command, false))
reject(command);
}

看下addWorker()到底干了啥


private boolean addWorker(Runnable firstTask, boolean core) {

retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//如果当前线程池停止了,或者 当前task为空且workQueue中没有任务了。这些情况可以直接退出该方法。
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

上面代码,除了线程池状态校验以及保证代码的异步安全,核心就是:


 w = new Worker(firstTask);
final Thread t = w.thread;
t.start();

创建一个Worker并启动其中的线程


Worker


Worker是什么?Worker是ThreadPoolExecutor的内部类,ThreadPoolExecutor中把excute()传递进来的Runnable认为是一个Work,那么执行Work的就是Worker了。


private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{

/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;

/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}

public void run() {
runWorker(this);
}

}

Worker的构造函数中持有了传入的Task,并通过ThreadFactory创建了个新的线程。


note: 新创建的线程持有了Worker对象自己,而Worker本身又实现了Runnable接口。所以当该线程启动时,run()就会被执行。


看下Worker的run()方法具体干了什么?


首先里面有个while循环,主要是获取task的,如果task一直能获取到,则就能执行到while内部。



  1. 当前task非空,即核心线程创建的时候,自带了一个task。会触发task.run()方法,并在它前后分别又beforeExecute(wt, task)afterExecute(task, thrown)hook点。

  2. 当前task==null,就需要到getTask()中获取。


final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

getTask()就跟ThreadPoolExecutor构造函数中的BlockQueue关联上了。当前线程如果执行完第一个Task后,应该怎么办呢?如果还不满足结束条件(比如说存活时间超过keepAlive*unit),则会向worksQueue(BlockQueue)中所要任务,然后执行。


private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?

for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}

int wc = workerCountOf(c);

// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}

try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

上面前段代码就是判断当前线程池是否还能继续存在,如果能存在并且未超时,那么就从workQueue中取task()。Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS):workQueue.take();



/**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element becomes available.
*
* @return the head of this queue
* @throws InterruptedException if interrupted while waiting
*/
E take() throws InterruptedException;

是个阻塞的行为。所以TheadPoolExecutor中的workQueue参数传入哪种类型的BlockQueue直接影响了未执行任务的执行顺序。 不过需要注意的是即使使用了优先级队列,高优先级的任务也不一定比低优先级任务先执行,因为任务是由线程发起的,workQueue没法影响线程的执行顺序。


作者:拙峰
链接:https://juejin.cn/post/7065617446815662111
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

算法. 无重复字符的最长子串

一、题目 难度中等 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s = "abcabcbb"输出: 3解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 示例 2: 输入: s = "bbbbb"输出:...
继续阅读 »

一、题目


难度中等


给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。


示例 1:


输入: s = "abcabcbb"输出: 3解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。


示例 2:


输入: s = "bbbbb"输出: 1解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。


示例 3:


输入: s = "pwwkew"输出: 3解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke" 是一个*子序列,*不是子串。


示例 4:


输入: s = ""输出: 0


提示:


• 0 <= s.length <= 5 * 104


• s 由英文字母、数字、符号和空格组成


二、我的解答


第一次解答:


我的思路跟官方的差不多,把光标从第一个开始,寻找最长


只不过我是每次删除第一个就全部重新加,而不是他那个窗口移动的概念


public static int lengthOfLongestSubstring(String s) {
int selection = 0;
int maxSize=0;
while (selection < s.length()) {
HashMap map = new HashMap<>();
List mapList = new ArrayList<>();
for (int i = selection; i < s.length(); i++) {
char val = s.charAt(i);
if (!map.containsKey(val)) {
map.put(val, val);
mapList.add(val);
} else {
break;
}
}
maxSize=Math.max(mapList.size(),maxSize);
selection++;
if (mapList.size() >= (s.length() - selection)) {
break;
}
}
return maxSize;
}

通过:




三、系统解答


方法一:滑动窗口


思路及算法


我们先用一个例子考虑如何在较优的时间复杂度内通过本题。


我们不妨以示例一中的字符串abcabcbb 为例,找出从每一个字符开始的,不包含重复字符的最长子串,那么其中最长的那个字符串即为答案。对于示例一中的字符串,我们列举出这些结果,其中括号中表示选中的字符以及最长的字符串:


以(a)bcabcbb 开始的最长字符串为(abc)abcbb;


以a(b)cabcbb 开始的最长字符串为a(bca)bcbb;


以 ab(c)abcbb 开始的最长字符串为ab(cab)cbb;


以 abc(a)bcbb 开始的最长字符串为 abc(abc)bb;


以 abca(b)cbb 开始的最长字符串为abca(bc)bb;


以abcab(c)bb 开始的最长字符串为abcab(cb)b;


以abcabc(b)b 开始的最长字符串为abcabc(b)b;


以abcabcb(b) 开始的最长字符串为 abcabcb(b)。


发现了什么?如果我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!这里的原因在于,假设我们选择字符串中的第 k 个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 r_k 。那么当我们选择第 k+1 个字符作为起始位置时,首先从 k+1到 r_k的字符显然是不重复的,并且由于少了原本的第 k 个字符,我们可以尝试继续增大 r_k,直到右侧出现了重复字符为止。


这样一来,我们就可以使用「滑动窗口」来解决这个问题了:


我们使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 r_k;


在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;


在枚举结束后,我们找到的最长的子串的长度即为答案。


判断重复字符


在上面的流程中,我们还需要使用一种数据结构来判断是否有重复的字符,常用的数据结构为哈希集合(即 C++ 中的 std::unordered_set,Java 中的 HashSet,Python 中的 set, JavaScript 中的 Set)。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。


至此,我们就完美解决了本题。


注释:思路和我一样,代码比我写的简单太多了吧


class Solution {    public int lengthOfLongestSubstring(String s) {
// 哈希集合,记录每个字符是否出现过
Set<Character> occ = new HashSet<Character>();
int n = s.length();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.remove(s.charAt(i - 1));
}
//判断rk+1<n是为了确定右指针是否已经走到了最后
//右指针走到最后的情况,说明后续都可以走到最后,只需用rk-i+1确定
while (rk + 1 < n && !occ.contains(s.charAt(rk + 1))) {
// 不断地移动右指针
occ.add(s.charAt(rk + 1));
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans, rk - i + 1);
}
return ans;
}
}

复杂度分析


时间复杂度O(N),其中 N 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。


空间复杂度:O(∣Σ∣),其中Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在 [0,128) 内的字符,即 ∣Σ∣=128。我们需要用到哈希集合来存储出现过的字符,而字符最多有 ∣Σ∣ 个,因此空间复杂度为 O(∣Σ∣)。


这个网友的很精彩


class Solution {    public int lengthOfLongestSubstring(String s) {
// 记录字符上一次出现的位置
int[] last = new int[128];
for(int i = 0; i < 128; i++) {
last[i] = -1;
}
int n = s.length();

int res = 0;
int start = 0; // 窗口开始位置
for(int i = 0; i < n; i++) {
int index = s.charAt(i);
start = Math.max(start, last[index] + 1);
res = Math.max(res, i - start + 1);
last[index] = i;
}

return res;
}
}

答案有个缺点,左指针并不需要依次递增,即多了很多无谓的循环。 发现有重复字符时,可以直接把左指针移动到第一个重复字符的下一个位置即可。


每次左指针右移一位,移除set的一个字符,这一步会导致很多无用的循环。while循环发现的重复字符不一定就是Set最早添加那个,还要好多次循环才能到达,这些都是无效循环,不如直接用map记下每个字符的索引,直接进行跳转


作者:哑巴湖小水怪
链接:https://juejin.cn/post/7068899468279545869
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter 文字环绕

文字环绕 需求 最近接到一个需求,类似于文字环绕,标题最多两行,超出省略,标题后面可以添加标签。效果如下: 富文本不能控制省略和折行,Flutter 提供了 TextPainter可以实现。 分析 标签有文字和颜色两个属性,个数不定: class Tag {...
继续阅读 »

文字环绕


需求


最近接到一个需求,类似于文字环绕,标题最多两行,超出省略,标题后面可以添加标签。效果如下:


Simulator Screen Shot - iPhone 13 - 2022-02-24 at 16.49.54.png


富文本不能控制省略和折行,Flutter 提供了 TextPainter可以实现。


分析


标签有文字和颜色两个属性,个数不定:


class Tag {

/// 标签文本
final String label;
/// 标签背景颜色
final Color color;

Tag({required this.label, required this.color});
}

标题最大行数可变,可能明天产品要最多显示三行;


文本样式可变;


先创建出来对应的Widget


class TagTitle extends StatefulWidget {
const TagTitle(
this.text, {
Key? key,
required this.tags,
this.maxLines = 2,
this.style = const TextStyle(color: Colors.black, fontSize: 16),
}) : super(key: key);

final String text;
final int maxLines;
final TextStyle style;
final List<Tag> tags;
}

实现


标题文字和标签文字有两种显示情况:



  1. 超出最大行数;

  2. 未超出最大行数;


先假设第一种情况,因为标签前后有间距,所以每个标签前后补一个空格,再把标题和文字拼接创建对应的TextSpan


    tagTexts = widget.tags.fold<String>(
' ', (previousValue, element) => '$previousValue${element.label} ');

_allSp = TextSpan(
text: '${widget.text}$tagTexts',
style: widget.style,
);


要绘制标题、省略号、标签、都需要TextSpan,所以一并创建出来,当然还有最重要的TextPainter


// 标签
final tagsSp = TextSpan(
text: tagTexts,
style: widget.style,
);

// 省略号
final ellipsizeTextSp = TextSpan(
text: ellipsizeText,
style: widget.style,
);

// 标题
final textSp = TextSpan(
text: widget.text,
style: widget.style,
);

final textPainter = TextPainter(
text: tagsSp,
textDirection: TextDirection.ltr,
maxLines: widget.maxLines,
)..layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

拿到标签、省略号、标题的尺寸:


final tagsSize = textPainter.size;

textPainter.text = ellipsizeTextSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

final ellipsizeSize = textPainter.size;

textPainter.text = textSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final textSize = textPainter.size;

算出标题超出最大长度的位置:


textPainter.text = _allSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

final pos = textPainter.getPositionForOffset(Offset(
textSize.width - tagsSize.width - ellipsizeSize.width,
textSize.height,
));

final endIndex = textPainter.getOffsetBefore(pos.offset);

如果超出的话,文字显示区域的宽度减去标签宽度减去省略号宽度,剩下的位置就是标题最大宽度偏移量,根据偏移量得到此位置的文字位置下标。


textPainter.didExceedMaxLines返回的是是否超出最大长度,也就是一开始分析的两种情况的哪一种,如果超出,就根据上面计算出来的下标截取标题文字,添加省略号,然后添加上标签;否则,直接显示标题文本和标签:


TextSpan textSpan;

if (textPainter.didExceedMaxLines) {
textSpan = TextSpan(
style: widget.style,
text: widget.text.substring(0, endIndex) + ellipsizeText,
children: _getWidgetSpan(),
);
} else {
textSpan = TextSpan(
style: widget.style,
text: widget.text,
children: _getWidgetSpan(),
);
}
return RichText(
text: textSpan,
overflow: TextOverflow.ellipsis,
maxLines: widget.maxLines,
);

标签因为带有背景,所以可以用WidgetSpan加上标签背景,这里使用CustomPaint实现:


List<WidgetSpan> _getWidgetSpan() {
return widget.tags
.map((e) => WidgetSpan(
child: CustomPaint(
painter: BgPainter(e.color),
child: Text(
' ' + e.label + ' ',
style: widget.style,
),
),
))
.toList();
}

这个BgPainter就一个功能,绘制背景色:


class BgPainter extends CustomPainter {
final Paint _painter;

final Color color;

BgPainter(this.color) : _painter = Paint()..color = color;

@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(2, 0, size.width - 2, size.height), _painter);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
oldDelegate != this;
}

使用:


TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗斯总统普京和美国总统sadaasdadadada',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),

附上完整代码:


main.dart


import 'package:custom/review/tag_title.dart';
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

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

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗斯总统普京和美国总统sadaasdadadada',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
],
),
);
}
}

Tag_title.dart:


//@dart=2.12
import 'package:flutter/material.dart';

import 'bg_painter.dart';

class TagTitle extends StatefulWidget {
const TagTitle(
this.text, {
Key? key,
required this.tags,
this.maxLines = 2,
this.style = const TextStyle(color: Colors.black, fontSize: 16),
}) : super(key: key);

final String text;
final int maxLines;
final TextStyle style;
final List<Tag> tags;

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

class TagTitleState extends State<TagTitle> {
late final String tagTexts;
late final TextSpan _allSp;
final String ellipsizeText = '...';

@override
void initState() {
super.initState();
tagTexts = widget.tags.fold<String>(
' ', (previousValue, element) => '$previousValue${element.label} ');
_allSp = TextSpan(
text: '${widget.text}$tagTexts',
style: widget.style,
);
}

List<WidgetSpan> _getWidgetSpan() {
return widget.tags
.map((e) => WidgetSpan(
child: CustomPaint(
painter: BgPainter(e.color),
child: Text(
' ' + e.label + ' ',
style: widget.style,
),
),
))
.toList();
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
assert(constraints.hasBoundedWidth);
// 标签
final tagsSp = TextSpan(
text: tagTexts,
style: widget.style,
);

// 省略号
final ellipsizeTextSp = TextSpan(
text: ellipsizeText,
style: widget.style,
);

// 标题
final textSp = TextSpan(
text: widget.text,
style: widget.style,
);

final textPainter = TextPainter(
text: tagsSp,
textDirection: TextDirection.ltr,
maxLines: widget.maxLines,
)..layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final tagsSize = textPainter.size;

textPainter.text = ellipsizeTextSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

final ellipsizeSize = textPainter.size;

textPainter.text = textSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final textSize = textPainter.size;

textPainter.text = _allSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

final pos = textPainter.getPositionForOffset(Offset(
textSize.width - tagsSize.width - ellipsizeSize.width,
textSize.height,
));

final endIndex = textPainter.getOffsetBefore(pos.offset);

TextSpan textSpan;

if (textPainter.didExceedMaxLines) {
textSpan = TextSpan(
style: widget.style,
text: widget.text.substring(0, endIndex) + ellipsizeText,
children: _getWidgetSpan(),
);
} else {
textSpan = TextSpan(
style: widget.style,
text: widget.text,
children: _getWidgetSpan(),
);
}
return RichText(
text: textSpan,
overflow: TextOverflow.ellipsis,
maxLines: widget.maxLines,
);
},
);
}
}
class Tag {

/// 标签文本
final String label;
/// 标签背景颜色
final Color color;

Tag({required this.label, required this.color});
}

bg_painter.dart:


//@dart=2.12
import 'dart:ui' as ui;

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class BgPainter extends CustomPainter {
final Paint _painter;

final Color color;

BgPainter(this.color) : _painter = Paint()..color = color;

@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(2, 0, size.width - 2, size.height), _painter);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
oldDelegate != this;
}

作者:艾维码
链接:https://juejin.cn/post/7068194837954035725
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

官方推荐Flow,LiveData:那我走?

记得在之前掘金上看到Google开发者的账号发了一篇《从 LiveData 迁移到 Kotlin 数据流》的文章。在之前接触ViewModel和LiveDta的时候就有在思考,ViewModel和Repository之间交互,通过什么来实现。后来翻了一下资料,...
继续阅读 »

记得在之前掘金上看到Google开发者的账号发了一篇《从 LiveData 迁移到 Kotlin 数据流》的文章。在之前接触ViewModel和LiveDta的时候就有在思考,ViewModel和Repository之间交互,通过什么来实现。后来翻了一下资料,发现官方推荐在ViewModel和Repository通过Flow来作为桥梁进行交互。


491616319700_.jpg


为了响应官方号召,我又一顿了解Flow。但在了解了Flow之后,当时心中就有大大的疑惑,Flow能够实现LiveData的功能,并且比LiveData功能更加强大,为什么不使用Flow来作为View和ViewModel之间的桥梁。


而现在官方确实推荐将Flow作为方案来替代LiveData。LiveData一脸懵逼:那我走?



image.png


LiveData


LiveData在2017年推出以来,作为Jetpack大家族的元老级人物,为安卓的MVVM架构作出了非凡的贡献,毕竟在当时的背景环境,大家都深陷RxJava的支配。而LiveData作为观察者模式的框架,能够以更平滑的学习曲线来实现变量的订阅,比起RxJava那一套更加轻量级,而且作为Google的亲儿子,在生命周期的管理上也有更出色的表现。



image.png


LiveData的缺点:


而LiveData它的缺点其实也非常明显,LiveData肩负着为UI提供数据订阅的能力,所以他的数据订阅只能在主线程,可能会有小伙伴说可以在子线程通过postValue去发布数据啊。但是其实这个postValue是有坑的,被坑过的小伙伴都应该知道短时间通过多次postValue,中间可能会存在数据的丢失。


而且在复杂的场景LiveData支持的能力确实有一些尴尬。


总结一下LiveDta有几个缺点:




  • 在异步线程修改数据可能存在数据丢失的问题




  • 在复杂的场景,LiveData的能力有一些捉襟见肘




LiveData你别走


但我们也不应该踩一捧一,确实LiveData整体上有更低的学习成本,在一些简单的场景LiveData已经完全能够满足我们的需要。


而且官方也说过并不会废弃LiveData,原因是:



  • 用 Java 写 Android 的人还需要它,因为Flow是协程的东西,所以如果你是用 Java 的,是没有办法使用Flow的,所以LiveData还是有意义的。

  • LiveData 的使用比较简单,而且功能上对于简单场景也是足够的,而 RxJava 和 Flow 这种东西学起来就没 LiveData 那么直观。


Flow


Flow是Google官方提供的一个类似于RxJava的响应式编程模型。它是基于Kotlin协程的。
它相对于Rxjava具有以下特点:



  • 具有更友好的API,学习成本较低

  • 跟Kotlin协程、LiveData结合更紧密,Flow能够转换成LiveData,在ViewModel中直接使用

  • 结合协程的作用域,当协程被取消时,Flow也会被取消,避免内存泄漏


我们知道Flow的特点之一就是冷流。那么什么是冷流呢?




  • 冷流:当数据被订阅的时候,发布者才开始执行发射数据流的代码。并且当有多个订阅者的时候,每一个订阅者何发布者都是一对一的关系,每个订阅者都会收到发布者完整的数据。




  • 热流:无论有没有订阅者订阅,事件始终都会发生。当热流有多个订阅者时,发布者跟订阅者是一对多的关系,热流可以与多个订阅者共享信息。




StateFlow


因为Flow是冷流,这与LiveData的特点完全不一样,因此Flow提供了StateFlow来实现热流。


StateFlowSharedFlow 的一个比较特殊的变种,而 SharedFlow 又是 Kotlin 数据流当中比较特殊的一种类型。StateFlow 与 LiveData 是最接近的,因为:



  • 它始终是有值的。

  • 它的值是唯一的。

  • 它允许被多个观察者共用 (因此是共享的数据流)。

  • 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。


官方推荐当暴露 UI 的状态给视图时,应该使用 StateFlow。这是一种安全和高效的观察者,专门用于容纳 UI 状态。


StateFlow使用


StateFlow替换掉LiveData是简单的。我们来看看StateFlow的构造函数:


/**
* Creates a [MutableStateFlow] with the given initial [value].
*/
@Suppress("FunctionName")
public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

我们在ViewModel上游中不断的发送值,View层通过collect函数去获取到上游发送的数据。


StateFlow只有在值发生改变时才会返回,如果发生更新但值没有变化时,StateFlow不会回调collect函数,但LiveData会进行回调。


stateIn


StateIn 能够将普通的流转换为StateFlow,但转换之后还需要一些配置工作.


image.png



  • scope 共享开始时所在的协程作用域范围

  • started 控制共享的开始和结束的策略

    • Lazily: 当首个订阅者出现时开始,在 scope 指定的作用域被结束时终止。

    • Eagerly: 立即开始,而在 scope 指定的作用域被结束时终止。

    • WhileSubscribed能够指定当前不有订阅者后,多少时间取消上游数据和能够指定多少时间后,缓存中的数据被丢失,回复称initialValue的值。



  • initialValue 初始值


WhileSubscribed


WhileSubscribed 策略会在没有收集器的情况下取消上游数据流。通过 stateIn 运算符创建的 StateFlow 会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程


@Suppress("FunctionName")
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
): SharingStarted =
StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis)

WhileSubscribed


WhileSubscribed支持传入stopTimeoutMillisreplayExpirationMillis参数。


其中stopTimeoutMillis支持设置超时停止的效果,单位为ms。当最后一个订阅者不再订阅上游时,StateFlow会停止上游数据的发送。


这样就可以提供APP的性能,当没有订阅者时或者应用被切到后台后会等待stopTimeoutMillis设置的时间后上游会停止发送数据,并且会缓存停止前的缓存数据。


replayExpirationMillis


如果当上游如果停止发送太久,这时候StateFlow中缓存的数据是比较陈旧的数据,当这时候有订阅者时,我们不希望给订阅者陈旧的数据。我们可以设置replayExpirationMillis参数,当停止共享携程超过设置的replayExpirationMillis时间后,StateFlow中会将缓存重置为默认值。


在视图中观察数据


ViewModel中的StateFlow需要结合生命周期知道他们已经不在需要感知到何时不再需要被监听。我们在View视图层提供了若干个协程构建器。



  • Activity.lifecycleScope.launch : 启动协程,并且在本 Activity 销毁时结束协程。

  • Fragment.lifecycleScope.launch : 启动协程,并且在本 Fragment 销毁时结束协程。

  • Fragment.viewLifecycleOwner.lifecycleScope.launch : 启动协程,并且在本 Fragment 中的视图生命周期结束时取消协程。

  • launchWhenX :启动协程,它会在 lifecycleOwner 进入 X 状态之前一直等待,又在离开 X 状态时挂起协程。


image.png


通过上面官方的这个图,我们可以看出当APP进入后台时,如果APP还在后台收集数据更新可能引发应用崩溃和资源的浪费。


repeatOnLifecycle


因此google官方提供了新的API接口repeatOnLifecycle能够在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程。


image.png


当视图处于 STARTED 状态时会开始收集流,并且在 RESUMED 状态时保持收集,最终在视图进入 STOPPED 状态时结束收集过程。


image.png


使用repeatOnLifecycle和StateFlow能够帮助我们应用根据应用生命周期优化性能和设备资源。


通过repeatOnLifecycleStateFlow能够帮助我们更好管理数据流。最后以官方的一句话结束本文。



当然,如果您并不需要使用到 Kotlin 数据流的强大功能,就用 LiveData 好了 :)


作者:丨大麦
链接:https://juejin.cn/post/7001071604855734285
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android自定义View第五弹(可滑动的星星评价)

距离上一篇自定义view已经过去了一年多了,这次主要给大家介绍的是可滑动的星星评价,虽然Google官方也提供了 RatingBar 但是没办法满足我的需要只能自己定义一个了,废话不多说先上图: 这个选中以及默认的心型都是UI提供的图片,上代码: 1.自定...
继续阅读 »

距离上一篇自定义view已经过去了一年多了,这次主要给大家介绍的是可滑动的星星评价,虽然Google官方也提供了 RatingBar 但是没办法满足我的需要只能自己定义一个了,废话不多说先上图:
在这里插入图片描述
这个选中以及默认的心型都是UI提供的图片,上代码:


1.自定义view的代码


import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import cn.neoclub.uki.R
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.roundToInt

/**
* Author: Mr.Dong
* Date: 2022/2/15 4:31 下午
* Description: 点击心心评价
*/
class HeartRatingBar : View {
private var starDistance = 0 //星星间距
private var starCount = 5 //星星个数
private var starSize = 0 //星星高度大小,星星一般正方形,宽度等于高度
private var starMark = 0 //评分星星
private var starFillBitmap: Bitmap? = null //亮星星
private var starEmptyDrawable : Drawable? = null//暗星星
private var onStarChangeListener : OnStarChangeListener? = null//监听星星变化接口

private var paint : Paint? = null//绘制星星画笔
//是否显示整数的星星
private var integerMark = false
//初始化可以被定义为滑动的距离(超过这个距离就是滑动,否则就是点击事件)
private var scaledTouchSlop:Int=0

constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}

/**
* 初始化UI组件
*
* @param context
* @param attrs
*/
private fun init(context: Context, attrs: AttributeSet?) {
//获取滑动的有效距离
scaledTouchSlop=ViewConfiguration.get(context).scaledTouchSlop
isClickable = true
//获取各种属性的值
val mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeartRatingBar)
starDistance = mTypedArray.getDimension(R.styleable.HeartRatingBar_starDistance, 0f).toInt()
starSize = mTypedArray.getDimension(R.styleable.HeartRatingBar_starSize, 20f).toInt()
starCount = mTypedArray.getInteger(R.styleable.HeartRatingBar_starCount, 5)
starEmptyDrawable = mTypedArray.getDrawable(R.styleable.HeartRatingBar_starEmpty)
starFillBitmap = drawableToBitmap(mTypedArray.getDrawable(R.styleable.HeartRatingBar_starFill))
mTypedArray.recycle()
paint = Paint()
//设置抗锯齿
paint?.isAntiAlias = true
//设置渲染器
paint?.shader = BitmapShader(starFillBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
}

/**
* 设置是否需要整数评分
* @param integerMark
*/
fun setIntegerMark(integerMark: Boolean) {
this.integerMark = integerMark
}

/**
* 设置显示的星星的分数
*
* @param mark
*/
private fun setStarMark(mark: Int) {
starMark = if (integerMark) {
//ceil函数 去除小数点后面的 返回 double 类型,返回值大于或等于给定的参数 例Math.ceil(100.675) = 101.0
ceil(mark.toDouble()).toInt()
} else {
(mark * 10).toFloat().roundToInt() * 1 / 10
}
if (onStarChangeListener != null) {
onStarChangeListener?.onStarChange(starMark) //调用监听接口
}
invalidate()
}

/**
* 获取显示星星的数目
*
* @return starMark
*/
fun getStarMark(): Int {
return starMark
}

/**
* 定义星星点击的监听接口
*/
interface OnStarChangeListener {
fun onStarChange(mark: Int)
}

/**
* 设置监听
* @param onStarChangeListener
*/
fun setOnStarChangeListener(onStarChangeListener: OnStarChangeListener?) {
this.onStarChangeListener = onStarChangeListener
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//设置view的宽度和高度 继承view必须重写此方法
setMeasuredDimension(starSize * starCount + starDistance * (starCount - 1), starSize)
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (starFillBitmap == null || starEmptyDrawable == null) {
return
}
//绘制空的星星
for (i in 0 until starCount) {
//设置starEmptyDrawable绘制的长方形区域,当调用draw()方法后就可以直接绘制
starEmptyDrawable?.setBounds(
(starDistance + starSize) * i,
0,
(starDistance + starSize) * i + starSize,
starSize
)
starEmptyDrawable?.draw(canvas)
}
if (starMark > 1) {
//绘制了第一个star
canvas.drawRect(0f, 0f, starSize.toFloat(), starSize.toFloat(), paint!!)
if (starMark - starMark == 0) { //第一步必走这里
//绘制亮星星
for (i in 1 until starMark) {
//每次位移start的宽度+间距
canvas.translate((starDistance + starSize).toFloat(), 0f)
canvas.drawRect(0f, 0f, starSize.toFloat(), starSize.toFloat(), paint!!)
}
} else { //非整形的star绘制走这里
for (i in 1 until starMark - 1) {
canvas.translate((starDistance + starSize).toFloat(), 0f)
canvas.drawRect(0f, 0f, starSize.toFloat(), starSize.toFloat(), paint!!)
}
canvas.translate((starDistance + starSize).toFloat(), 0f)
canvas.drawRect(
0f,
0f,
starSize * (((starMark - starMark) * 10).toFloat().roundToInt() * 1.0f / 10),
starSize.toFloat(),
paint!!
)
}
} else {
//startMark=0 啥都没绘制
canvas.drawRect(0f, 0f, (starSize * starMark).toFloat(), starSize.toFloat(), paint!!)
}
}

//记录一下上次down的x的位置
private var downX:Int=0

override fun onTouchEvent(event: MotionEvent): Boolean {
var x = event.x.toInt()
if (x < 0) x = 0
if (x > measuredWidth) x = measuredWidth
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX=x
//对于除数不能为0的限制
if(starCount==0||(measuredWidth * 1 / starCount)==0){
return false
}
val count=x * 1 / (measuredWidth * 1 / starCount)
setStarMark(count+1)
}
MotionEvent.ACTION_MOVE -> {
//当滑动距离的绝对值小于官方定义的有效滑动距离则不走move当做down处理
if(abs(event.x-downX)<scaledTouchSlop){
return false
}
if(starCount==0||(measuredWidth * 1 / starCount)==0){
return false
}
setStarMark(x * 1 / (measuredWidth * 1 / starCount))
}
MotionEvent.ACTION_UP -> {}
}
invalidate()
return super.onTouchEvent(event)
}

/**
* drawable转bitmap
*
* @param drawable
* @return
*/
private fun drawableToBitmap(drawable: Drawable?): Bitmap? {
if (drawable == null) return null
val bitmap = Bitmap.createBitmap(starSize, starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, starSize, starSize)
drawable.draw(canvas)
return bitmap
}
}

2.自定义View的使用


    <cn.neoclub.uki.message.widget.HeartRatingBar
android:id="@+id/rb_rating_bar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:starCount="5"
app:starDistance="7dp"
app:starEmpty="@drawable/icon_heart_rating_default"
app:starFill="@drawable/icon_heart_rating_select"
app:starSize="40dp" />

3.attrs.xml文件中的属性


 <declare-styleable name="HeartRatingBar">
<attr name="starDistance" format="dimension"/>
<attr name="starSize" format="dimension"/>
<attr name="starCount" format="integer"/>
<attr name="starEmpty" format="reference"/>
<attr name="starFill" format="reference"/>
</declare-styleable>

4.送你两张图,怕你运行不起来


1.icon_heart_rating_select.png


在这里插入图片描述


2.icon_heart_rating_default.png


在这里插入图片描述


是不是感觉这边少了个icon,对了就是少一张😄(其实是有图的)


作者:小翘_上海
链接:https://juejin.cn/post/7067093473492467742
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android监听截屏

Android系统没有提供默认的截屏事件监听方式,需要开发者自己想办法实现。查看了网上推荐的实现方式,主要是通过内容观察者(ContentObserver)监听媒体数据库的变化,根据内容名称(路径)中是否包含关键字,判断是否为截屏事件。 关键字: pr...
继续阅读 »

Android系统没有提供默认的截屏事件监听方式,需要开发者自己想办法实现。查看了网上推荐的实现方式,主要是通过内容观察者(ContentObserver)监听媒体数据库的变化,根据内容名称(路径)中是否包含关键字,判断是否为截屏事件。
关键字:


    private static final String[] KEYWORDS = {
"screenshot", "screen_shot", "screen-shot", "screen shot",
"screencapture", "screen_capture", "screen-capture", "screen capture",
"screencap", "screen_cap", "screen-cap", "screen cap", "snap", "截屏"
};

第一步:对ContentResolver添加内、外存储变化监听;


mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, MainHandler.get());
mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MainHandler.get());
mResolver = AppContext.get().getContentResolver();
// 添加监听
mResolver.registerContentObserver(
MediaStore.Images.Media.INTERNAL_CONTENT_URI,
false,
mInternalObserver
);
mResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
false,
mExternalObserver
);

第二步:当内容观察者(ContentObserver)监听到变化时,会调用onChange方法,此时,我们使用ContentResolver去查询最新的一条数据;
需要注意的是,查询外部存储一定要有读取存储权限(Manifest.permission.READ_EXTERNAL_STORAGE),否则会在查询的时候报错;
第三步:判断查到到数据是否为截图文件;在这里有一个很难处理到问题,在华为荣耀手机上,截图预览图生成的同时就会通知存储内容变化,而小米则是在截图预览图消失后通知变化;
解决方案:



  1. 判断当前文件路径是否与上次有效路径相同,相同执行步骤2,不相同则执行步骤3;

  2. 当前路径与上次路径相同,取消回调请求,重新延迟发送回调请求;

  3. 当前路径与上次路径不同,判断内容的生成时间(MediaStore.Images.ImageColumns.DATE_TAKEN)和添加时间(MediaStore.Images.ImageColumns.DATE_ADDED)是否相同,相同执行步骤4,不相同则执行步骤5;

  4. 内容的生成时间和添加时间相同,认为此时为生成长截图,立刻取消回调请求,执行空回调(用于取消弹窗等操作);

  5. 内容的生成时间和添加时间不同,检查是否含有关键字,若判定为截图,更新上次有效路径,取消回调请求,重新延迟发送回调请求;


 // 获取各列的索引
int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
int dateAddIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED);
// 获取行数据
final String data = cursor.getString(dataIndex);
long dateTaken = cursor.getLong(dateTakenIndex);
long dateAdded = cursor.getLong(dateAddIndex);
if (data.length() > 0) {
if (TextUtils.equals(lastData, data)) {
MainHandler.get().removeCallbacks(shotCallBack);
MainHandler.get().postDelayed(shotCallBack, 500);
} else if (dateTaken == 0 || dateTaken == dateAdded * 1000) {
MainHandler.get().removeCallbacks(shotCallBack);
if (listener != null) {
listener.onShot(null);
}
} else if (checkScreenShot(data)) {
MainHandler.get().removeCallbacks(shotCallBack);
lastData = data;
MainHandler.get().postDelayed(shotCallBack, 500);
}
}

完整代码:(其中AppContext为全局Application单例,MainHandler为全局主线程Handler单例)


public class ScreenShotHelper {
private static final String[] KEYWORDS = {
"screenshot", "screen_shot", "screen-shot", "screen shot",
"screencapture", "screen_capture", "screen-capture", "screen capture",
"screencap", "screen_cap", "screen-cap", "screen cap", "snap", "截屏"
};

/**
* 读取媒体数据库时需要读取的列
*/
private static final String[] MEDIA_PROJECTIONS = {
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATE_ADDED,
};
/**
* 内部存储器内容观察者
*/
private ContentObserver mInternalObserver;
/**
* 外部存储器内容观察者
*/
private ContentObserver mExternalObserver;
private ContentResolver mResolver;
private OnScreenShotListener listener;
private String lastData;
private Runnable shotCallBack = new Runnable() {
@Override
public void run() {
if (listener != null) {
final String path = lastData;
if (path != null && path.length() > 0) {
listener.onShot(path);
}
}
}
};

private ScreenShotHelper() {
// 初始化
mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, null);
mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null);

mResolver = AppContext.get().getContentResolver();
// 添加监听
mResolver.registerContentObserver(
MediaStore.Images.Media.INTERNAL_CONTENT_URI,
false,
mInternalObserver
);
mResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
false,
mExternalObserver
);
}

private static class Instance {
static ScreenShotHelper mInstance = new ScreenShotHelper();
}

public static ScreenShotHelper get() {
return Instance.mInstance;
}

public void setScreenShotListener(OnScreenShotListener listener) {
this.listener = listener;
}

public void removeScreenShotListener(OnScreenShotListener listener) {
if (this.listener == listener) {
synchronized (ScreenShotHelper.class) {
if (this.listener == listener) {
this.listener = null;
}
}
}
}

public void stopListener() {
mResolver.unregisterContentObserver(mInternalObserver);
mResolver.unregisterContentObserver(mExternalObserver);
}

private void handleMediaContentChange(Uri contentUri) {
Cursor cursor = null;
try {
// 数据改变时查询数据库中最后加入的一条数据
cursor = mResolver.query(
contentUri,
MEDIA_PROJECTIONS,
null,
null,
MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
);
if (cursor == null) {
return;
}
if (!cursor.moveToFirst()) {
return;
}
// 获取各列的索引
int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
int dateAddIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED);
// 获取行数据
final String data = cursor.getString(dataIndex);
long dateTaken = cursor.getLong(dateTakenIndex);
long dateAdded = cursor.getLong(dateAddIndex);
if (data.length() > 0) {
if (TextUtils.equals(lastData, data)) {
//更改资源文件名也会触发,并且传递过来的是之前的截屏文件,所以只对分钟以内的有效
if (System.currentTimeMillis() - dateTaken < 3 * 3600) {
MainHandler.get().removeCallbacks(shotCallBack);
MainHandler.get().postDelayed(shotCallBack, 500);
}
} else if (dateTaken == 0 || dateTaken == dateAdded * 1000) {
MainHandler.get().removeCallbacks(shotCallBack);
if (listener != null) {
listener.onShot(null);
}
} else if (checkScreenShot(data)) {
MainHandler.get().removeCallbacks(shotCallBack);
lastData = data;
MainHandler.get().postDelayed(shotCallBack, 500);
}
}
} catch (Exception e) {
//
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
}

/**
* 根据包含关键字判断是否是截屏
*/
private boolean checkScreenShot(String data) {
if (data == null || data.length() < 2) {
return false;
}
data = data.toLowerCase();
for (String keyWork : KEYWORDS) {
if (data.contains(keyWork)) {
return true;
}
}
return false;
}

private class MediaContentObserver extends ContentObserver {
private Uri mContentUri;

MediaContentObserver(Uri contentUri, Handler handler) {
super(handler);
mContentUri = contentUri;
}

@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
if (listener != null) {
handleMediaContentChange(mContentUri);
}
}
}

public interface OnScreenShotListener {
void onShot(@Nullable String data);
}

}

总结: 

1.必须要有读取内存的权限; 

2.内容生成时间为毫秒,内容添加时间为秒,比较时需要注意换算; 

3.当内容生成时间等于内容添加时间时,应当取消之前的截屏监听操作(尤其是会遮挡页面视图的部分);


作者:LetterZ
链接:https://juejin.cn/post/7068241484541067278
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

网上有很多,比如说“Flutter Dio 亲妈级别封装教程”这篇文章,该文章上有几点问题:



  1. 重试机制代码错误

  2. token存取耦合很高

  3. 网络请求只能针对单一地址进行访问

  4. 网络请求缓存机制也不是很完美。


一旦依照这样的封装去做,那么项目后期的扩展性和易用性会有一定的阻碍,那么如何做到token存取无耦合,而且还能让app多种网络地址一同请求,还可以做到针对不同请求不同超时时长处理,网络缓存还加入可自动清理的lru算法呢?那么今天这篇文章为你揭晓企业级flutter dio网络层封装。


搭建前夕准备


三方库:


dio_cache_interceptor lru缓存库
dio 网络库
retrofit 网络生成库
connectivity_plus 网络情况判断


技能:


单例模式
享元模式
迭代


文章:


持久化:跟我学企业级flutter项目:dio网络框架增加公共请求参数&header


准备好如上技能,我们来封装一套优秀的网络层


一、准备好几个基本拦截器


1、超时拦截器


import 'dart:collection';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_base_lib/src/tools/net/cache_object.dart';

class TimeInterceptor extends Interceptor {

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
Map extra = options.extra;
bool connect = extra.containsKey(SysConfig.connectTimeout);
bool receive = extra.containsKey(SysConfig.receiveTimeOut);
if(connect||receive){
if(connect){
int connectTimeout = options.extra[SysConfig.connectTimeout];
options.connectTimeout = connectTimeout;
}
if(receive){
int receiveTimeOut = options.extra[SysConfig.receiveTimeOut];
options.receiveTimeout = receiveTimeOut;
}
}
super.onRequest(options, handler);

}

}

作用:单独针对个别接口进行超时时长设定,如(下载,长链接接口)


2、缓存拦截器


dio_cache_interceptor 这个库中有lru算法缓存拦截库,可直接集成


3、持久化拦截器


跟我学企业级flutter项目:dio网络框架增加公共请求参数&header 本篇文章介绍了如何持久化


4、重试拦截器



import 'dart:async';
import 'dart:io';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/application.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_ulog/flutter_ulog.dart';

import '../dio_utli.dart';

/// 重试拦截器
class RetryOnConnectionChangeInterceptor extends Interceptor {
Dio? dio;

RequestInterceptorHandler? mHandler;
// RetryOnConnectionChangeInterceptor(){
//
// }

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
mHandler = handler;
super.onRequest(options, handler);
}


@override
Future onError(DioError err, ErrorInterceptorHandler handler) async{
if (dio!=null&&Application.config.httpConfig.retry&&await _shouldRetry(err)) {
return await retryLoop(err,handler,1);
}
return super.onError(err, handler);
}

Future retryLoop(DioError err, ErrorInterceptorHandler handler,int retry) async {
try {
ULog.d("${err.requestOptions.uri.toString()} retry : ${retry}",tag: "${SysConfig.libNetTag}Retry");
await retryHttp(err,handler);
} on DioError catch (err) {
if(await _shouldRetry(err)&&retry _shouldRetry(DioError err) async{
return err.error != null && err.error is SocketException && await isConnected();
}

Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
}

该重试拦截器与其他文章封装不同,主要是用重试次数来管理重试机制。


5、日志拦截器



import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_ulog/flutter_ulog.dart';
typedef void LibLogPrint(String message);
class LibLogInterceptor extends Interceptor {
LibLogInterceptor({
this.request = true,
this.requestHeader = true,
this.requestBody = false,
this.responseHeader = true,
this.responseBody = false,
this.error = true
});

/// Print request [Options]
bool request;

/// Print request header [Options.headers]
bool requestHeader;

/// Print request data [Options.data]
bool requestBody;

/// Print [Response.data]
bool responseBody;

/// Print [Response.headers]
bool responseHeader;

/// Print error message
bool error;

@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler
) async
{
var builder = StringBuffer('*** Request *** \n');
builder.write(_printKV('uri', options.uri));
//options.headers;

if (request) {
builder.write(_printKV('method', options.method));
builder.write(_printKV('responseType', options.responseType.toString()));
builder.write(_printKV('followRedirects', options.followRedirects));
builder.write(_printKV('connectTimeout', options.connectTimeout));
builder.write(_printKV('sendTimeout', options.sendTimeout));
builder.write(_printKV('receiveTimeout', options.receiveTimeout));
builder.write(_printKV(
'receiveDataWhenStatusError', options.receiveDataWhenStatusError));
builder.write(_printKV('extra', options.extra));
}
if (requestHeader) {
builder.write('headers:\n');
options.headers.forEach((key, v) => builder.write(_printKV(' $key', v)));
}
if (requestBody) {
var res = options.data;
builder.write('data:\n');
builder.write(_message(res));
// try{
// ULog.json(res.toString(),tag: "${SysConfig.libNetTag}RequestJson");
// } on Exception catch (e) {
// ULog.d(res,tag: "${SysConfig.libNetTag}RequestJson");
// }
}
ULog.d(builder.toString(),tag: "${SysConfig.libNetTag}Request");
handler.next(options);
}

// Handles any object that is causing JsonEncoder() problems
Object toEncodableFallback(dynamic object) {
return object.toString();
}

String _message(dynamic res) {
if (res is Map || res is Iterable) {
var encoder = JsonEncoder.withIndent(' ', toEncodableFallback);
return encoder.convert(res);
} else {
return res.toString();
}
}

@override
void onResponse(Response response, ResponseInterceptorHandler handler) async
{
var builder = StringBuffer('*** Response *** \n');
_printResponse(response,builder,(message){
ULog.d(message,tag: "${SysConfig.libNetTag}Response");
});
handler.next(response);
}

@override
void onError(DioError err, ErrorInterceptorHandler handler) async
{
if (error) {
var builder = StringBuffer('*** DioError *** \n');
builder.write('uri: ${err.requestOptions.uri}\n');
builder.write('$err');
if (err.response != null) {
_printResponse(err.response!,builder,(message){
ULog.e(message,tag: "${SysConfig.libNetTag}Error");
});
}else{
ULog.e(builder.toString(),tag: "${SysConfig.libNetTag}Error");
}
}

handler.next(err);
}

void _printResponse(Response response,StringBuffer builder,LibLogPrint pr) {
builder.write(_printKV('uri', response.requestOptions.uri));
if (responseHeader) {
builder.write(_printKV('statusCode', response.statusCode));
if (response.isRedirect == true) {
builder.write(_printKV('redirect', response.realUri));
}

builder.write('headers:\n');
response.headers.forEach((key, v) => builder.write(_printKV(' $key', v.join('\r\n\t'))));
}
if (responseBody) {
var res = response.toString();
builder.write('Response Text:\r\n');
var resJ = res.trim();
if (resJ.startsWith("{")) {
Map decode = JsonCodec().decode(resJ);
builder.write(_message(decode));
}else if (resJ.startsWith("[")) {
List decode = JsonCodec().decode(resJ);
builder.write(_message(decode));
}else {
builder.write(res);
}

// try{
// ULog.json(res,tag: "${SysConfig.libNetTag}ResponseJson");
// } on Exception catch (e) {
// ULog.d(res,tag: "${SysConfig.libNetTag}ResponseJson");
// }
}
pr(builder.toString());

}

String _printKV(String key, Object? v) {
return '$key: $v \n';
}

}

在这里插入图片描述
主要是日志拦截打印


6、错误拦截器



import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_base_lib/src/exception/lib_network_exception.dart';
import 'package:flutter_base_lib/src/tools/net/dio_utli.dart';
import 'package:flutter_ulog/flutter_ulog.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_base_lib/src/lib_localizations.dart';

/// 错误处理拦截器
class ErrorInterceptor extends Interceptor {
// 是否有网
Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
@override
Future onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.type == DioErrorType.other) {
bool isConnectNetWork = await isConnected();
if (!isConnectNetWork && err.error is SocketException) {
err.error = SocketException(LibLocalizations.getLibString().libNetWorkNoConnect!);
}else if (err.error is SocketException){
err.error = SocketException(LibLocalizations.getLibString().libNetWorkError!);
}
}
err.error = LibNetWorkException.create(err);
ULog.d('DioError : ${err.error.toString()}',tag: "${SysConfig.libNetTag}Interceptor");
super.onError(err, handler);
}

}

与其他人封装不同,服务器请求异常code,我将其抛到业务层自主处理。常规异常则走库文案。



import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/lib_localizations.dart';

class LibNetWorkException implements Exception{

final String _message;
final int _code;

int get code{
return _code;
}

String get message{
return _message;
}

LibNetWorkException( this._code,this._message);

@override
String toString() {
return "$_code : $_message";
}



factory LibNetWorkException.create(DioError error) {
switch (error.type) {
case DioErrorType.cancel:{
return LibNetWorkException(-1, LibLocalizations.getLibString().libNetRequestCancel!);
}
case DioErrorType.connectTimeout:{
return LibNetWorkException(-1, LibLocalizations.getLibString().libNetFailCheck!);
}
case DioErrorType.sendTimeout:{
return LibNetWorkException(-1, LibLocalizations.getLibString().libNetTimeOutCheck!);
}
case DioErrorType.receiveTimeout:{
return LibNetWorkException(-1, LibLocalizations.getLibString().libNetResponseTimeOut!);
}
case DioErrorType.response:{
try{
return LibNetWorkException(error.response!.statusCode!,"HTTP ${error.response!.statusCode!}:${LibLocalizations.getLibString().libNetServerError!}");
} on Exception catch (_) {
return LibNetWorkException(-1, error.error.message);
}
}
default:
{
return LibNetWorkException(-1, error.error.message);
}
}
}
}

二、工具类封装


1、主要类




import 'dart:io';

import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/tools/net/interceptor/error_interceptor.dart';
import 'package:flutter_base_lib/src/tools/net/interceptor/lib_log_interceptor.dart';

import '../../../flutter_base_lib.dart';
import 'interceptor/presistent_interceptor.dart';
import 'interceptor/retry_on_connection_change_interceptor.dart';
import 'interceptor/time_interceptor.dart';

class DioUtil{

final
String _baseUrl;
final HttpConfig _config;
final List
_interceptors;

late Dio _dio;

Dio
get dio{
return _dio;
}
DioUtil._internal(
this._baseUrl, this._config, this._interceptors){
BaseOptions options =
new BaseOptions(
baseUrl: _baseUrl,
connectTimeout: _config.connectTimeout,
receiveTimeout: _config.receiveTimeOut,
);
_dio =
new Dio(options);
var retry = new Dio(options);
_interceptors.forEach((element) {
if(element is RetryOnConnectionChangeInterceptor){
element.dio = retry;
}
else{
if(!(element is ErrorInterceptor)){
retry.interceptors.add(element);
}
}
_dio.interceptors.add(element);
});
proxy(_dio);
proxy(retry);
}

void proxy(Dio dio){
if (SpSotre.instance.getBool(SysConfig.PROXY_ENABLE)??false) {
String? porxy = SpSotre.instance.getString(SysConfig.PROXY_IP_PROT)??null;
if(porxy!=null){
(dio.httpClientAdapter
as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
client.findProxy = (uri) {
return "PROXY $porxy";
};
//代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
};
}
}
}

static late Map
_dioUtils = Map();

static DioUtil instance(String baseUrl,{HttpConfig? config, List
? interceptors,List? applyInterceptors}){
if(!_dioUtils.containsKey(baseUrl)){
List
list = [PresistentInterceptor(),TimeInterceptor(),RetryOnConnectionChangeInterceptor(),LibLogInterceptor(requestBody: Application.config.debugState,responseBody: Application.config.debugState),ErrorInterceptor()];
// List
list = [ErrorInterceptor(),PresistentInterceptor()];
var inter = interceptors??list;
if(applyInterceptors!=null){
inter.addAll(applyInterceptors);
}
_dioUtils[baseUrl] = DioUtil._internal(baseUrl,config??Application.config.httpConfig,inter);
}
return _dioUtils[baseUrl]!;
}

// CancelToken _cancelToken = new CancelToken();


}


工具类封装,主要运用享元模式,可以支持多种url进行访问,不同的url有不同的配置。(灵活可用)


2、辅助类:




class HttpConfig{
final int _connectTimeout ;
final int _receiveTimeOut ;
final bool _retry;
final int _retryCount;

get connectTimeout{
return _connectTimeout;
}

get receiveTimeOut{
return _receiveTimeOut;
}

get retry{
return _retry;
}
get retryCount{
return _retryCount;
}

HttpConfig(HttpConfigBuilder builder): _connectTimeout = builder._connectTimeout,_receiveTimeOut = builder._receiveTimeOut,_retry = builder._retry,_retryCount = builder._retryCount;
}

class HttpConfigBuilder {
int _connectTimeout = 10000;//连接超时时间
int _receiveTimeOut = 30000;//接收超时时间
bool _retry = false;
int _retryCount = 3;

// var maxRetry = 1 重试次数

HttpConfigBuilder setConnectTimeout(int connectTimeout){
_connectTimeout = connectTimeout;
return this;
}

HttpConfigBuilder setReceiveTimeOut(int receiveTimeOut){
_receiveTimeOut = receiveTimeOut;
return this;
}

HttpConfigBuilder setRetry(bool retry){
_retry = retry;
return this;
}

HttpConfigBuilder setRetryCount(int retryCount){
_retryCount = _retryCount;
return this;
}

HttpConfig build() => HttpConfig(this);
}


三、使用


import 'package:flutter_app_me/data/model/api_result.dart';
import 'package:flutter_app_me/data/model/user.dart';
import 'package:flutter_app_me/data/model/user_infos.dart';
import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';

import 'api_methods.dart';

part 'api_service.g.dart';

@RestApi()
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

@GET(ApiMethods.userinfoJson)
Future> userinfoJson();

// "test123332","123456"
@POST(ApiMethods.login)
@Extra({SysConfig.connectTimeout:100000})
Future> userLogin(@Queries() User user);
}

网络请求配置




class BusinessErrorException implements Exception {
final int _errorCode;
final String? _errorMsg;

BusinessErrorException(
this._errorCode, this._errorMsg);

int
get errorCode {
return _errorCode;
}

String?
get errorMsg => _errorMsg;
}


class TokenTimeOutException implements Exception {
final String? _errorMsg;
TokenTimeOutException(
this._errorMsg);
String?
get errorMsg => _errorMsg;

}

class RequestCodeErrorException implements Exception {
final String? _errorMsg;
final int _errorCode;
RequestCodeErrorException(
this._errorCode, this._errorMsg);

int
get errorCode {
return _errorCode;
}

String?
get errorMsg => _errorMsg;
}

业务基本异常


import 'package:business_package_auth/business_package_auth.dart';
import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:dio/dio.dart';
import 'package:wisdomwork_lib/src/model/api_result.dart';

const int httpSuccessCode = 0;
const int httpErrorCode = 1;
const int httpTokenExt = 10001;

extension SuccessExt on Success {
Success appSuccess() {
var data = this.data;
if (data is ApiResult) {
if (data.code != httpSuccessCode) {
switch (data.code){
case httpTokenExt:
TipToast.instance.tip(data.msg ?? LibLocalizations.getLibString().libBussinessTokenTimeOut!,tipType: TipType.warning);
BlocProvider.of(LibRouteNavigatorObserver.instance.navigator!.context).add(LogOut());
throw TokenTimeOutException(data.msg);
case httpErrorCode:
TipToast.instance.tip(data.msg ?? LibLocalizations.getLibString().libBussinessRequestCodeError!,tipType: TipType.error);
throw RequestCodeErrorException(data.code!,data.msg);
default:
throw BusinessErrorException(data.code!, data.msg);
}

}
}
return this;
}
}

extension ErrorExt on Error {
void appError() {
var exception = this.exception;
if (exception is LibNetWorkException) {
TipToast.instance.tip(exception.message, tipType: TipType.error);
}
}
}


typedef ResultF = Future> Function();

mixin RemoteBase {

Future>> remoteDataResult(ResultF resultF) async {
try {
var data = await resultF.call();
return Success(data).appSuccess();
} on DioError catch (err, stack) {
var e = err.error;
ULog.e(e.toString(), error: e, stackTrace: stack);
return Error(e)..appError();
} on Exception catch (e, stack) {
ULog.e(e.toString(), error: e, stackTrace: stack);
return Error(e)..appError();
}
}

}

业务基本异常处理方式


import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:wisdomwork/data/services/api_service.dart';
import 'package:wisdomwork_lib/wisdomwork_lib.dart';

mixin WisdomworkRemoteBase{
var rest = RestClient(DioUtil.instance(
AppEnvironment.envConfig![AppConfig.apiName]!,
applyInterceptors: [UiNetInterceptor()]).dio);
}

业务请求接口,实现

     final data = await AppResponsitory.instance.login(state.phoneText, state.codeText);
if (userResult != null) {
if (userResult is Success) {
if (userResult.data!.data!= null) {
onGetUser(userResult.data!.data!, context);
}
} else if(userResult is Error){
var exception = (userResult as Error).exception;
if(exception is BusinessErrorException){
Fluttertoast.showToast(msg: exception.errorMsg.toString());
}
}
}

业务请求与异常处理

收起阅读 »

Android如何优雅地解决重复Drawable资源

1. 前言 最近鸿洋大神和路遥大佬分别在他们的公众号上发布了关于解决Shape/Selector冗余的方案。这篇文章在上周末就已经写好了。虽然类似的解决方案特别多,实现思路也都差不多。但我仍然要安利一下我的这个解决方案。原因有以下几点。 很纯粹,就是用代...
继续阅读 »

1. 前言



最近鸿洋大神和路遥大佬分别在他们的公众号上发布了关于解决Shape/Selector冗余的方案。这篇文章在上周末就已经写好了。虽然类似的解决方案特别多,实现思路也都差不多。但我仍然要安利一下我的这个解决方案。原因有以下几点。




  1. 很纯粹,就是用代码的方式实现了xml实现的Drawable,不用重写自定义View或者Hook系统的基础组件。




  2. 最大程度的复刻xml所拥有的能力,甚至连单位dp还是px的api都提供好了。




  3. 使用Builder模式将方法和参数都约束起来,使用起来很方便,不用去众多的api中寻找方法。结合Kotlin的语法,一个字“香”。




  4. 内部实现了缓存策略,以及根据Hash判重策略,这也是目前市面上的其他解决方案所没有的。




当然美中不足的是,目前所有的xml替换都是需要手工去完成,如果在编译期能够通过gradle插件自动转换,那就完美了。如果您有相关的经验,可以尝试一起把这个库做得更好。



2. Android为什么用xml生成Drawable


xml是Android系统中生成Drawable的首选方案,所以很多同学都习惯了使用xml生成GradientDrawable和SelectorDrawable,它们确实很方便。但是随之而来的问题,我相信很多同学都是深有体会的,哪怕是GradientDrawable中一个圆角大小的改动,或者一个颜色值的改动,都需要在原来的xml文件基础上拷贝一份新的xml文件,这样导致项目中的drawable文件越来越多。甚至一些编码规范没做好的团队,明明完全一样效果的drawable在项目中也有可能出现多份。


针对这种情况,有没有必要处理呢?大部分的xml文件也就1 2kb,占用空间很小,对包体积大小影响也没那么大。虽然说Android系统Drawable缓存是以文件名为维度的,但是它的回收策略做的挺棒的,冗余的xml对内存占用有影响,但没那么大。


那就任由文件数量膨胀吗?我觉得答案是见仁见智的,不处理也可以,无非就是写起来臃肿点呗,至少不用花时间去想一套解决方案。当然我们也可以精益求精,使用代码生成Drawable方案,实现与xml完全一样的效果,同时又能避免冗余的xml文件出现。


意外的收获👉 在项目使用svg作为图片时,发现在Android5.0 和Android6.0手机上,xml定义的selector图片显示不正常。究其原因是因为Android7.0以下不支持svg格式的fillType,导致selector渲染出来的图片有问题。想了很多方法都无法解决,最终通过代码生成selector的方案解决了。



在开始写通过代码生成Drawable之前,首先思考一个问题?为什么Android系统会首选xml生成Drawable方案呢?


通过分析xml渲染Drawable原理,我觉得系统兼容可能是使用xml的一个重要原因。以GradientDrawable的setPadding方法为例,该方法在Android Q版本引入。如果我们在xml文件引入padding,在Android Q以下版本也不会出问题。如果是代码中使用就需要做版本判断


<padding android:top="10dp" android:bottom="10dp" android:left="10dp" android:right="10dp"></padding>
复制代码


闲话少叙,先看看最终的效果,下图左边是通过xml生成GradientDrawable,右边是通过代码生成GradientDrawable效果。



3. xml实现和代码实现


看下具体代码实现



  1. GradientDrawable xml实现



2. GradientDrawable代码实现



  1. StateListDrawable xml实现



4. StateListDrawable 代码实现


addState(StatePressed)方法表示android:state_pressed="true"


minusState(StatePressed)方法表示android:state_pressed="false"


当然也可以添加多个状态



4. 特性以及源码


该库有以下特性:



  1. xml能实现的,它全部能实现

  2. 使用Builder模式,更容易构建Drawable

  3. 支持所有的android:state_xxx

  4. GradientDrawable,只要所有构建的参数内容一样(顺序可以打乱),内存中只会保留一份

作者:字节小站
链接:https://juejin.cn/post/7068462054213943304
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter快速开发——列表分页加载封装

在 App 中,列表数据加载是一个很常见的功能,几乎大多数 App 中都存在列表数据的展示,而对于大数据量的列表展示,为提高用户体验、减少服务器压力等,一般采用分页加载列表数据,首次只加载一页数据,当用户向下滑动列表到底部时再触发加载下一页数据。为方便开发过程...
继续阅读 »

在 App 中,列表数据加载是一个很常见的功能,几乎大多数 App 中都存在列表数据的展示,而对于大数据量的列表展示,为提高用户体验、减少服务器压力等,一般采用分页加载列表数据,首次只加载一页数据,当用户向下滑动列表到底部时再触发加载下一页数据。

为方便开发过程中快速实现列表分页的功能,对列表分页加载统一封装是必不可少的,这样在开发过程中只需关注实际的业务逻辑而不用在分页数据加载的处理上花费过多时间,从而节省开发工作量、提高开发效率。

0x00 效果

首先来看一下经过封装后的列表分页加载的效果:

paging.gif

封装后的使用示例代码:

State:

class ArticleListsState  extends PagingState<Article>{
}

Controller:

class ArticleListsController extends PagingController<Article, ArticleListsState> {
final ArticleListsState state = ArticleListsState();
/// 用于接口请求
final ApiService apiService = Get.find();


@override
ArticleListsState getState() => ArticleListsState();

@override
Future<PagingData<Article>?> loadData(PagingParams pagingParams) async{
/// 请求接口数据
PagingData<Article>? articleList = await apiService.getArticleList(pagingParams);
return articleList;
}
}

View:

class ArticleListsPage extends StatelessWidget {
final controller = Get.find<ArticleListsController>();
final state = Get.find<ArticleListsController>().state;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("文章列表")),
body: buildRefreshListWidget<Article,ArticleListsController>(itemBuilder: (item, index){
return _buildItem(item);
}),
);
}

/// item 布局
Widget _buildItem(Article item) {
return Card(...);
}
}

0x01 实现

上面展示了通过封装后的列表分页加载实现的文章列表效果并附上了关键示例代码,通过示例代码可以看出,在使用封装后的列表分页加载功能时只需要关注数据请求本身和界面布局展示,而无需关注分页的具体细节,使列表分页加载的实现变得更简单。下面将通过代码介绍具体如何实现列表分页加载的封装。

整体介绍

在看具体实现之前,先带大家从整体结构、最终实现的功能、使用到的三方库上做一个整体介绍。

整体结构

整个列表封装分为三层,StateControllerView

  • State: 用于存放界面状态数据,一个复杂的界面可能存在很多的状态数据,为了便于对状态数据的维护将其统一放到 State 里,对于有列表分页加载的页面,其列表数据也统一封装到 State 里。
  • Controller: 页面业务逻辑处理。
  • View: 界面 UI 元素,即 Widget 。

实现功能

封装后的列表分页加载实现功能主要如下:

  • 列表数据显示
  • 下拉刷新
  • 上拉加载
  • 自动判断是否还有更多数据
  • 自动处理分页逻辑
  • 列表 item 点击事件封装

使用到的第三方库

列表分页加载封装中 GetX 主要使用到了依赖管理和状态管理,当然 GetX 除了依赖管理还有很多其他功能,因本篇文章主要介绍列表分页的封装,不会过多介绍 GetX,关于 GetX 更多使用及介绍可参考以下文章:

具体实现

前面介绍了对于列表分页加载的封装整体分为三层:StateControllerView,而封装的主要工作就是对这三层的封装,实现 PagingState 、PagingController 的基类以及 buildRefreshListWidget 函数的封装。

PagingState

PagingState 用于封装保存分页状态数据及列表数据,不涉及实际业务逻辑处理,源码如下:

class PagingState<T>{

/// 分页的页数
int pageIndex = 1;

///是否还有更多数据
bool hasMore = true;

/// 用于列表刷新的id
Object refreshId = Object();

/// 列表数据
List<T> data = <T>[];
}

PagingState 有一个泛型 T 为列表 data 的 item 类型 ,即列表数据 item 的数据实体类型。refreshId 刷新列表界面的 id,用于后面 Controller 刷新指定 Widget 使用,属于 GetX 状态管理的功能,具体可详阅 GetX 相关文章。其他变量的作用在注释里描述得很详细,这里就不作赘述了。

PagingController

PagingController 封装分页的逻辑处理,源码如下:

abstract class PagingController<M,S extends PagingState<M>> extends GetxController{

/// PagingState
late S pagingState;
/// 刷新控件的 Controller
RefreshController refreshController = RefreshController();

@override
void onInit() {
super.onInit();
/// 保存 State
pagingState = getState();
}

@override
void onReady() {
super.onReady();
/// 进入页面刷新数据
refreshData();
}


/// 刷新数据
void refreshData() async{
initPaging();
await _loadData();
/// 刷新完成
refreshController.refreshCompleted();
}

///初始化分页数据
void initPaging() {
pagingState.pageIndex = 1;
pagingState.hasMore = true;
pagingState.data.clear();
}

/// 数据加载
Future<List<M>?> _loadData() async {
PagingParams pagingParams = PagingParams.create(pageIndex: pagingState.pageIndex);
PagingData<M>? pagingData = await loadData(pagingParams);
List<M>? list = pagingData?.data;

/// 数据不为空,则将数据添加到 data 中
/// 并且分页页数 pageIndex + 1
if (list != null && list.isNotEmpty) {
pagingState.data.addAll(list);
pagingState.pageIndex += 1;
}

/// 判断是否有更多数据
pagingState.hasMore = pagingState.data.length < (pagingData?.total ?? 0);

/// 更新界面
update([pagingState.refreshId]);
return list;
}


/// 加载更多
void loadMoreData() async{
await _loadData();
/// 加载完成
refreshController.loadComplete();
}

/// 最终加载数据的方法
Future<PagingData<M>?> loadData(PagingParams pagingParams);

/// 获取 State
S getState();

}

PagingController 继承自 GetxController ,有两个泛型 MS ,分别为列表 item 的数据实体类型和 PageState 的类型。

成员变量 pagingState 类型为泛型 S 即 PagingState 类型,在 onInit 中通过抽象方法 getState 获取,getState 方法在子类中实现,返回 PagingState 类型对象。

refreshController 为 pull_to_refresh 库中控制刷新控件 SmartRefresher 的 Controller ,用于控制刷新/加载完成。

refreshData 、loadMoreData 方法顾名思义是下拉刷新和上拉加载更多,在对应事件中调用,其内部实现调用 _loadData 加载数据,加载完成后调用 refreshController 的刷新完成或加载完成, refreshData 中加载数据之前还调用了初始化分页数据的 initPaging 方法,用于重置分页参数和数据。

_loadData 是数据加载的核心代码,首先创建 PagingParams 对象,即分页请求数据参数实体,创建时传入了分页的页数,值为 PagingState 中维护的分页页数 pageIndexPagingParams 实体源码如下:

class PagingParams {

int current = 1;
Map<String, dynamic>? extra = {};
Map<String, dynamic> model = {};
String? order = 'descending';
int size = 10;
String? sort = "id";

factory PagingParams.create({required int pageIndex}){
var params = PagingParams();
params.current = pageIndex;
return params;
}
}

字段包含当前页数、每页数据条数、排序字段、排序方式以及扩展业务参数等。此类可根据后台接口分页请求协议文档进行创建。

分页参数创建好后,调用抽象方法 loadData 传入创建好的参数,返回 PagingData 数据,即分页数据实体,源码如下:

class PagingData<T> {

int? current;
int? pages;
List<T>? data;
int? size;
int? total;

PagingData();

factory PagingData.fromJson(Map<String, dynamic> json) => $PagingDataFromJson<T>(json);

Map<String, dynamic> toJson() => $PagingDataToJson(this);

@override
String toString() {
return jsonEncode(this);
}
}

该实体包含列表的真实数据 data ,以及分页相关参数,比如当前页、总页数、总条数等,可根据后台分页接口返回的实际数据进行调整。其中 fromJson 、toJson 是用于 json 数据解析和转换用。

关于 json 数据解析可参考前面写的 : Flutter应用框架搭建(三)Json数据解析

数据加载完成后,判断数据是否为空,不为空则将数据添加到 data 集合中,并且分页的页数加 1。然后判断是否还有更多数据,此处是根据 data 中的数据条数与分页返回的总条数进行比较判断的,可能不同团队的分页接口实现规则不同,可根据实际情况进行调整,比如使用页数进行判断等。

方法最后调用了 Controller 的 update 方法刷新界面数据。

流程如下:

paging2.png

View

View 层对 ListView 和 pull_to_refresh 的 SmartRefresher 进行封装,满足列表数据展示和下拉刷新/上拉加载更多功能。其封装主要为 Widget 参数配置的封装,涉及业务逻辑代码不多,故未将其封装为 Widget 控件,而是封装成方法进行调用, 共三个方法:

  • buildListView: ListView 控件封装
  • buildRefreshWidget: 下拉刷新/上拉加载更多控件封装
  • buildRefreshListWidget: 带分页加载的 ListView 控件封装

其中前面两个是单独分别对 ListView 和 SmartRefresher 的封装,第三个则是前两者的结合。

buildListView:

Widget buildListView<T>(
{required Widget Function(T item, int index) itemBuilder,
required List<T> data,
Widget Function(T item, int index)? separatorBuilder,
Function(T item, int index)? onItemClick,
ScrollPhysics? physics,
bool shrinkWrap = false,
Axis scrollDirection = Axis.vertical}) {
return ListView.separated(
shrinkWrap: shrinkWrap,
physics: physics,
padding: EdgeInsets.zero,
scrollDirection: scrollDirection,
itemBuilder: (ctx, index) => GestureDetector(
child: itemBuilder.call(data[index], index),
onTap: () => onItemClick?.call(data[index], index),
),
separatorBuilder: (ctx, index) =>
separatorBuilder?.call(data[index], index) ?? Container(),
itemCount: data.length);
}

代码不多,主要是对 ListView 的常用参数包装了一遍,并添加了泛型 T 即列表数据 item 的类型。其次对 itemCount 和 itemBuilder 做了特殊处理, itemCount 赋值为 data.length 列表数据的长度;ListView 的 itemBuilder 调用了传入的 itemBuilder 方法,后者参数与 ListView 的参数有区别,传入的是 item 数据和下标 index, 且使用 GestureDetector 包裹封装了 item 点击事件调用onItemClick

buildRefreshWidget:

Widget buildRefreshWidget({
required Widget Function() builder,
VoidCallback? onRefresh,
VoidCallback? onLoad,
required RefreshController refreshController,
bool enablePullUp = true,
bool enablePullDown = true
}) {
return SmartRefresher(
enablePullUp: enablePullUp,
enablePullDown: enablePullDown,
controller: refreshController,
onRefresh: onRefresh,
onLoading: onLoad,
header: const ClassicHeader(idleText: "下拉刷新",
releaseText: "松开刷新",
completeText: "刷新完成",
refreshingText: "加载中......",),
footer: const ClassicFooter(idleText: "上拉加载更多",
canLoadingText: "松开加载更多",
loadingText: "加载中......",),
child: builder(),
);
}

对 SmartRefresher 参数进行封装,添加了 header 和 footer 的统一处理,这里可以根据项目实际需求进行封装,可以使用其他下拉刷新/上拉加载的风格或者自定义实现效果,关于 SmartRefresher 的使用请参考官网 : flutter_pulltorefresh

buildRefreshListWidget:

Widget buildRefreshListWidget<T, C extends PagingController<T, PagingState<T>>>(
{
required Widget Function(T item, int index) itemBuilder,
bool enablePullUp = true,
bool enablePullDown = true,
String? tag,
Widget Function(T item, int index)? separatorBuilder,
Function(T item, int index)? onItemClick,
ScrollPhysics? physics,
bool shrinkWrap = false,
Axis scrollDirection = Axis.vertical
}) {
C controller = Get.find(tag: tag);
return GetBuilder<C>(builder: (controller) {
return buildRefreshWidget(
builder: () =>
buildListView<T>(
data: controller.pagingState.data,
separatorBuilder: separatorBuilder,
itemBuilder: itemBuilder,
onItemClick: onItemClick,
physics: physics,
shrinkWrap: shrinkWrap,
scrollDirection: scrollDirection
),
refreshController: controller.refreshController,
onRefresh: controller.refreshData,
onLoad: controller.loadMoreData,
enablePullDown: enablePullDown,
enablePullUp: enablePullUp && controller.pagingState.hasMore,
);
}, tag: tag, id: controller.pagingState.refreshId,);
}

buildRefreshListWidget 是对前面两者的再次封装,参数也基本上是前面两者的结合,buildRefreshWidget 的 builder 传入的是 buildListView 。

为了将下拉刷新、上拉加载更多的操作进行统一封装,这里引入了 PagingController 的泛型 C 并通过 GetX 的依赖管理获取到当前的 PagingController 实例 controller:

  • buildListView 的 data 传入 PagingState 的 data 即分页数据,即 controller.pagingState.data
  • refreshController 传入 PagingController 中创建的 refreshController 对象,即 controller.refreshController
  • onRefresh / onRefresh 调用 PagingController 的 refreshData / loadMoreData 方法
  • enablePullUp 使用方法传入的 enablePullUp 和 PagingState 的 hasMore(是否有更多数据) 共同判断

列表数据加载完成后将自动刷新界面,这里使用了 GetBuilder 包裹 buildRefreshWidget,并添加 tag 和 id 参数,其中 tag 是 GetX 依赖注入的 tag ,用于区分注入的实例, id 则为刷新的 id,可通过 id 刷新指定控件,这里传入的就是 PagingState 里定义的 refreshId ,即刷新指定列表。

整体 View 结构如下:

paging3.png

0x02 总结

经过上诉的封装后就能快速实现文章开头展示的列表分页加载效果,通过简单的代码就能实现完整的列表分页加载功能,让开发者关注业务本身,从而节省开发工作量、提高开发效率和质量。最后附上一张整体的结构关系图:

paging4.png

源码:flutter_app_core


作者:loongwind
链接:https://juejin.cn/post/7067455179506860069
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Binder机制和AIDL的理解

Android 进程间通信 为什么要去理解Android的进程间通信机制 对于Android开发工程师来说,如果不去理解进程间通信机制也可以使用系统提供的API完成应用开发,但如果想要达到更高的层级,那么就不能简单只会调用API。无论是工作中遇到一些疑难问题,...
继续阅读 »

Android 进程间通信


为什么要去理解Android的进程间通信机制


对于Android开发工程师来说,如果不去理解进程间通信机制也可以使用系统提供的API完成应用开发,但如果想要达到更高的层级,那么就不能简单只会调用API。无论是工作中遇到一些疑难问题,还是想要学习源码的一些功能实现,或者是想要提升APP的性能等,这些工作都需要我们去看系统的源码,而系统的源码中进程间通信无处不在,如果不理解进程间通信机制,那么很难看懂系统源码,而且容易迷失在大量的代码中。


Android 进程间通信机制


为什么使用Binder作为Android进程间通信机制


Android Bander设计与实现 - 设计篇 这篇文章写得很好了。主要是为了弥补Linux中其他进程间通信方式得性能和安全性不足。当然Binder机制也并非是谷歌为了Android原创技术,Binder机制源于OpenBinder,OpenBinder要早于Android系统出现。所以如果想要立即Android得进程间通信,主要就是理解Binder机制。


Binder进程间通信基本框架



在Android中,2个应用或者进程之间的通信都需要经过Binder代理,二者不能直接通信,同样APP在使用系统服务时也需要跨进程通信,比如我们最常用的ActivityManagerService(AMS)也是一个系统服务进程,此外APP使用WIFI 、定位、媒体服务等都是系统进程,APP想要使用这些系统服务的功能一定要通过Binder进行通信。


Binder到底是什么


我们一直在说利用Binder机制进行进程间通信,那么Binder具体是什么?是一个Java类,还是一个底层驱动?通常我们说Binder机制是Android系统不同层Binder相关代码组成的一套跨进程通信功能。Binder机制相关代码从最底层的驱动层到最顶层的应用层都有,所以要读懂Binder机制,就需要我们耐心的逐层进行分析。



Binder机制代码结构


如何理解AIDL


我们从上图没有看到任何AIDL相关的信息,也就是说Binder机制是与AIDL无关的,那么我们日常中如果要跨进程都要写一个AIDL类然后由AS生成一些Java类,我们使用这些类实现进程间通信,这么做的目的其实是由AS帮我们生成一些模板代码,减少我们的工作和出错概率,其实不用AIDL我们也可以实现Binder通信,并且可以更好的理解Binder机制。下面我写一个Demo进程,这个Demo中有AIDL文件并生成相关代码,但我们不用,只是用来作为对比,我们用最少的代码实现Binder通信,并通过对比我们写的代码和AIDL生成的代码来更好的理解AIDL生成的代码的作用。代码Github


不使用ADIL,手动实现进程间通信



项目结构


代码中client为客户端,server为服务端




客户端进程发送一个字符串给服务端,服务端进程接收到将字符显示到界面上。项目中没有用到AIDL为我们生成Binder通信类,而是用最简单的方式实现Binder通信,因而我们可以看清Binder通信最关键的地方。首先我们要知道,实现了IBinder接口的类的对象是可以跨进程传递的。


服务端

1.服务端RemoteService继承Service

2.创建一个继承Binder的类ServerBinder,并覆写onTransact方法,用于处理Client的调用,Binder实现了IBinder接口

3.服务端覆写Service的onBind方法,返回一个ServerBinder对象(这个ServerBinder对象是最终传递给Client端)


public class RemoteService extends Service {
public static final int TRANSAVTION_showMessage = IBinder.FIRST_CALL_TRANSACTION;

@Nullable
@Override
public IBinder onBind(Intent intent) {
return new ServerBinder();
}

static class ServerBinder extends Binder {
public ServerBinder() {
}

@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {

switch (code) {
case TRANSAVTION_showMessage:
String message = data.readString();
Log.d("ServerBinder", "showMessage " + message);
if (ServerMainActivity.tvShowMessage != null) {//显示收到数据逻辑
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ServerMainActivity.tvShowMessage.setText(message);
}
});
}
if (reply != null) {
reply.writeNoException();
}
return true;
}
return super.onTransact(code, data, reply, flags);
}


}
}

客户端

1.客户端创建一个ServiceConnection对象,用于与服务端建立连接,并获取到服务端的IBinder对象

2.客户端通过bindService与服务端的RemoteService建立连接


public class ClientMainActivity extends AppCompatActivity {
private Button mSendString;
private EditText mStingEditText;
public static final int TRANSAVTION_showMessage = IBinder.FIRST_CALL_TRANSACTION;
private IBinder mServer;//服务端的Binder对象
private boolean isConnection = false;

private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {

isConnection = true;
mServer = service;//建立连接成功,保存服务端进程的IBinder对象
Log.d("Client"," onServiceConnected success");
}

@Override
public void onServiceDisconnected(ComponentName name) {
isConnection = false;
}
};

//与服务端进程中RemoteService建立连接
private void attemptToBindService() {
Intent intent = new Intent();
intent.setClassName("com.binder.server", "com.binder.server.RemoteService");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSendString = findViewById(R.id.btn_send_string);
mStingEditText = findViewById(R.id.et_string);
mSendString.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isConnection) {
attemptToBindService();
return;
}
//发送数据给服务端进程
Parcel data = Parcel.obtain();
Parcel replay = Parcel.obtain();
if (mServer != null) {
try {
data.writeString(mStingEditText.getText().toString());
Log.d("Client"," mServer.transact call");
//发送数据到服务端进程
mServer.transact(TRANSAVTION_showMessage, data, replay, 0);
replay.readException();
} catch (RemoteException e) {
e.printStackTrace();
} finally {
replay.recycle();
data.recycle();
}
}


}
});
}

@Override
protected void onStart() {
super.onStart();
if (!isConnection) {
attemptToBindService();
}
}

从上面的代码来看Binder的跨进程通信核心就是客户端获取到服务端的IBinder对象,然后调用这个对象的transact方法发送数据,实现进程间通信。


使用ADIL生成相关类,进行进程间通信



加入AIDL文件


interface IShowMessageAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void showMessage(String msg);
}

修改Client端代码

public class ClientMainActivityUseAidl extends AppCompatActivity {
private Button mSendString;
private EditText mStingEditText;
private IShowMessageAidlInterface mServer;//服务端的Binder对象代理
private boolean isConnection = false;

private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
isConnection = true;
//调用IShowMessageAidlInterface.Stub.asInterface静态方法,将service转换为一接口
mServer = IShowMessageAidlInterface.Stub.asInterface(service);
Log.d("Client"," onServiceConnected success");
}

@Override
public void onServiceDisconnected(ComponentName name) {
isConnection = false;
}
};
private void attemptToBindService() {
Intent intent = new Intent();
intent.setClassName("com.binder.server", "com.binder.server.RemoteServiceUseAidl");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSendString = findViewById(R.id.btn_send_string);
mStingEditText = findViewById(R.id.et_string);
mSendString.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isConnection) {
attemptToBindService();
return;
}
try {
//直接调用接口的showMessage方法
mServer.showMessage(mStingEditText.getText().toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
});
}

@Override
protected void onStart() {
super.onStart();
if (!isConnection) {
attemptToBindService();
}
}

1.客户端利用 IShowMessageAidlInterface生成类中的Stub内部类将接受到的IBinder对象转换为一个接口

2.在发送数据时,直接调用IShowMessageAidlInterface接口的showMessage方法


asInterface方法

   public static com.binder.server.IShowMessageAidlInterface asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
//查询obj对象是否是本地接口,也就是是不是在同一个进程
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.binder.server.IShowMessageAidlInterface))) {
如果是同一个进程直接返回
return ((com.binder.server.IShowMessageAidlInterface)iin);
}
//如果是不同进程,则将IBinder对象利用Proxy封装一层
return new com.binder.server.IShowMessageAidlInterface.Stub.Proxy(obj);
}

Proxy类

 private static class Proxy implements com.binder.server.IShowMessageAidlInterface
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
//代理对象做的工作是把AIDL接口中定义的方法中的数据进行封装,方便进行跨进程传输
@Override public void showMessage(java.lang.String msg) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(msg);
boolean _status = mRemote.transact(Stub.TRANSACTION_showMessage, _data, _reply, 0);
if (!_status && getDefaultImpl() != null) {
getDefaultImpl().showMessage(msg);
return;
}
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
public static com.binder.server.IShowMessageAidlInterface sDefaultImpl;
}

所以我们可以知道,客户端用到了AIDL文件生成Stub类中的asInterface方法,把接收到的远程IBinder转换为IShowMessageAidlInterface接口,而这个接口的具体实现其实是Proxy类,代理类对方法传入数据进行封装,然后发送给mRemote 服务端。


修改服务端代码


public class RemoteServiceUseAidl extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new IShowMessageAidlInterface.Stub() {
@Override
public void showMessage(String msg) throws RemoteException {
if (ServerMainActivity.tvShowMessage != null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ServerMainActivity.tvShowMessage.setText(msg);
}
});
}
}
};
}
}

服务端的 onBind方法返回AIDL生成的Stub类的对象,Stub是个抽象类,其中待实现的方法为AIDL中定义的showMessage方法。


 public static abstract class Stub extends android.os.Binder implements com.binder.server.IShowMessageAidlInterface
{
private static final java.lang.String DESCRIPTOR = "com.binder.server.IShowMessageAidlInterface";
static final int TRANSACTION_showMessage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
java.lang.String descriptor = DESCRIPTOR;
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(descriptor);
return true;
}
case TRANSACTION_showMessage:
{
data.enforceInterface(descriptor);
java.lang.String _arg0;
_arg0 = data.readString();
this.showMessage(_arg0);
reply.writeNoException();
return true;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
}

}

可以看到Sub抽象类中继承自Binder,也就是客端最终拿到的是这个Stub IBinder对象,客户端调用tansact方法最终会调用到Stub类的onTransact进行处理,Stub的onTransact方法根据code确定客端户调用了哪个方法,然后对接收到的data数据进行读取解析,将处理好的数据交给IShowMessageAidlInterface中对应的方法。


总结:

1.AIDL生成的类中Stub的静态方法asInterface和Proxy类是给客户端用于发送数据的

2.Stub抽象类是由服务端实现,接收处理客户端数据的


作者:传道士
链接:https://juejin.cn/post/7069013946929250311
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

深入探索Flutter性能优化

目录 一、检测手段 1、Flutter Inspector 2、性能图层 3、Raster 线程问题 4、UI 线程问题定位 5、检查多视图叠加的视图渲染开关 checkerboardOffscreenLayers 6、检查缓存的图像开关 checkerb...
继续阅读 »

目录



  • 一、检测手段

    • 1、Flutter Inspector

    • 2、性能图层

    • 3、Raster 线程问题

    • 4、UI 线程问题定位

    • 5、检查多视图叠加的视图渲染开关 checkerboardOffscreenLayers

    • 6、检查缓存的图像开关 checkerboardRasterCacheImages



  • 二、关键优化指标

    • 1、页面异常率

    • 2、页面帧率

    • 3、页面加载时长



  • 三、布局加载优化

    • 1、常规优化

    • 2、深入优化



  • 四、启动速度优化

    • 1、引擎预加载

    • 2、Dart VM 预热



  • 五、内存优化

    • 1、const 实例化

    • 2、识别出消耗多余内存的图片

    • 3、针对 ListView item 中有 image 的情况来优化内存



  • 六、包体积优化

    • 1、图片优化

    • 2、移除冗余的二三库

    • 3、启用代码缩减和资源缩减

    • 4、构建单 ABI 架构的包



  • 七、总结


前言


Flutter 作为目前最火爆的移动端跨平台框架,能够帮助开发者通过一套代码库高效地构建多平台的精美应用,并支持移动、Web、桌面和嵌入式平台。对于 Android 来说,Flutter 能够创作媲美原生的高性能应用,但是,在较为复杂的 App 中,使用 Flutter 开发也很难避免产生各种各样的性能问题。在这篇文章中,我将和你一起全方位地深入探索 Flutter 性能优化的疆域。


一、检测手段


准备


以 profile 模式启动应用,如果是混合 Flutter 应用,在 flutter/packages/flutter_tools/gradle/flutter.gradle 的 buildModeFor 方法中将 debug 模式改为 profile即可。


为什么要在分析模式下来调试应用性能?


分析模式在发布模式的基础之上,为分析工具提供了少量必要的应用追踪信息。


那,为什么要在发布模式的基础上来调试应用性能?


与调试代码可以在调试模式下检测 Bug 不同,性能问题需要在发布模式下使用真机进行检测。这是因为,相比发布模式而言,调试模式增加了很多额外的检查(比如断言),这些检查可能会耗费很多资源,而更重要的是,调试模式使用 JIT 模式运行应用,代码执行效率较低。这就使得调试模式运行的应用,无法真实反映出它的性能问题


而另一方面,模拟器使用的指令集为 x86,而真机使用的指令集是 ARM。这两种方式的二进制代码执行行为完全不同,因此,模拟器与真机的性能差异较大,例如,针对一些 x86 指令集擅长的操作,模拟器会比真机快,而另一些操作则会比真机慢。这也同时意味着,你无法使用模拟器来评估真机才能出现的性能问题。


1、Flutter Inspector


Flutter Inspector有很多功能,但你应该把注意力花在更有用的功能学习上,例如:“Select Widget Mode” 和 “Repaint Rainbow”


Select Widget Mode


点击 “Select Widget Mode” 图标,可以在手机上查看当前页面的布局框架与容器类型。



作用


快速查看陌生页面的布局实现方式


Repaint Rainbow


点击 “Repaint Rainbow” 图标,它会 为所有 RenderBox 绘制一层外框,并在它们重绘时会改变颜色



作用


帮你找到 App 中频繁重绘导致性能消耗过大的部分


例如:一个小动画可能会导致整个页面重绘,这个时候使用 RepaintBoundary Widget 包裹它,可以将重绘范围缩小至本身所占用的区域,这样就可以减少绘制消耗。


使用场景


例如 页面的进度条动画刷新时会导致整个布局频繁重绘


缺点


使用 RepaintBoundary Widget 会创建额外的绘制画布,这将会增加一定的内存消耗


2、性能图层


性能图层会在当前应用的最上层,以 Flutter 引擎自绘的方式展示 Raster 与 UI 线程的执行图表,而其中每一张图表都代表当前线程最近 300 帧的表现,如果 UI 产生了卡顿(跳帧),这些图表可以帮助你分析并找到原因。


蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧,如果其中有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条


如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码的执行时间。如下图所示:



3、Raster 线程问题定位


它定位的是 渲染引擎底层渲染的异常


解决方案是 把需要静态缓存的图像加入到 RepaintBoundary。而 RepaintBoundary 可以确定 Widget 树的重绘边界,如果图像足够复杂,Flutter 引擎会自动将其缓存,避免重复刷新。当然,因为缓存资源有限,如果引擎认为图像不够复杂,也可能会忽略 RepaintBoundary。


4、UI 线程问题定位


问题场景


在视图构建时,在 build 方法中使用了一些复杂的运算,或是在主 Isolate 中进行了同步的 I/O 操作。


使用 Performance 进行检测


点击 Android Studio 底部工具栏中的 “Open DevTools” 按钮,然后在打开的 Dart DevTools 网页中将顶部的 tab 切换到 Performance。


与性能图层能够自动记录应用执行的情况不同,使用 Performance 来分析代码执行轨迹,你需要手动点击 “Record” 按钮去主动触发,在完成信息的抽样采集后,点击 “Stop” 按钮结束录制。这时,你就可以得到在这期间应用的执行情况了。


使用 Performance 记录应用的执行情况,即 CPU 帧图,又被称为火焰图。火焰图是基于记录代码执行结果所产生的图片,用来展示 CPU 的调用栈,表示的是 CPU 的繁忙程度


其中:



  • y 轴:表示调用栈,其每一层都是一个函数。调用栈越深,火焰就越高,底部就是正在执行的函数,上方都是它的父函数

  • x 轴:表示单位时间,一个函数在 x 轴占据的宽度越宽,就表示它被采样到的次数越多,即执行时间越长


所以,我们要 检测 CPU 耗时问题,皆可以查看火焰图底部的哪个函数占据的宽度最大。只要有 “平顶”,就表示该函数可能存在性能问题。如下图所示:



一般的耗时问题,我们通常可以 使用 Isolate(或 compute)将这些耗时的操作挪到并发主 Isolate 之外去完成



dart 的单线程执行异步任务是怎么实现的?



网络调用的执行是由操作系统提供的另外的底层线程做的,而在 event queue 里只会放一个网络调用的最终执行结果(成功或失败)和响应执行结果的处理回调。


5、使用 checkerboardOffscreenLayers 检查多视图叠加的视图渲染


只要在 MaterialApp 的初始化方法中,将 checkerboardOffscreenLayers 开关设置为 true,分析工具就会自动帮你检测多视图叠加的情况。


这时,使用了 saveLayer 的 Widget 会自动显示为棋盘格式,并随着页面刷新而闪烁。


而 saveLayer 一般会通过一些功能性 Widget,在涉及需要剪切或半透明蒙层的场景中间接地使用。


6、使用 checkerboardRasterCacheImages 检查缓存的图像


它也是用来检测在界面重绘时频繁闪烁的图像(即没有静态缓存)。解决方案是把需要静态缓存的图像加入到 RepaintBoundary。


二、关键优化指标


1、页面异常率


页面异常率,即 页面渲染过程中出现异常的概率。


它度量的是页面维度下功能不可用的情况,其统计公式为:



页面异常率 = 异常发生次数 / 整体页面 PV 数。



统计异常发生次数


利用 Zone 与 FlutterError 这两个方法,然后在异常拦截的方法中,去累计异常的发生次数。


统计整体页面 PV 数


继承自 NavigatorObserver 的观察者,并在其 didPush 方法中,去累加页面的打开次数。


2、页面帧率


Flutter 在全局 Window 对象上提供了帧回调机制。我们可以在 Window 对象上注册 onReportTimings 方法,将最近绘制帧耗费的时间(即 FrameTiming),以回调的形式告诉我们。


有了每一帧的绘制时间后,我们就可以计算 FPS 了。


为了让 FPS 的计算更加平滑,我们需要保留最近 25 个 FrameTiming 用于求和计算。


由于帧的渲染是依靠 VSync 信号驱动的,如果帧绘制的时间没有超过 16.67 ms,我们也需要把它当成 16.67 ms 来算,因为绘制完成的帧必须要等到下一次 VSync 信号来了之后才能渲染。而如果帧绘制时间超过了 16.67 ms,则会占用后续 VSync 的信号周期,从而打乱后续的绘制次序,产生卡顿现象。


那么,页面帧率的统计公式就是:



FPS = 60 * 实际渲染的帧数 / 本来应该在这个时间内渲染完成的帧数。



首先,定义一个容量为 25 的列表,用于存储最近的帧绘制耗时 FrameTiming。


然后,在 FPS 的计算函数中,你再将列表中每帧绘制时间与 VSync 周期 frameInterval 进行比较,得出本来应该绘制的帧数。


最后,两者相除就得到了 FPS 指标。


3、页面加载时长



页面加载时长 = 页面可见的时间 - 页面创建的时间(包括网络加载时长)



统计页面可见的时间


WidgetsBinding 提供了单次 Frame 回调的 addPostFrameCallback 方法,它会在当前 Frame 绘制完成之后进行回调,并且只会回调一次。一旦监听到 Frame 绘制完成回调后,我们就可以确认页面已经被渲染出来了,因此我们可以借助这个方法去获取页面的渲染完成时间 endTime。


统计页面创建的时间


获取页面创建的时间比较容易,我们只需要在页面的初始化函数 initState() 里记录页面的创建时间 startTime。


最后,再将这两个时间做减法,你就能得到页面的加载时长。


需要注意的是,正常的页面加载时长一般都不应该超过2秒。如果超过了,则意味着有严重的性能问题。


三、布局加载优化



Flutter 为什么要使用声明书 UI 的编写方式?



为了减轻开发人员的负担,无需编写如何在不同的 UI 状态之间进行切换的代码,Flutter 使用了声明式的 UI 编写方式,而不是 Android 和 iOS 中的命令式编写方式。


这样的话,当用户界面发生变化时,Flutter 不会修改旧的 Widget 实例,而是会构造新的 Widget 实例


Fluuter 框架使用 RenderObjects 管理传统 UI 对象的职责(比如维护布局的状态)。 RenderObjects 在帧之间保持不变, Flutter 的轻量级 Widget 通知框架在状态之间修改 RenderObjects, 而 Flutter Framework 则负责处理其余部分。


1、常规优化


常规优化即针对 build() 进行优化,build() 方法中的性能问题一般有两种:耗时操作和 Widget 堆叠


1)、在 build() 方法中执行了耗时操作


我们应该尽量避免在 build() 中执行耗时操作,因为 build() 会被频繁地调用,尤其是当 Widget 重建的时候。


此外,我们不要在代码中进行阻塞式操作,可以将文件读取、数据库操作、网络请求等通过 Future 来转换成异步方式来完成。


最后,对于 CPU 计算频繁的操作,例如图片压缩,可以使用 isolate 来充分利用多核心 CPU。


isolate 作为 Flutter 中的多线程实现方式,之所以被称之为 isolate(隔离),是因为每一个 isolate 都有一份单独的内存


Flutter 会运行一个事件循环,它会从事件队列中取得最旧的事件,处理它,然后再返回下一个事件进行处理,依此类推,直到事件队列清空为止。每当动作中断时,线程就会等待下一个事件


实质上,不仅仅是 isolate,所有的高级 API 都能够应用于异步编程,例如 Futures、Streams、async 和 await,它们全部都是构建在这个简单的事件循环之上。


而,async 和 await 实际上只是使用 futures 和 streams 的替代语法,它将代码编写形式从异步变为同步,主要用来帮助你编写更清晰、简洁的代码。


此外,async 和 await 也能使用 try on catch finally 来进行异常处理,这能够帮助你处理一些数据解析方面的异常。


2)、build() 方法中堆砌了大量的 Widget


这将会导致三个问题:



  • 1、代码可读性差:画界面时需要一个 Widget 嵌套一个 Widget,但如果 Widget 嵌套太深,就会导致代码的可读性变差,也不利于后期的维护和扩展。

  • 2、复用难:由于所有的代码都在一个 build(),会导致无法将公共的 UI 代码复用到其它的页面或模块。

  • 3、影响性能:我们在 State 上调用 setState() 时,所有 build() 中的 Widget 都将被重建,因此 build() 中返回的 Widget 树越大,那么需要重建的 Widget 就越多,也就会对性能越不利。


所以,你需要 控制 build 方法耗时,将 Widget 拆小,避免直接返回一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用。


3)、使用 Widget 而不是函数


如果一个函数可以做同样的事情,Flutter 就不会有 StatelessWidget ,使用 StatelessWidget 的最大好处在于:能尽量避免不必要的重建。总的来说,它的优势有:



  • 1)、允许性能优化:const 构造函数,更细粒度的重建等等。

  • 2)、确保在两个不同的布局之间切换时,能够正确地处理资源(因为函数可能重用某些先前的状态)。

  • 3)、确保热重载正常工作,使用函数可能会破坏热重载。

  • 4)、在 flutter 自带的 Widget 显示工具中能看到 Widget 的状态和参数。

  • 5)、发生错误时,有更清晰的提示:此时,Flutter 框架将为你提供当前构建的 Widget 名称,更容易排查问题。

  • 6)、可以定义 key 和方便使用 context 的 API。


4)、尽可能地使用 const


如果某一个实例已经用 const 定义好了,那么其它地方再次使用 const 定义时,则会直接从常量池里取,这样便能够节省 RAM。


5)、尽可能地使用 const 构造器


当构建你自己的 Widget 或者使用 Flutter 的 Widget 时,这将会帮助 Flutter 仅仅去 rebuild 那些应当被更新的 Widget。


因此,你应该尽量多用 const 组件,这样即使父组件更新了,子组件也不会重新进行 rebuild 操作。特别是针对一些长期不修改的组件,例如通用报错组件和通用 loading 组件等。


6)、使用 nil 去替代 Container() 和 SizedBox()


首先,你需要明白 nil 仅仅是一个基础的 Widget 元素 ,它的构建成本几乎没有。


在某些情况下,如果你不想显示任何内容,且不能返回 null 的时候,你可能会返回类似 const SizedBox/Container 的 Widget,但是 SizedBox 会创建 RenderObject,而渲染树中的 RenderObject 会带来多余的生命周期控制和额外的计算消耗,即便你没有给 SizedBox 指定任何的参数。


下面,是我平时使用 nil 的一套方式:


// BEST
text != null ? Text(text) : nil
or
if (text != null) Text(text)
text != null ? Text(text) : const Container()/SizedBox()
复制代码

7)、列表优化


在构建大型网格或列表的时候,我们要尽量避免使用 ListView(children: [],) 或 GridView(children: [],)。


因为,在这种场景下,不管列表内容是否可见,会导致列表中所有的数据都会被一次性绘制出来,这种用法类似于 Android 的 ScrollView。


如果我们列表数据比较大的时候,建议使用 ListView 和 GridView 的 builder 方法,它们只会绘制可见的列表内容,类似于 Android 的 RecyclerView。


其实,本质上,就是对列表采用了懒加载而不是直接一次性创建所有的子 Widget,这样视图的初始化时间就减少了


8)、针对于长列表,记得在 ListView 中使用 itemExtent。


有时候当我们有一个很长的列表,想要用滚动条来大跳时,使用 itemExtent 就很重要了,它会帮助 Flutter 去计算 ListView 的滚动位置而不是计算每一个 Widget 的高度,与此同时,它能够使滚动动画有更好的性能


9)、减少可折叠 ListView 的构建时间


针对于可折叠的 ListView,未展开状态时,设置其 itemCount 为 0,这样 item 只会在展开状态下才进行构建,以减少页面第一次的打开构建时间


10)、尽量不要为 Widget 设置半透明效果


考虑用图片的形式代替,这样被遮挡的部分 Widget 区域就不需要绘制了。


除此之外,还有网络请求预加载优化、抽取文本 Theme 等常规的优化方式就不赘述了。


2、深入优化


1)、优化光栅线程


所有的 Flutter 应用至少都会运行在两个并行的线程上:UI 线程和 Raster 线程


**UI 线程是你构建 Widgets 和运行应用逻辑的地方。
**
Raster 线程是 Flutter 用来栅格化你的应用的。它从 UI 线程获取指令并将它们转换为可以发送到图形卡的内容。


在光栅线程中,会获取图片的字节,调整图像的大小,应用透明度、混合模式、模糊等等,直到产生最后的图形像素。然后,光栅线程会将其发送到图形卡,继而发送到屏幕上显示。


使用 Flutter DevTools-Performance 进行检测,步骤如下:



  • 1、在 Performance Overlay 中,查看光栅线程和 UI 线程哪个负载过重。

  • 2、在 Timeline Events 中,找到那些耗费时间最长的事件,例如常见的 SkCanvas::Flush,它负责解决所有待处理的 GPU 操作。

  • 3、找到对应的代码区域,通过删除 Widgets 或方法的方式来看对性能的影响。


2)、用 key 加速 Flutter 的性能优化光栅线程


一个 element 是由 Widget 内部创建的,它的主要目的是,知道对应的 Widget 在 Widget 树中所处的位置。但是元素的创建是非常昂贵的,通过 Keys(ValueKeys 和 GlobalKeys),我们可以去重复使用它们。



GlobalKey 与 ValueKey 的区别?



GlobalKey 是全局使用的 key,在跨小部件的场景时,你就可以使用它去刷新其它小部件。但,它是很昂贵的,如果你不需要访问 BuildContext、Element 和 State,应该尽量使用 LocalKey。


而 ValueKey 和 ObjectKey、UniqueKey 一样都归属于局部使用的 LocalKey,无法跨容器使用,ValueKey 比较的是 Widget 的值,而 ObjectKey 比较的是对象的 key,UniqueKey 则每次都会生成一个不同的值。


元素的生命周期



  • Mount:挂载,当元素第一次被添加到树上的时候调用。

  • Active:当需要激活之前失活的元素时被调用。

  • Update:用新数据去更新 RenderObject。

  • Deactive:当元素从 Widget 树中被移除或移动时被调用。如果一个元素在同一帧期间被移动了且它有 GlobalKey,那么它仍然能够被激活。

  • UnMount:卸载,如果一个元素在一帧期间没有被激活,它将会被卸载,并且再也不会被复用。


优化方式


**为了去改善性能,你需要去尽可能让 Widget 使用 Activie 和 Update 操作,并且尽量避免让 Widget触发 UnMount 和 Mount。**而使用 GlobayKeys 和 ValueKey 则能做到这一点:


/// 1、给 MaterialApp 指定 GlobalKeys
MaterialApp(key: global, home: child,);
/// 2、通过把 ValueKey 分配到正在被卸载的根 Widget,你就能够
/// 减少 Widget 的平均构建时间。
Widget build(BuildContext context) {
return Column(
children: [
value
? const SizedBox(key: ValueKey('SizedBox'))
: const Placeholder(key: ValueKey('Placeholder')),
GestureDetector(
key: ValueKey('GestureDetector'),
onTap: () {
setState(() {
value = !value;
});
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
!value
? const SizedBox(key: ValueKey('SizedBox'))
: const Placeholder(key: ValueKey('Placeholder')),
],
);
}


如何知道哪些 Widget 会被 Update,哪些 Widget会被 UnMount?



只有 build 直接 return 的那个根 Widget 会自动更新,其它都有可能被 UnMount,因此都需要给其分配 ValueKey。



为什么没有给 Container 分配 ValueKey?



因为 Container 是 GestureDetector 的一个子 Widget,所以当给 GestureDetector 使用 ValueKey 去实现复用更新时,Container 也能被自动更新。


优化效果


优化前:



优化后:



可以看到,平均构建时间 由 5.5ms 减少到 1.6ms,优化效果还是很明显的。


优势


大幅度减少 Widget的平均构建时间。


缺点



  • 过多使用 ValueKey 会让你的代码变得更冗余。

  • 如果你的根 Widget 是 MaterialApp 时,则需要使用 GlobalKey,但当你去重复使用 GlobalKey 时可能会导致一些错误,所以一定要避免滥用 Key。


注意📢:在大部分场景下,Flutter 的性能都是足够的,不需要这么细致的优化,只有当产生了视觉上的问题,例如卡顿时才需要去分析优化。


四、启动速度优化


1、Flutter 引擎预加载


使用它可以达到页面秒开的一个效果,具体实现为:


在 HIFlutterCacheManager 类中定义一个 preLoad 方法,使用 Looper.myQueue().addIdleHandler 添加一个 idelHandler,当 CPU 空闲时会回调 queueIdle 方法,在这个方法里,你就可以去初始化 FlutterEngine,并把它缓存到集合中。


预加载完成之后,你就可以通过 HIFlutterCacheManager 类的 getCachedFlutterEngine 方法从集合中获取到缓存好的引擎。


2、Dart VM 预热


对于 Native + Flutter 的混合场景,如果不想使用引擎预加载的方式,那么要提升 Flutter 的启动速度也可以通 过Dart VM 预热来完成,这种方式会提升一定的 Flutter 引擎加载速度,但整体对启动速度的提升没有预加载引擎提升的那么多。



无论是引擎预加载还是 Dart VM 预热都是有一定的内存成本的,如果 App 内存压力不大,并且预判用户接下来会访问 Flutter 业务,那么使用这个优化就能带来很好的价值;反之,则可能造成资源浪费,意义不大。


五、内存优化


1、const 实例化


优势


**const 对象只会创建一个编译时的常量值。在代码被加载进 Dart Vm 时,在编译时会存储在一个特殊的查询表里,由于 flutter 采用了 AoT 编译,const + values 的方式会提供一些小的性能优势。**例如:const Color() 仅仅只分配一次内存给当前实例。


应用场景


Color()、GlobayKey() 等等。


2、识别出消耗多余内存的图片


Flutter Inspector:点击 “Invert Oversized Images”,它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在 App 页面中找到它。



针对这些图片,你可以指定 cacheWidth 和 cacheHeight 为展示大小,这样可以让 flutter 引擎以指定大小解析图片,减少内存消耗。


3、针对 ListView item 中有 image 的情况来优化内存


ListView 不能够杀死那些在屏幕可视范围之外的那些 item,如果 item 使用了高分辨率的图片,那么它将会消耗非常多的内存。


换言之,ListView 在默认情况下会在整个滑动/不滑动的过程中让子 Widget 保持活动状态,这一点是通过 AutomaticKeepAlive 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,以使被包裹的子 Widget 保持活跃。


其次,如果用户向后滚动,则不会再次重新绘制子 Widget,这一点是通过 RepaintBoundaries 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,它会让被包裹的子 Widget 仅仅绘制一次,以此获得更高的性能。


但,这样的问题在于,如果加载大量的图片,则会消耗大量的内存,最终可能使 App 崩溃。


解决方案


通过将这两个选项置为 false 来禁用它们,这样不可见的子元素就会被自动处理和 GC。


ListView.builder(
...
addAutomaticKeepAlives: false (true by default)
addRepaintBoundaries: false (true by default)
);

由于重新绘制子元素和管理状态等操作会占用更多的 CPU 和 GPU 资源,但是它能够解决你 App 的内存问题,并且会得到一个高性能的视图列表。


六、包体积优化


1、图片优化


对图片压缩或使用在线的网络图片。


2、移除冗余的二三库


随着业务的增加,项目中会引入越来越多的二三方库,其中有不少是功能重复的,甚至是已经不再使用的。移除不再使用的和将相同功能的库进行合并可以进一步减少包体积。


3、启用代码缩减和资源缩减


打开 minifyEnabled 和 shrinkResources,构建出来的 release 包会减少 10% 左右的大小,甚至更多。


4、构建单 ABI 架构的包


目前手机市场上,x86 / x86_64/armeabi/mips / mips6 的占有量很少,arm64-v8a 作为最新一代架构,是目前的主流,而 armeabi-v7a 只存在少部分的老旧手机中。


所以,为了进一步优化包大小,你可以构建出单一架构的安装包,在 Flutter 中可以通过以下方式来构建出单一架构的安装包


cd 
flutter build apk --split-per-abi

如果想进一步压缩包体积可将 so 进行动态下发,将 so 放在远端进行动态加载,不仅能进一步减少包体积也可以实现代码的热修复和动态加载。


七、总结


在本篇文章中,我主要从以下 六个方面 讲解了 Flutter 性能优化相关的知识:


1)、检测手段:Flutter Inspector、性能图层、Raster 和 UI 线程问题的定位
使用 checkerboardOffscreenLayers 检查多视图叠加的视图渲染 、使用 checkerboardRasterCacheImages 检查缓存的图像。
2)、关键优化指标:包括页面异常率、页面帧率、页面加载时长。
3)、布局加载优化:十大常规优化、优化光栅线程、用 key 加速 Flutter 的性能。
4)、启动速度优化:引擎预加载和 Dart VM 预热。
5)、内存优化:const 实例化、识别出消耗多余内存的图片、针对 ListView item 中有 image 的情况来优化内存。
6)、包体积优化:图片优化、移除冗余的二三库、启用代码缩减和资源缩减、构建单 ABI 架构的包。


在近一年实践 Flutter 的过程中,越发发现一个人真正应该具备的核心能力应该是你的思考能力。


思考能力,包括 结构化思考/系统性思考/迁移思考/层级思考/逆向思考/多元思考 等,使用这些思考能力分析问题时能快速地把握住问题的本质,在本质上做功夫,才是王道,才是真的 yyds。


作者:jsonchao
链接:https://juejin.cn/post/7066954522655981581
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »