有时候我们需要将拍摄或录制的视频文件转成Gif图。

比如我在写博客时就需要将录屏的视频文件转成Gif动图以便在页面中直接展示。

本文记录一下视频转高质量Gif图的命令。

生成Gif的FFmpeg的命令

  1. 使用ffmpeg生成调色板文件

这个命令是使用ffmpeg生成一个高质量的调色板图片(palette.png),调色板可以帮我们生成色彩高还原度的Gif:

1
ffmpeg -y -i input.mp4 -vf fps=30,scale=320:-1:flags=lanczos,palettegen palette.png
  1. 结合调色板生成Gif动图
1
ffmpeg -y -i input.mp4 -i palette.png -filter_complex "fps=30,scale=320:-1:flags=lanczos[x];[x][1:v]paletteuse" foo.gif

简化成一个函数,供系统调用

由于我使用的是Mac系统,所以我可以在当前用户根目录的.zprofile中新建一个函数:

1
2
3
4
5
function makegif {
ffmpeg -y -i $1 -vf fps=30,scale=300:-1:flags=lanczos,palettegen palette.png
ffmpeg -y -i $1 -i palette.png -filter_complex "fps=30,scale=300:-1:flags=lanczos[x];[x][1:v]paletteuse" $1.gif
rm palette.png
}

这样,每次需要生成Gif图的时候,只需要在终端中执行命令即可,例如:

1
makegif 1589167714045893.mp4

-c300

之前在做Android开发时,被要求实现点击输入框外的空白区域时,隐藏输入法。相对于iOS,Android端实现起来略显复杂,需要拦截触摸事件自行处理。

好在现在有了Flutter,可以很方便地解决这个问题。由于使用Flutter写出的代码在iOS/Android端的行为几乎一致,也不用担心被要求Android端所有的操作与行为必须跟iOS一模一样了。

获取点击事件并隐藏输入法

实现起来非常简单,这也是Flutter具有优势的地方。

要侦测用户点击了输入框之外的区域,我们需要在页面内容的Widget外增加一个GestureDetector,当用户点击到空白区域时,我们将输入框的焦点取消掉,相应地输入法也会隐藏掉。

1
2
3
4
5
6
7
8
9
10
11
body: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (mobileFocusNode.hasFocus) {
mobileFocusNode.unfocus();
} else if (passwordFocusNode.hasFocus) {
passwordFocusNode.unfocus();
}
},
child: _buildLoginContent(context),
),

-c300

注意这里的

1
behavior: HitTestBehavior.translucent

如果不加这句,当用户点击空白区域时,如果该区域没有任何元素则不会触发onTap()事件。加上后则可以正常触发。

GestureDetector是Flutter检测手势事件的Widget,做过Android开发的朋友都知道,Android中关于手势事件传递和处理非常复杂。而GestureDetector非常“聪明”地帮我们处理了事件监听。

另外需要提醒一下,在dispose()方法中也需要隐藏掉输入法,否则跳转页面时偶尔输入法不能正常隐藏:

1
2
3
4
5
6
7
8
9
@override
void dispose() {
if (mobileFocusNode.hasFocus) {
mobileFocusNode.unfocus();
} else if (passwordFocusNode.hasFocus) {
passwordFocusNode.unfocus();
}
super.dispose();
}

代码可以点击这里在Github上下载。

在使用Flutter开发一款APP之前,通常我们需要考虑如何设计工程的状态管理架构;选择一种结构清晰、易于维护的方式对于APP开发来说就显得十分有必要。

本文我们就来介绍如何使用官方推荐的Provider来实现MVVM架构的状态管理。

什么是Flutter中的MVVM?

MVVM拆解来说就是三个部分:

  • Model
    数据模型。通常来说,Model中保存了相关业务的数据,比如说用户(User),它其中包含idnamepassword。它就是一个Model。
  • View
    视图。通俗讲就是展示给用户的界面及控件,比如Flutter中参与界面展示的Widget。为什么我们要强调参与界面展示的Widget呢?因为在Flutter中几乎所有的东西都可以理解为Widget。
  • ViewModel
    负责实现View与Model的交互。这个是最关键的部分,ViewModel将视图和数据模型进行解耦,并且负责他们之间的交互。简单讲就是所有的业务逻辑都由它负责,而不是将业务逻辑和View都糅合在一起。

如果您熟悉安卓开发,也可以参考我之前的文章使用DataBinding实现MVVM,了解一下安卓开发上的MVVM架构。

Flutter中的MVVM模式的几种方式

在不使用任何第三方包的时候,官方也提供了不错的选择,那就是StatefulWidget,当我们需要改变状态来刷新UI时,只需要调用setState()方法。

这种方法简单直接,而且也可以理解为一种MVVM模式,只不过View和Model仍然耦合在一起,ViewModel并没有承担起它应有的角色。随着我们的工程变得越来越大时,代码里的setState()就会变得越来越多,显得非常混乱,并且有时候会忘记调用setState(),导致浪费很多时间来定位问题。

官方早期也提供的一种状态管理模式叫做BLOC。这种方式依赖于第三方包rxDart,以流(Stream)的方式很好地解决了setState()的问题。但是这种学习难度较大,对Flutter的新手并不友好。后来出现了一种第三方库Provider,这是一种先进的状态管理和依赖注入的工具,并且易于学习和理解,所以目前官方也推荐首选Provider

本文我们也是主要介绍如何使用Provider来实现MVVM模式。

初始化工程

为了专注讲解如何使用MVVM架构,这里就不从创建工程开始讲了。这里我创建了一个初始工程,从我现有的APP中抽出了一个登录页面,去掉了一些不必要的代码。可以直接点击在这里下载。这个初始工程中包含了以下内容:

  • 页面路由设置
  • 用户登录页面
  • 一个空的首页
  • API类用来模拟网络请求
  • 模拟发送验证码和登录请求
  • 一个简单的用户Model

登录页的界面如图:

-c300

加入Provider库

我们引入写此文最新Provider版本的依赖:

1
2
dependencies:
provider: ^4.1.0

由于APP中有可能需要使用到多个Provider以提供不同的功能,如果我们使用老版本的Provider时可能需要这样写:

1
2
3
4
5
6
7
8
9
10
Provider<Something>(
create: (_) => Something(),
child: Provider<SomethingElse>(
create: (_) => SomethingElse(),
child: Provider<AnotherThing>(
create: (_) => AnotherThing(),
child: someWidget,
),
),
),

好在新版本中我们有了MultiProvider,可以将多个Provider集合在一起,我们不必再写如此多的层级了。只需这样写,传入provider列表即可:

1
2
3
4
MultiProvider(
providers: providers,
child: someWidget,
)

创建Provider服务配置列表

我们创建dart文件provider_setup.dart,然后将APP中所需要的服务配置好。如果有了解服务端Spring Boot的朋友,可以对比其中的Bean的注入理解一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:flutter_provider_mvvm/api.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';

List<SingleChildWidget> providers = [
...independentServices,
...dependentServices,
];

List<SingleChildWidget> independentServices = [
Provider(create: (_) => Api()),
];

List<SingleChildWidget> dependentServices = [
//这里使用ProxyProvider来定义需要依赖其他Provider的服务
];

这里我们将APP中需要的服务都定义到这里。如果一个Provider依赖另一个Provider,我们可以使用ProxyProvider。这里我们为了保持简单,只定义了网络请求服务Api()

将Provider应用于整个APP

由于我们整个APP都需要使用Provider进行状态管理,所以我们需要在main.dart中将整个APP包裹在MultiProvider中,并且将上面创建的所有Provider列表传入到参数providers中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: providers,
child: MaterialApp(
title: 'Flutter MVVM',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: RoutePaths.LOGIN,
onGenerateRoute: Router.generateRoute,
),
);
}
}

使用ViewModel进行状态管理

既然我们在MVVM模式模式下进行开发APP,那么ViewModel是必不可少的。也就是当状态属性变化时,我们需要UI(也就是View层)进行相应的更改。

Provider中有ChangeNotifierProvider可以帮助我们监听是否状态发生了变化,它的child参数是一个Consumer可以帮我们来消费状态的变化。通俗来讲就是在这里调用Widget的build方法来进行UI刷新。

那在哪里去触发状态变化的通知呢?答案就是使用ChangeNotifier,当调用其中的notifyListeners()方法时,就可以通知监听它的ChangeNotifierProvider进行刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(
child: Consumer<T>(
//Widget的builder方法与child
builder: widget.builder,
child: widget.child,
),
create: (BuildContext context) {
//这里是我们的ViewModel,一个ChangeNotifier
return model;
},
);
}

现在我们的生产者消费者都有了,可以完善我们的MVVM模式了。

我们先创建一个ViewModel的基类,继承自ChangeNotifier,这样我们可以将一些公共的属性加入到里面,比如api,页面是否正在加载等:

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

Api api;
bool disposed = false;

BaseModel({@required Api api}) : api = api;

ViewState _state = ViewState.Idle;

ViewState get state => _state;

void setState(ViewState viewState) {
_state = viewState;
notifyListeners();
}

@override
void dispose() {
super.dispose();
disposed = true;
}

@override
void notifyListeners() {
if (!disposed) {
super.notifyListeners();
}
}
}

这里的ViewState是一个页面状态的枚举,其中标识了页面是否处于加载或者空闲的状态,UI也可以根据这个状态来对应展示。

1
enum ViewState { Idle, Busy }

ChangeNotifier中提供了销毁方法dispose(),我们可以在这个方法里标记页面是否已经被销毁。如果被销毁的话,我们不再通知页面进行刷新。

支持ChangeNotifierProvider的Widget基类

我们不可能在每一个Widget都使用ChangeNotifierProvider来包裹一下,所以这里我们需要一个Widget基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class BaseView<T extends BaseModel> extends StatefulWidget {
final Widget Function(BuildContext context, T model, Widget child) builder;

final T model;
final Widget child;
final Function(T) onModelReady;

BaseView({Key key, this.model, this.builder, this.child, this.onModelReady})
: super(key: key);

@override
_BaseViewState<T> createState() => _BaseViewState<T>();
}

class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
T model;

@override
void initState() {
model = widget.model;
if (widget.onModelReady != null) {
widget.onModelReady(model);
}
super.initState();
}

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(
child: Consumer<T>(
builder: widget.builder,
child: widget.child,
),
create: (BuildContext context) {
return model;
},
);
}
}

为什么这里我们需要使用StatefulWidget呢?因为我们需要在initState()在所有的子类中给出初始化的机会。

在所有需要应用MVVM模式的Widget都可以继承这个基类,传入ChangeNotifierProvider所需要的参数,其中包括viewModel,builder,child,还有初始化时的回调方法onModelReady()。

基础工作做完,这样我们可以在我们的APP中应用MVVM模式了。

LoginViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class LoginViewModel extends BaseModel {

LoginViewModel({@required Api api}) : super(api: api);

Timer _timer;
int _countdownTime = 0;

Future<void> sendSms(String mobile) async {
await api.sensSms(mobile);
}

Future<bool> login(String mobile, String sms) async {
return await api.login(mobile, sms) != null;
}

void startCountdown() {
_countdownTime = 60;
if (_timer == null) {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
countdownTime--;
if (countdownTime == 0) {
cancelTimer();
}
});
}
}

void cancelTimer() {
if (_timer != null) {
_timer.cancel();
_timer = null;
}
}

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

int get countdownTime => _countdownTime;

set countdownTime(int value) {
_countdownTime = value;
notifyListeners();
}

}

修改登录页面

下面是我们修改后的登录页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class _LoginViewState extends State<LoginView> {
final mobileTextController = TextEditingController();
final smsTextController = TextEditingController();

@override
Widget build(BuildContext context) {
return BaseView<LoginViewModel>(
model: LoginViewModel(api: Provider.of(context)),
onModelReady: (model) {},
builder: (context, model, child) {
return Scaffold(
backgroundColor: Color(0xFFF5F5F5),
appBar: AppBar(
title: Text('登录/注册'),
),
body: Builder(
builder: (context) => _buildLoginContent(context, model),
));
},
);
}
...

由于我们把应用Provider的细节封装成了一个Widget:BaseView,所以无论应用到哪个页面我们都可以很方便的使用,即使将现在的APP重构为使用Provider的MVVM架构,也不需要很多工作量。

下面我们来看这个页面中比较关键的几个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GestureDetector(
onTap: () async {
if (model.countdownTime == 0) {
await model.sendSms(mobileTextController.text);
model.startCountdown();
}
},
child: Text(
model.countdownTime > 0
? '${model.countdownTime}秒后重新发送'
: '请输入短信验证码',
style: TextStyle(
fontSize: 14,
color: model.countdownTime > 0
? Color(0xFFa5a5a5)
: Color(0xFF191919),
),
),
)

这里是发送短信验证码的按钮。点击文字时请求API,并开始倒计时,每隔1秒model中的countdownTime会自减1。因为我们在set方法中调用了notifyListeners(),所以它的值改变时,UI也会相应地进行刷新,无须其他操作,也无须在UI中做setState(),或者逻辑判断。UI唯一需要关心的就是model中的属性。

我们将所有的业务逻辑都放在model里面后会使页面清晰而简单。

再比如我们点击登录时的操作,登录成功后跳转到APP的首页:

1
2
3
4
5
6
onPressed: () async {
if (await model.login(
mobileTextController.text, smsTextController.text)) {
Navigator.of(context).pushNamed(RoutePaths.HOME);
}
}

-c300

将Provider应用到APP首页

从上面登录页面的示例中,我们可以看到如何使用Provider实现MVVM模式。

下面我们来看登陆成功后跳转到的APP首页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class HomeView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BaseView<HomeViewModel>(
onModelReady: (model) async {
model.loadData();
},
model: HomeViewModel(api: Provider.of(context)),
builder: (context, model, child) => Scaffold(
backgroundColor: Color(0xFFF5F5F5),
appBar: AppBar(
title: Text('首页'),
),
body: _buildBody(context, model),
),
);
}

Widget _buildBody(BuildContext context, HomeViewModel model) {
return Container(
child: model.state == ViewState.Busy
? Center(
child: CircularProgressIndicator(),
)
: Center(
child: Text('APP首页'),
));
}
}

进入到首页后,在onModelReady的回调方法中我们首先请求Api数据,请求数据时我们会设置页面的状态,标记为繁忙;请求完成时,标记为空闲。UI的展示也根据这个状态展示加载框还是首页内容。

下面是HomeViewModel中网络请求的部分:

1
2
3
4
5
6
7
Future<void> loadData() async {
print('加载首页数据...');
setState(ViewState.Busy);
await Future.delayed(Duration(seconds: 2));
setState(ViewState.Idle);
print('加载首页数据完成');
}

-c300

-c300

复杂业务逻辑的细粒化状态管理

对于大部分的业务场景来说,一个页面对应ViewModel。这样做是没问题的,因为许多页面并没有那么复杂。

但是对于类似淘宝、京东首页这种量级的APP来说,整个页面对应一个ViewModel显然是不行的,否则View和ViewModel中的代码会非常庞大,难以维护;并且我们希望某一个功能模块的数据变化只进行局部刷新,而不是整个页面刷新。

那么此时该使用什么办法来解决呢?

那就是将不同的功能分别写在不同的BaseView和ViewModel中,然后再组合使用他们。

因为BaseView也是一个Widget,根据我们的架构,它对应一个ViewModel。这种拆分组合不同功能模块的方式非常容易理解,也比较容易实现;更重要的一点它非常有利于代码重用和扩展,其他同事阅读代码也比较清晰。

最后记住一点:如非必要不要化简为繁,过度封装。

我也已经用这种MVVM架构模式完成了数个基于Flutter的APP,也已经发布到了App Store和安卓市场。这种基于Provider的MVVM架构模式极大地提升了Flutter的开发体验,并且也易于维护和扩展。

毫无疑问Flutter会是将来的发展趋势,后续我也会继续探索分享Flutter的开发心得和其他的互联网技术,欢迎关注我刚开通的公众号”程序员磊哥“,谢谢~

这个工程的完整的代码我也上传到了Github上面,可以点击这里下载查看

今早正在愉快地使用Flutter开发客户端的时候,突然发现所有接口都使用不了了,觉得很奇怪,服务器上什么都没有动怎么突然不行了呢?

于是登录CentOS服务器查看Spring Boot服务的日志,发现了以下异常:

1
2
3
4
org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is java.lang.IllegalStateException: Unable to create the directory [/tmp/tomcat.1964230947136987004.8000] to use as the base directory
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:157)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142)

哦,原来是Tomcat无法创建它的临时工作目录了。

于是就执行

1
df -h

查了一下服务器的硬盘信息:

1
2
3
4
5
6
7
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1 99G 94G 0 100% /
devtmpfs 7.8G 0 7.8G 0% /dev
tmpfs 7.8G 0 7.8G 0% /dev/shm
tmpfs 7.8G 532K 7.8G 1% /run
tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
tmpfs 1.6G 0 1.6G 0% /run/user/1000

果然!根目录下使用率100%,硬盘被占满了!这是个严重的问题,不过该怎么排查哪些文件占用的空间呢?查了一番资料,发现使用下面命令可以一层层的跟踪排查:

1
du -x -m --max-depth 1 /

先从根目录开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@cscc log]# du -x -h -m --max-depth 1 / 
5 /tmp
4104 /root
668 /opt
1 /media
55960 /var
708 /WWW
1 /srv
38 /etc
2544 /home
27338 /data
1 /mnt
214 /boot
4115 /usr
1 /lost+found
95687 /

OK,发现/var目录占用最多,于是就怀疑是日志文件的问题,因为系统日志都在/var目录下。接着进入/var/log目录,执行命令确定一下:

1
ll -h

果然,全部都是日志文件占用的空间:

1
2
3
4
5
-rw-------  1 root    root            6.8G Oct 10 11:22 messages
-rw------- 1 root root 7.9G Sep 15 03:33 messages-20190915
-rw------- 1 root root 9.9G Sep 22 03:37 messages-20190922
-rw------- 1 root root 11G Sep 29 03:11 messages-20190929
-rw------- 1 root root 11G Oct 6 03:49 messages-20191006

也可以执行命令查看目录所占的空间:

1
du -sh *

服务器一共就100G的存储空间,却被这些日志文件占去了一大半,查看了一下内容,发现大部分都是服务器上的定时任务的信息,因为定时任务非常频繁,而且打印的信息量比较大,看着这块要优化一下了。

先把这些日志删除一下,保证服务正常:

1
rm messages*

删除后,启动服务器,一切终于正常了!

本文原创地址为:https://www.examplecode.cn/2019/10/10/centos-disk-usage-100/
转载请注明出处。

在客户端开发的过程中,JSON的序列化与反序列化是一个常见的操作,通常我们的API服务端会返回JSON格式的数据。此时我们就需要解析JSON并转化为客户端中的对象。在客户端开发的初期,我们需要人工解析每一个字段,类似于很多obj.id = json[‘id’],obj.name = json[‘name’]…这样的代码。

遥想当年笔者刚接触Android开发的时候,刚开始并没有使用Gson,Jackson等自动解析工具,所以就出现了全体成员编写解析JSON代码的壮观景象!

手动解析JSON的缺点是:一、效率低下,我们浪费了大量的时间在写一些枯燥无比的代码,这样简直就是浪费生命。二、容易出错,我们谁都不能保证我们在敲代码的过程中不会敲错一个字母,于是就会出现解析异常,程序崩溃等BUG。

在Flutter发布的初期,并没有一个成熟的方式解析JSON,好在目前我们有了’json_serializable‘这个插件。通过这个插件,我们可以生成所有序列化/反序列化的方法。本文我们就来介绍如何生成解析JSON的方法。

一、添加插件依赖

在我们Flutter工程的pubspec.yaml中添加如下:

1
2
3
4
5
6
7
8
9
# 其他的依赖也添加到这里
dependencies:
analyzer: 0.38.2
json_annotation: ^3.0.0

# 这里是开发期间使用的插件
dev_dependencies:
build_runner: ^1.6.7
json_serializable: ^3.2.2

添加完后在工程根目录下执行’flutter pub get’,或者在Android Studio中点击’Packages get’按钮,下载插件。

注意这里指定版本的依赖’analyzer: 0.38.2’,加入这个是因为在json_annotation的最新3.x版本存在一些问题,会导致生成解析JSON错误,如果后续解决了这个问题,可以把这个依赖删除。

二、定义Model

下面我们定义一个Model,这里我们以User举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'package:json_annotation/json_annotation.dart';
import 'package:x_marriage/core/models/region.dart';

part 'user.g.dart';

@JsonSerializable(explicitToJson: true)
class User {

int id;
String name;
Region address;

@JsonKey(name: 'login_time')
DateTime loginTime;

User();

factory User.fromJson(Map<String, dynamic> json) =>
_$UserFromJson(json);

Map<String, dynamic> toJson() => _$UserToJson(this);
}

定义完后,我们会发现AS会提示错误:

原因很简单,因为文件”user.g.dart”还有下面的方法我们还没有生成。不过不用担心,下面我们执行生成命令后,这些错误都会消失。

注意这里:

1
@JsonSerializable(explicitToJson: true)

这个注解的意思是在我们User中有嵌套对象时,调用User的toJson()方法时,也会调用嵌套对象的toJson()方法。比如上面的User中的address。

还有一点我们需要注意:

1
2
@JsonKey(name: 'login_time')
DateTime loginTime;

当API返回的字段与我们的字段不一致时,比如客户端使用驼峰的命名方式,服务器采用下划线的命名方式,我们就可以使用‘@JsonKey’来指定解析特定字段。

三、执行命令,生成解析代码

在工程的根目录下执行:

1
flutter pub run build_runner build

执行后我们会看到日志信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[INFO] Generating build script...
[INFO] Generating build script completed, took 481ms

[INFO] Initializing inputs
[INFO] Reading cached asset graph...
[INFO] Reading cached asset graph completed, took 140ms

[INFO] Checking for updates since last build...
[INFO] Checking for updates since last build completed, took 1.1s

[INFO] Running build...
[INFO] 1.1s elapsed, 0/16 actions completed.
[INFO] 2.3s elapsed, 0/16 actions completed.
[INFO] 3.4s elapsed, 0/16 actions completed.
[INFO] 4.5s elapsed, 0/16 actions completed.
[INFO] 10.4s elapsed, 1/16 actions completed.
[INFO] 11.6s elapsed, 3/18 actions completed.
[INFO] 12.8s elapsed, 17/29 actions completed.
[INFO] 13.8s elapsed, 53/65 actions completed.
[INFO] 15.0s elapsed, 65/79 actions completed.
[INFO] 16.0s elapsed, 87/97 actions completed.
[INFO] Running build completed, took 16.4s

[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 129ms

[INFO] Succeeded after 16.5s with 24 outputs (118 actions)

执行完后我们会发现工程中多了刚才User中缺少的文件:

在这个文件中,之前我们报错的方法’$UserFromJson’和 ‘$UserToJson’也都生成好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

User _$UserFromJson(Map<String, dynamic> json) {
return User()
..id = json['id'] as int
..name = json['name'] as String
..address = json['address'] == null
? null
: Region.fromJson(json['address'] as Map<String, dynamic>)
..loginTime = json['login_time'] == null
? null
: DateTime.parse(json['login_time'] as String);
}

Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'address': instance.address?.toJson(),
'login_time': instance.loginTime?.toIso8601String(),
};

这样解析JSON的代码就生成完毕了!Happy Coding!

其他一些问题的探讨

模板类

简单来说,生成自动解析JSON的代码就两步:一、定义Model。二、执行生成命令。非常简单。

但是我们发现定义Model时略显啰嗦。需要定义part …,和这两个方法’$UserFromJson’和 ‘$UserToJson’,相信没人喜欢每次都写这样的代码!笔者的做法是定义一个模板文件,每次需要新建Model类时就拷贝一下,改一下文件名和类名,然后只需要定义其属性即可。

代码也放上,如果有人喜欢,可以也定义这样一个类在工程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'package:json_annotation/json_annotation.dart';

part 'template_model.g.dart';

@JsonSerializable(explicitToJson: true)
class TemplateModel {

String value;

TemplateModel();

factory TemplateModel.fromJson(Map<String, dynamic> json) =>
_$TemplateModelFromJson(json);

Map<String, dynamic> toJson() => _$TemplateModelToJson(this);
}

为什么需要生成代码?

比如其他的一些序列化/反序列化的库,比如Gson、Jackson等,我们不需要生成代码,直接可以进行解析,而在Flutter中,我们却需要用生成代码的方式,为什么呢?

因为Flutter禁用了Dart中的反射机制。上面的库中由于使用了反射机制,所以在运行时可以动态地解析Json字段并赋值。

为什么Flutter要禁用反射机制呢?因为在构建阶段Flutter可以知道哪些代码被使用了,哪些代码没有被使用,在构建时可以删除掉没用使用的代码,从而减小安装包的体积。如果使用了反射,则很难去发现那些未被使用的代码。

本文原创地址为:https://www.examplecode.cn/2019/10/06/flutter-json-serializable/
转载请注明出处。

近日,公司的前端设计的同事问我如果在手机浏览器中访问他设计的界面。顿时脑中浮现出了无数种方法:Nginx,Tomcat,Spring Boot,NodeJS,Apache,Django,Flask,Tornado…

作为公司的技术扛把子,总不能跟设计说这些东西吧,技术的目的就是让生活变得简单,不是吗?

于是经过研究,发现一种使用Python可以快速搭建HTTP服务器的方法。

好在公司的设计使用的Mac电脑,Python是内置的,如果使用Windows也不用担心,点击这里去官网下载安装一下即可。

下面我们就来介绍一下:

首先进入到目标目录,比如:cd ~/Download/design/,然后根据Python的版本不同执行以下命令:

Python 2.x

1
2
3
4
5
//使用默认端口号8000
python -m SimpleHTTPServer

//自定义端口号
python -m SimpleHTTPServer 3333

Python 3.x

1
2
3
4
5
6
7
8
//使用默认端口号8000
python3 -m http.server

//自定义端口号
python3 -m http.server 3333

//限制访问IP
python3 -m http.server --bind 127.0.0.1

刷新页面

如果页面更改后没有刷新,在命令行执行刷新命令

1
refresh

这样仅需一行代码,即可快速搭建一个Http服务器。

本机访问

1
http://127.0.0.1:端口号

其他设备(手机)访问

如果其他设备需要访问这个Http服务器,首先要确保这些设备与你的机器在同一个局域网内,并且互通。

然后找到本机的IP进行访问:

1
http://本机IP:端口号

本文原创地址为:https://www.examplecode.cn/2019/04/26/python-http-server/
转载请注明出处。

在移动开发的过程中我们会不可避免地使用到数据的持久化。当我们需要存储一些简单的数据时,我们会使用到NSUserDefaults (iOS) 或 SharedPreferences (Android),本文我们就来介绍这个方便的Flutter插件shared_preferences来实现本地存储的需求。

添加依赖

要使用这个插件,首先在工程的pubspec.yaml文件中加入对这个插件的依赖:

1
2
dependencies:
shared_preferences: ^0.5.2

下载插件

1
flutter packages get

使用示例

1
2
3
4
5
6
_incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
print('Pressed $counter times.');
await prefs.setInt('counter', counter);
}

注意:在使用这个插件时,我们需要进行异步调用,也就是需要使用async/await。

工具类

我这里也封装了一个工具类pref_util.dart,需要的话可以直接拷贝到工程中,可以少写一些枯燥的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import 'package:shared_preferences/shared_preferences.dart';

setPrefString(String key, String value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString(key, value);
}

setPrefInt(String key, int value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setInt(key, value);
}

setPrefDouble(String key, double value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setDouble(key, value);
}

setPrefStringList(String key, List<String> value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setStringList(key, value);
}

setPrefBool(String key, bool value) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool(key, value);
}

getPrefString(String key, {String defaultValue = ''}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString(key) ?? defaultValue;
}

getPrefInt(String key, {int defaultValue = 0}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getInt(key) ?? defaultValue;
}

getPrefDouble(String key, {double defaultValue = 0}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getDouble(key) ?? defaultValue;
}

getPrefStringList(String key, {List<String> defaultValue = const []}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getStringList(key) ?? defaultValue;
}

getPrefBool(String key, {bool defaultValue = false}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getBool(key) ?? defaultValue;
}

removePref(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove(key);
}

prefContains(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.get(key) != null;
}

本文原创地址为:https://www.examplecode.cn/2019/04/25/flutter-shared-preference/
转载请注明出处。

我的博客中关于Flutter的所有文章可以点击这里找到,欢迎关注!

如果我们的APP是国际化的产品,通常我们需要获取当前设备的语言设置信息。本文我们就来介绍如果正确获取到当前的语言设置。

获取当前的语言设置

正确的获取方式应该是在我们APP创建一个监听的回掉函数,用来监听当前设备语言设置的变化。

注册这个监听方法后在APP第一次启动的时候会被调用。

注册方式:

1
2
3
4
5
6
7
8
9
10
11
12
class MyApp extends StatelessWidget {
// 这个Widget是我们APP的根Widget
@override
Widget build(BuildContext context) {
return MaterialApp(
home: StartPage(),
localeResolutionCallback: (deviceLocale, supportedLocales) {
print('deviceLocale: $deviceLocale');
},
);
}
}

在监听到当前设备的最新语言设置后,我们可以将这个值保存在本地存储中,比如(Shared Preference)中,以便在其他地方使用。

关于如何使用Shared Preference,可以点击这里参考我的这篇文章

本文原创地址为:https://www.examplecode.cn/2019/04/15/flutter-device-language-info/
转载请注明出处。

我的博客中关于Flutter的所有文章可以点击这里找到,欢迎关注!

是否联网及网络类型

通过使用插件connectivity可以获取到当前设备是否联网,以及当前的网络类型是WIFI还是移动网络。

添加依赖

要使用这个插件,首先在工程的pubspec.yaml文件中加入对这个插件的依赖:

1
2
dependencies:
connectivity: ^0.4.2

下载插件

1
flutter packages get

判断是否联网

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

Future<bool> isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}

获取网络类型

1
2
3
4
5
6
7
8
import 'package:connectivity/connectivity.dart';

var connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.mobile) {
// 网络类型为移动网络
} else if (connectivityResult == ConnectivityResult.wifi) {
// 网络类型为WIFI
}

总结

通过这个插件可以获取到当前网络是WIFI或者移动网络,但不能获取到具体的移动网络类型,如2G/3G/4G/4G+等。

本文原创地址为:https://www.examplecode.cn/2019/04/13/flutter-network-type/
转载请注明出处。

我的博客中关于Flutter的所有文章可以点击这里找到,欢迎关注!

获取APP包的信息

通过使用插件package_info可以获取到当前APP的包名,版本名,版本号等信息。

添加依赖

要使用这个插件,首先在工程的pubspec.yaml文件中加入对这个插件的依赖:

1
2
dependencies:
package_info: ^0.4.0+2

下载插件

1
flutter packages get

获取APP的信息:

1
2
3
4
5
6
7
8
9
10
11
12
import 'package:package_info/package_info.dart';

PackageInfo packageInfo = await PackageInfo.fromPlatform();

//APP名称
String appName = packageInfo.appName;
//包名
String packageName = packageInfo.packageName;
//版本名
String version = packageInfo.version;
//版本号
String buildNumber = packageInfo.buildNumber;

获取设备相关的其他信息

关于如何获取屏幕宽度高度和分辨率等信息,可以参考这篇文章

获取设备信息,可以参考这篇文章

本文原创地址为:https://www.examplecode.cn/2019/04/12/flutter-package-info/
转载请注明出处。

我的博客中关于Flutter的所有文章可以点击这里找到,欢迎关注!