学起来Flutter将支持桌面应用开发

英文原文 Flutter团队正在扩展Flutter,支持创建macOS、Windows和Linux应用程序。从长远来看,这项工作将提供一个完全继承的解决方案,flutter create,flutter run 和 flutter build 在桌面平台开发上的表现将和现在的移动平台开发中一样,但是目前这项工作还在进行中。 当前的状况下面提供了平台状况的高级概述。 详细信息请参阅 源码仓库 重要提示:Flutter桌面API仍处于早期阶段,如有更改,恕不另行通知。不会提供API或ABI的向后兼容性。Flutter更新之后,所有使用了Flutter的项目的代码都需要做更新并且重新编译。macOS系统这是最成熟的桌面平台(出于一些原因,它非常接近于我们已经支持的iOS)。 桌面版中以Flutter开头的类与iOS通用,所以应该基本稳定。以FLE开头的类仍处于早期阶段。 Windows系统当前的 Windows shell 只是 GLFW 占位符, 以便与前期实验. 未来它将被 Win32 或者 UWP shell 替代,因为Win32 或者 UWP shell 允许在Flutter应用程序中嵌入view-level。 预计,最终版本的shell APIs和当前实现的方式完全不同。 Linux和Windows一样,当前 Linux shell 只是 GLFW 占位符。我们想创建一个库,让开发可以任何部分嵌入Flutter,无论你使用GTK+, Qt, wxWidgets, Motif, 还是其他任意工具包。但是我们还没有确定一个好方法。 插件所有平台都支持编写插件(例如 flutter-desktop-embedding 这些插件),但是,目前依然很少有插件实际上具有桌面支持。 工具Flutter支持桌面的工具开发还在进行中。要使用任何桌面支持工具(例如用flutter devices列出主机)目前必须满足两点: 你不能使用稳定的Flutter channel。因为桌面支持还没有被认为是稳定的和适合生产环境的你必须设置ENABLE_FLUTTER_DESKTOP环境变量为true。这是为了避免在指定长期解决方案时影响现有的移动开发工作流程(参见:#30724。预构建Shell库默认情况下,桌面库未下载,可以通过运行运行flutter precache下载,根据你的你的操作系统带上参数 --linux,--macos或 --windows。 C++ WrapperWindows和Linux库提供C语言API。为了更容易使用他们,可以使用C++包装器,将其构建到应用程序,中以便与提供更高级的API调用。上面提到的flutter precache命令会将这个包装器的源码下载到与该库同目录下的cpp_client_wrapper文件夹中。 使用Shells由于目前没有桌面shell工具的支持,你需要自己写一个应用的运行工具,并且在库里链接,就像任何你使用的插件那样。这将需要做一些你熟悉所使用的桌面系统的原生开发。如果你在桌面系统系统开发方面没有经验,你需要等到flutter桌面开发工具支持可用。 所以,使用Shells请参阅你所使用的操作系统的库的头。将来会补充更多的文档。至于现在,可以参考flutter-desktop-embedding示例,也许会有启发。 另外,你的Flutter桌面应用程序还需要bundle Flutter assets(由flutter build bundle创建)。在Windows和Linux你将还会需要Flutter引擎的ICU数据。(在你的Flutter目录中下的bin/cache/artifacts/engine查找icudtl.dat) macOS 注意目前你必须在XIB中设置FLEView,而不是在代码中设置(以后会改)。如下: 拖入一个OpenGL视图修改类型为FLEView.选中Double Buffer选项. 如果你的视图没有被绘制出来,可能是因为忘记这个步骤.选中Hi-Res Backing支持选项. 如果在高DPI显示器上只显示部分程序,那么可能是因为忘记这个步骤。

April 28, 2019 · 1 min · jiezi

Flutter绘制弯曲虚线

去看原文:http://tryenough.com/flutter-curved-line效果 开始去看原文:http://tryenough.com/flutter-curved-line修改main.dart文件: import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Drawing Shapes", home: DrawingPage(), ); }}class DrawingPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Curved Line"), ), body: CustomPaint( painter: CurvePainter(), child: Container(), ), ); }}class CurvePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { } @override bool shouldRepaint(CustomPainter oldDelegate) { return false; }}去看原文:http://tryenough.com/flutter-curved-line接下来我们在CurvePainter中实现绘制: ...

April 28, 2019 · 1 min · jiezi

Flutter绘制虚线

欢迎去看原文:http://tryenough.com/flutter-dotline效果 实现方案方案一: 如果你用canvas画,可以参考这个库来绘制虚线: https://pub.dartlang.org/packages/path_drawing#-installing-tab- 欢迎去看原文:http://tryenough.com/flutter-dotline方案二: 定义分割线 class MySeparator extends StatelessWidget { final double height; final Color color; const MySeparator({this.height = 1, this.color = Colors.black}); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final boxWidth = constraints.constrainWidth(); final dashWidth = 10.0; final dashHeight = height; final dashCount = (boxWidth / (2 * dashWidth)).floor(); return Flex( children: List.generate(dashCount, (_) { return SizedBox( width: dashWidth, height: dashHeight, child: DecoratedBox( decoration: BoxDecoration(color: color), ), ); }), mainAxisAlignment: MainAxisAlignment.spaceBetween, direction: Axis.horizontal, ); }, ); }}使用 const MySeparator() ...

April 28, 2019 · 1 min · jiezi

Flutter简介

Flutter特点Google 出品使用Dart语言开发支持跨平台,高性能,使用自绘渲染引擎 特点详解 1.高性能 2.快速内存分配Flutter框架使用函数式流,这使得它在很大程度上依赖于底层的内存分配器。因此,拥有一个能够有效地处理琐碎任务的内存分配器将显得十分重要,在缺乏此功能的语言中,Flutter将无法有效地工作。当然Chrome V8的JavaScript引擎在内存分配上也已经做的很好,事实上Dart开发团队的很多成员都是来自Chrome团队的,所以在内存分配上Dart并不能作为超越JavaScript的优势,而对于Flutter来说,它需要这样的特性,而Dart也正好满足而已。 3.类型安全由于Dart是类型安全的语言,支持静态类型检测,所以可以在编译前发现一些类型的错误,并排除潜在问题,这一点对于前端开发者来说可能会更具有吸引力。与之不同的,JavaScript是一个弱类型语言,也因此前端社区出现了很多给JavaScript代码添加静态类型检测的扩展语言和工具,如:微软的TypeScript以及Facebook的Flow。相比之下,Dart本身就支持静态类型,这是它的一个重要优势 Flutter架构 Flutter Framework这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下: 底下两层(Foundation和Animation、Painting、Gestures)在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。Rendering层,这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)。Widgets层是Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道。 Flutter Engine这是一个纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。 总结Flutter作为一款跨平台,开源,具有良好分层的框架,在大前端越来越流行的趋势下,是很值得花时间学习一下的。加油!!!

April 27, 2019 · 1 min · jiezi

深入理解Flutter多线程

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/54da18ed1a9e Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。 事件队列和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。应用程序启动后,开始执行main函数并运行main isolate。 每个isolate包含一个事件循环以及两个事件队列,event loop事件循环,以及event queue和microtask queue事件队列,event和microtask队列有点类似iOS的source0和source1。 event queue:负责处理I/O事件、绘制事件、手势事件、接收其他isolate消息等外部事件。microtask queue:可以自己向isolate内部添加事件,事件的优先级比event queue高。 这两个队列也是有优先级的,当isolate开始执行后,会先处理microtask的事件,当microtask队列中没有事件后,才会处理event队列中的事件,并按照这个顺序反复执行。但需要注意的是,当执行microtask事件时,会阻塞event队列的事件执行,这样就会导致渲染、手势响应等event事件响应延时。为了保证渲染和手势响应,应该尽量将耗时操作放在event队列中。 async、await在异步调用中有三个关键词,async、await、Future,其中async和await需要一起使用。在Dart中可以通过async和await进行异步操作,async表示开启一个异步操作,也可以返回一个Future结果。如果没有返回值,则默认返回一个返回值为null的Future。 async、await本质上就是Dart对异步操作的一个语法糖,可以减少异步调用的嵌套调用,并且由async修饰后返回一个Future,外界可以以链式调用的方式调用。这个语法是JS的ES7标准中推出的,Dart的设计和JS相同。 下面封装了一个网络请求的异步操作,并且将请求后的Response类型的Future返回给外界,外界可以通过await调用这个请求,并获取返回数据。从代码中可以看到,即便直接返回一个字符串,Dart也会对其进行包装并成为一个Future。 Future<Response> dataReqeust() async { String requestURL = 'https://jsonplaceholder.typicode.com/posts'; Client client = Client(); Future<Response> response = client.get(requestURL); return response;}Future<String> loadData() async { Response response = await dataReqeust(); return response.body;}在代码示例中,执行到loadData方法时,会同步进入方法内部进行执行,当执行到await时就会停止async内部的执行,从而继续执行外面的代码。当await有返回后,会继续从await的位置继续执行。所以await的操作,不会影响后面代码的执行。 下面是一个代码示例,通过async开启一个异步操作,通过await等待请求或其他操作的执行,并接收返回值。当数据发生改变时,调用setState方法并更新数据源,Flutter会更新对应的Widget节点视图。 class _SampleAppPageState extends State<SampleAppPage> { List widgets = []; @override void initState() { super.initState(); loadData(); } loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }}FutureFuture就是延时操作的一个封装,可以将异步任务封装为Future对象。获取到Future对象后,最简单的方法就是用await修饰,并等待返回结果继续向下执行。正如上面async、await中讲到的,使用await修饰时需要配合async一起使用。 ...

April 26, 2019 · 2 min · jiezi

走近科学探究阿里闲鱼团队通过数据提升Flutter体验的真相

背景闲鱼客户端的flutter页面已经服务上亿级用户,这个时候Flutter页面的用户体验尤其重要,完善Flutter性能稳定性监控体系,可以及早发现线上性能问题,也可以作为用户体验提升的衡量标准。那么Flutter的性能到底如何?是否像官方宣传的那么丝滑?Native的性能指标是否可以用来检测Flutter页面?下面给大家分享我们在实践中总结出来的Flutter的性能稳定性监控方案。 目标过度的丢帧从视觉上会出现卡顿现象,体现在用户滑动操作不流畅;页面加载耗时过长容易中断操作流程;Flutter部分exception会导致发生异常代码后面的逻辑没有走到从而造成逻辑bug甚至白屏。这些问题很容易考验用户耐心,引起用户反感。 所以我们制定以下三个指标作为线上Flutter性能稳定性标准: 页面滑动流畅度页面加载耗时(首屏时长+可交互时长)Exception率最终目标是让这些数据指标驱动Flutter用户体验升级。 页面滑动流畅度我们先大概了解下屏幕渲染流程:CPU先把UI对象转变GPU可以识别的信息存储进displaylist列表,GPU执行绘图指令来执行displaylist,取出相应的图元信息,进行栅格化渲染,显示到屏幕上,这样一个循环的过程实现屏幕刷新。 闲鱼客户端采用的Native、Flutter混合技术方案,Native页面FPS监控采用集团高可用方案,Flutter页面是否可以直接采用这套方案监控? 普遍的FPS检测方案Android端采用的是Choreographer.FrameCallBack,IOS采用的是CADisplayLink注册的回调,原理是类似的,在每次发出Vsync信号,并且CPU开始计算的时候执行到对应的回调,这个时候表示屏幕开始一次刷新,计算固定时间内屏幕渲染次数来得到fps。(这种方式只能检测到CPU卡顿,对于GPU的卡顿是无法监控到的)。由于这两种方法都是在主线程做检测处理,而flutter的屏幕绘制是在UI TaskRunner中进行,真正的渲染操作是在GPU TaskRunner中,关于详细的Flutter线程问题可以参考闲鱼之前的文章:深入理解Flutter引擎线程模式。 这里我们得出结论:Native的FPS检测方法并不适用于Flutter。 Flutter官方给我们提供了 Performance Overlay (具体参考 Flutter performance profiling) 作为检测帧率工具,可否直接拿来用? 上图显示了Performance Overlay模式下的帧率统计,可以看到,Flutter分开计算GPU 和UI TaskRunner。UI Task Runner被Flutter Engine用于执行Dart root isolate代码,GPU Task Runner被用于执行设备GPU的相关调用。通过对flutter engine源码分析,UI frame time是执行window.onBeginFrame所花费的总时间。GPU frame time是处理CPU命令转换为GPU命令并发送给GPU所花费的时间。 这种方式只能在debug和profile模式下开启,没有办法作为线上版本的fps统计。但是我们可以通过这种方式获得启发,通过监听Flutter页面刷新回调方法handleBeginFrame()、handleDrawFrame()来计算实际FPS。 具体实现方式:注册WidgetsFlutterBinding监听页面刷新回调handleBeginFrame()、handleDrawFrame() handleBeginFrame: Called by the engine to prepare the framework to produce a new frame.handleDrawFrame: Called by the engine to produce a new frame.通过计算handleBeginFrame和handleDrawFrame之间的时间间隔计算帧率,主要流程如下图: 效果到这里,我们完成Flutter中页面帧率的统计,这种方式统计的是UI TaskRunner中的CPU操作耗时,GPU操作在Flutter引擎内部实现,要修改引擎来监控完整的渲染耗时,我们目前大部分的场景没有复杂到gpu卡顿,问题主要还是集中在CPU,所以说可以反应出大部分问题。从线上数据来看,release模式下Flutter的流畅度还是蛮不错的,ios的主要页面均值基本维持在50fps以上,android相对ios略低。这里需要注意的是帧率的均值fps在反复滑动过程中会有一个稀释效果,导致一些卡顿问题没有暴露出来,所以除了fps均值,需要综合掉帧范围、卡顿秒数、滑动时长等数据才能反应出页面流畅度情况。 页面加载时长集团内部高可用方案统计Native页面加载时长是通过容器初始化后开启定时器在容器layout的时候检查屏幕渲染程度,计算可见组件的屏幕覆盖率,满足条件水平>60%,垂直>80%以上认为满足页面填充程度,再检查主线程心跳判断是否加载完成 再来看看weex页面加载流程和统计数据的定义 ...

April 26, 2019 · 1 min · jiezi

Flutter-pageview切换指示器

欢迎去看原文:http://tryenough.com/flutter-indicator-scrollview效果 代码// Copyright 2017, the Flutter project authors. Please see the AUTHORS file// for details. All rights reserved. Use of this source code is governed by a// BSD-style license that can be found in the LICENSE file.import 'dart:math';import 'package:flutter/material.dart';void main() { runApp(new MyApp());}class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', home: new MyHomePage(), debugShowCheckedModeBanner: false, ); }}/// An indicator showing the currently selected page of a PageControllerclass DotsIndicator extends AnimatedWidget { DotsIndicator({ this.controller, this.itemCount, this.onPageSelected, this.color: Colors.white, }) : super(listenable: controller); /// The PageController that this DotsIndicator is representing. final PageController controller; /// The number of items managed by the PageController final int itemCount; /// Called when a dot is tapped final ValueChanged<int> onPageSelected; /// The color of the dots. /// /// Defaults to `Colors.white`. final Color color; // The base size of the dots static const double _kDotSize = 8.0; // The increase in the size of the selected dot static const double _kMaxZoom = 2.0; // The distance between the center of each dot static const double _kDotSpacing = 25.0; Widget _buildDot(int index) { double selectedness = Curves.easeOut.transform( max( 0.0, 1.0 - ((controller.page ?? controller.initialPage) - index).abs(), ), ); double zoom = 1.0 + (_kMaxZoom - 1.0) * selectedness; return new Container( width: _kDotSpacing, child: new Center( child: new Material( color: color, type: MaterialType.circle, child: new Container( width: _kDotSize * zoom, height: _kDotSize * zoom, child: new InkWell( onTap: () => onPageSelected(index), ), ), ), ), ); } Widget build(BuildContext context) { return new Row( mainAxisAlignment: MainAxisAlignment.center, children: new List<Widget>.generate(itemCount, _buildDot), ); }}class MyHomePage extends StatefulWidget { @override State createState() => new MyHomePageState();}class MyHomePageState extends State<MyHomePage> { final _controller = new PageController(); static const _kDuration = const Duration(milliseconds: 300); static const _kCurve = Curves.ease; final _kArrowColor = Colors.black.withOpacity(0.8); final List<Widget> _pages = <Widget>[ new ConstrainedBox( constraints: const BoxConstraints.expand(), child: new FlutterLogo(colors: Colors.blue), ), new ConstrainedBox( constraints: const BoxConstraints.expand(), child: new FlutterLogo(style: FlutterLogoStyle.stacked, colors: Colors.red), ), new ConstrainedBox( constraints: const BoxConstraints.expand(), child: new FlutterLogo(style: FlutterLogoStyle.horizontal, colors: Colors.green), ), ]; @override Widget build(BuildContext context) { return new Scaffold( body: new IconTheme( data: new IconThemeData(color: _kArrowColor), child: new Stack( children: <Widget>[ new PageView.builder( physics: new AlwaysScrollableScrollPhysics(), controller: _controller, itemBuilder: (BuildContext context, int index) { return _pages[index % _pages.length]; }, ), new Positioned( bottom: 0.0, left: 0.0, right: 0.0, child: new Container( color: Colors.grey[800].withOpacity(0.5), padding: const EdgeInsets.all(20.0), child: new Center( child: new DotsIndicator( controller: _controller, itemCount: _pages.length, onPageSelected: (int page) { _controller.animateToPage( page, duration: _kDuration, curve: _kCurve, ); }, ), ), ), ), ], ), ), ); }}

April 26, 2019 · 2 min · jiezi

flutter-绘制流水水波上升动态效果

欢迎去浏览原文:http://tryenough.com/flutter-wave效果 你可以先简单理解下贝塞尔曲线的原理: 推荐这个关于贝塞尔的教程:http://www.html-js.com/article/1628 代码:1.创建绘制波浪边界的代码 创建一个基础的绘制类,可接收动画的x和y值: import 'package:flutter/material.dart';abstract class BasePainter extends CustomPainter{ Animation<double> _xAnimation; Animation<double> _yAnimation; set XAnimation(Animation<double> value) { _xAnimation = value; } set YAnimation(Animation<double> value) { _yAnimation = value; } Animation<double> get YAnimation => _yAnimation; Animation<double> get XAnimation => _xAnimation;}实现 欢迎去浏览原文:http://tryenough.com/flutter-waveimport 'dart:math';import 'package:flutter_wave/painter_base.dart';import 'package:flutter/material.dart';class WavePainter extends BasePainter { int waveCount; int crestCount; double waveHeight; List<Color> waveColors; double circleWidth; Color circleColor; Color circleBackgroundColor; bool showProgressText; TextStyle textStyle; WavePainter( {this.waveCount = 1, this.crestCount = 2, this.waveHeight, this.waveColors, this.circleColor = Colors.grey, this.circleBackgroundColor = Colors.white, this.circleWidth = 5.0, this.showProgressText = true, this.textStyle = const TextStyle( fontSize: 60.0, color: Colors.blue, fontWeight: FontWeight.bold, shadows: [ Shadow(color: Colors.grey, offset: Offset(5.0, 5.0), blurRadius: 5.0) ], )}); @override void paint(Canvas canvas, Size size) { double width = size.width; double height = size.height; if (waveHeight == null) { waveHeight = height / 10; height = height + waveHeight; } if (waveColors == null) { waveColors = [ Color.fromARGB( 100, Colors.blue.red, Colors.blue.green, Colors.blue.blue) ]; } Offset center = new Offset(width / 2, height / 2); double xMove = width * XAnimation.value; double yAnimValue = 0.0; if (YAnimation != null) { yAnimValue = YAnimation.value; } double yMove = height * (1.0 - yAnimValue); Offset waveCenter = new Offset(xMove, yMove); var paintCircle = new Paint() ..color = Colors.grey ..style = PaintingStyle.fill ..strokeWidth = circleWidth ..maskFilter = MaskFilter.blur(BlurStyle.inner, 5.0);// canvas.drawCircle(center, min(width, height) / 2, paintCircle); List<Path> wavePaths = []; for (int index = 0; index < waveCount; index++) { double direction = pow(-1.0, index); Path path = new Path() ..moveTo(waveCenter.dx - width, waveCenter.dy) ..lineTo(waveCenter.dx - width, center.dy + height / 2) ..lineTo(waveCenter.dx + width, center.dy + height / 2) ..lineTo(waveCenter.dx + width, waveCenter.dy); for (int i = 0; i < 2; i++) { for (int j = 0; j < crestCount; j++) { double a = pow(-1.0, j); path ..quadraticBezierTo( waveCenter.dx + width * (1 - i - (1 + 2 * j) / (2 * crestCount)), waveCenter.dy + waveHeight * a * direction, waveCenter.dx + width * (1 - i - (2 + 2 * j) / (2 * crestCount)), waveCenter.dy); } } path..close(); wavePaths.add(path); } var paint = new Paint() ..color = circleBackgroundColor ..style = PaintingStyle.fill ..maskFilter = MaskFilter.blur(BlurStyle.inner, 5.0); canvas.saveLayer( Rect.fromCircle(center: center, radius: min(width, height) / 2), paint);// canvas.drawCircle(center, min(width, height) / 2, paint); paint// ..blendMode = BlendMode.srcATop ..style = PaintingStyle.fill ..strokeWidth = 2.0 ..maskFilter = MaskFilter.blur(BlurStyle.inner, 10.0); for (int i = 0; i < wavePaths.length; i++) { if (waveColors.length >= wavePaths.length) { paint.color = waveColors[i]; } else { paint.color = waveColors[0]; } canvas.drawPath(wavePaths[i], paint); }// paint.blendMode = BlendMode.srcATop; if (showProgressText) { TextPainter tp = TextPainter( text: TextSpan( text: '${(yAnimValue * 100.0).toStringAsFixed(0)}%', style: textStyle), textDirection: TextDirection.rtl) ..layout(); tp.paint( canvas, Offset(center.dx - tp.width / 2, center.dy - tp.height / 2)); } canvas.restore(); } @override bool shouldRepaint(CustomPainter oldDelegate) { return oldDelegate != this; }}欢迎去浏览原文:http://tryenough.com/flutter-wave2.创建工厂方法,用于创建波浪图形 ...

April 26, 2019 · 4 min · jiezi

Flutter滚动-中间显示整图-前后露出部分图

欢迎去看原文:http://tryenough.com/flutter-middle-scroll效果 代码代码比较简单,这也是flutter强大的地方。 import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: DisplayPage(), ); }}class DisplayPage extends StatefulWidget { @override _DisplayPageState createState() => new _DisplayPageState();}class _DisplayPageState extends State<DisplayPage> { static bool _isAddGradient = false; final List descriptions = [ 'tryenough.com', 'tryenough.com', 'tryenough.com', ]; var decorationBox = DecoratedBox( decoration: _isAddGradient ? BoxDecoration( gradient: LinearGradient( begin: FractionalOffset.bottomRight, end: FractionalOffset.topLeft, colors: [ Color(0x00000000).withOpacity(0.9), Color(0xff000000).withOpacity(0.01), ], ), ) : BoxDecoration(), ); @override Widget build(BuildContext context) { PageController controller = PageController(viewportFraction: 0.8); controller.addListener((){ }); return Scaffold( backgroundColor: Colors.black, body: Center( child: SizedBox.fromSize( size: Size.fromHeight(550.0), child: PageView.builder( controller: controller, itemCount: 3, itemBuilder: (BuildContext context, int index) { return Padding( padding: EdgeInsets.symmetric( vertical: 16.0, horizontal: 8.0, ), child: GestureDetector( onTap: () { Scaffold.of(context).showSnackBar(SnackBar( backgroundColor: Colors.deepOrangeAccent, duration: Duration(milliseconds: 800), content: Center( child: Text( descriptions[index], style: TextStyle(fontSize: 25.0), ), ), )); }, child: Material( elevation: 5.0, borderRadius: BorderRadius.circular(8.0), child: Stack( fit: StackFit.expand, children: [ FlutterLogo(style: FlutterLogoStyle.stacked, colors: Colors.red), decorationBox, ], ), ), ), ); }, ), ), ), ); }}核心就是pageview的controller中的viewportFraction属性。尝试修改下看看效果吧。^_^ ...

April 25, 2019 · 1 min · jiezi

Flutter-使用动画播放一组图片

请支持原文:http://tryenough.com/images-animation效果如下图: 代码import 'package:flutter/material.dart';import 'package:sprintf/sprintf.dart'; //这个是一个拼接字符串的flutter库,主要是为了使用方便,你可以选择不使用,这样的话你需要自己拼接图片路径class ImagesAnimation extends StatefulWidget { final double w; final double h; final ImagesAnimationEntry entry; final int durationSeconds; ImagesAnimation({Key key, this.w : 80, this.h : 80, this.entry, this.durationSeconds : 3}):super(key:key); @override _InState createState() { return _InState(); }}class _InState extends State<ImagesAnimation> with TickerProviderStateMixin{ AnimationController _controller; Animation<int> _animation; @override void initState() { super.initState(); _controller = new AnimationController(vsync: this, duration: Duration(seconds: widget.durationSeconds)) ..repeat(); _animation = new IntTween(begin: widget.entry.lowIndex, end: widget.entry.highIndex).animate(_controller);//widget.entry.lowIndex 表示从第几下标开始,如0;widget.entry.highIndex表示最大下标:如7 } @override Widget build(BuildContext context) { return new AnimatedBuilder( animation: _animation, builder: (BuildContext context, Widget child) { String frame = _animation.value.toString(); return new Image.asset( sprintf(widget.entry.basePath, [frame]), //根据传进来的参数拼接路径 gaplessPlayback: true, //避免图片闪烁 width: widget.w, height: widget.h, ); }, ); }}class ImagesAnimationEntry { int lowIndex = 0; int highIndex = 0; String basePath; ImagesAnimationEntry(this.lowIndex, this.highIndex, this.basePath);}请支持原文:http://tryenough.com/images-animation使用的地方:ImagesAnimation(w: 100, h: 100, entry: ImagesAnimationEntry(1, 7, "images/men_sport_%s.png")),//"images/men_sport_%s.png" 表示图片在你本地的路径,%s会被下标代替

April 23, 2019 · 1 min · jiezi

Flutter实战之坑——按返回键回到手机桌面不退出app

原理:在main里监听最外层返回键,然后通讯原生,执行 moveTaskToBack(false) 回到手机桌面不退出app安卓文件里 MainActivity.java import android.os.Bundle;import io.flutter.app.FlutterActivity;import io.flutter.plugins.GeneratedPluginRegistrant;import android.view.KeyEvent;import io.flutter.plugin.common.MethodCall;import io.flutter.plugin.common.MethodChannel;public class MainActivity extends FlutterActivity { //通讯名称,回到手机桌面 private final String CHANNEL = "android/back/desktop"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler( new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { if (methodCall.method.equals("backDesktop")) { result.success(true); moveTaskToBack(false); } } } ); }}新建一个 android_back_desktop.dart import 'package:flutter/services.dart';import 'package:flutter/material.dart';class AndroidBackTop { //初始化通信管道-设置退出到手机桌面 static const String CHANNEL = "android/back/desktop"; //设置回退到手机桌面 static Future<bool> backDeskTop() async { final platform = MethodChannel(CHANNEL); //通知安卓返回,到手机桌面 try { final bool out = await platform.invokeMethod('backDesktop'); if (out) debugPrint('返回到桌面'); } on PlatformException catch (e) { debugPrint("通信失败(设置回退到安卓手机桌面:设置失败)"); print(e.toString()); } return Future.value(false); }}入口文件 main.dart import 'package:flutter_smart_park/untils/android_back_desktop.dart';export 'package:common_utils/common_utils.dart';void main() async { runApp(MyApp());}class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Provide<ConfigModel>( builder: (context, child, configModel) { return MaterialApp( title: 'test', debugShowCheckedModeBanner: false, home: WillPopScope( onWillPop: () async { AndroidBackTop.backDeskTop(); //设置为返回不退出app return false; //一定要return false }, child: Text("Test"), ), ); }, ); }}End老铁们,来个素质三连呗!!!欢迎关注我的博客 ...

April 23, 2019 · 1 min · jiezi

flutter 自定义带水波纹和点击态的cell

请支持原文:http://tryenough.com/flutter-custom-cell看效果 代码:请支持原文:http://tryenough.com/flutter-custom-cellclass _CListTile extends StatefulWidget { _CListTile( {Key key, this.text, this.textColor: Colors.black, this.textHighLightColor: const Color(0xff25C78A), this.leadingIconPath, this.leadingHighLightIconPath, @required this.onTab}) : super(key: key); final Function onTab; final String text; final Color textColor; final Color textHighLightColor; final String leadingIconPath; final String leadingHighLightIconPath; _CListTileState createState() => _CListTileState();}class _CListTileState extends State<_CListTile> { bool _highlight = false; void _handleTapDown(TapDownDetails details) { setState(() { _highlight = true; }); } void _handleTapUp(TapUpDetails details) { setState(() { _highlight = false; }); } void _handleTapCancel() { setState(() { _highlight = false; }); } void _handleTap() { widget.onTab(); } Widget build(BuildContext context) { return GestureDetector( onTapDown: _handleTapDown, onTapUp: _handleTapUp, onTap: _handleTap, onTapCancel: _handleTapCancel, child: Container( height: 52, child: Material( child: InkWell( onTap: (){ if(widget.onTab != null) { widget.onTab(); } }, child: Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: <Widget>[ Padding(padding: EdgeInsets.only(left: 16)), _highlight ? Image.asset(widget.leadingHighLightIconPath, width: 25) : Image.asset(widget.leadingIconPath, width: 25), Padding(padding: EdgeInsets.only(left: 15)), Text(widget.text, style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w600, color: _highlight ? widget.textHighLightColor : widget.textColor)), ], ), ), ) ), ); }}请支持原文:http://tryenough.com/flutter-custom-cell使用的地方_CListTile( text: "test title", leadingIconPath: "images/test.png", leadingHighLightIconPath: "images/test1.png", onTab: () { print("test"); }),

April 22, 2019 · 1 min · jiezi

flutter在2019年会有怎样的表现?

Flutter的趋势在移动端,受成本和效率的驱使,跨平台一站式开发慢慢成为一个趋势。从Hybird,RN,WEEX,Flutter,到各种小程序或快应用的大量涌现,虽然很多跨平台方案都有各自的优缺点,目前还没有完美无缺的终极方案,但这已是未来移动端开发不可逆转的一大方向。而Google推出并开源的移动应用开发框架Flutter,更是其中的明星。笔者从自身在做Flutter相关的分享中,特别强烈的感受是,有非常非常多的Native技术栈的同学在学习和使用Flutter,有非常多的前端技术栈的同学在时刻关注Flutter的hummingbird和desktop-embedding的进展。尤其自Flutter1.0 发布后,Flutter受到了业界更多的关注和期待。跨平台解决方案比较目前几个主流的跨平台解决方案:基于浏览器技术的Hybird基于桥接Native组件,如RN、WEEX基于底层统一渲染,如Flutter它们有各种的优缺点,但浏览器技术无疑是其中的历史最长、标准最完善、用户最多、生态最丰富的。RN、WEEX也可以归类为javascript生态的一个小分支。而Flutter走的是和前两者截然不同的路线,它是一个新兴的挑战者,通过底层统一渲染,得到高度一致的跨端效果;通过引入dart,得到AOT的接近原生的性能,和JIT的快速开发体验;通过上层完善的组件体系(material design & cupertino),得到高保真的UI体验。但它也并非尽善尽美。同时基于底层统一渲染的跨平台方案有很多,在移动端有实际应用的如QT、cocos2d等。对比Flutter和QT,最大的区别在语言和背后团队。语言:Flutter选择了Dart,QT是C++。Dart相比C++,对开发者来说无疑于相比骑自行车和开飞机的区别,Dart更容易编写,除此以外,Dart还拥有AOT和JIT两种模式、类型安全、快速内存分配等等特点,确实如Flutter团队所述,同时拥有一两条这些优点的语言不少,但是将所有这些优点集于一身的,只有Dart。背后团队:Flutter的背后是Google,QT的背后是TrollTech,从社区影响力和号召力而言不可同日而语。但同时也必须要认识到的是通过底层统一渲染的跨平台方案,也有它天然的劣势。它很难复用系统天然提供的组件。在摆脱对操作系统的依赖和复用操作系统的能力上,要考虑如何达到了一个最佳的平衡。Flutter的生态如果拿Flutter生态同React和Native进行比较的话基于核心UI表达层向上,这一层会更接近前端的体系,以React生态为参照物,主要的几部分路由体系一种面向以Flutter为主的应用,它的路由以Flutter为主,Native的路由部分往往以简易桥接的形态存在。一种面向混合技术栈为主的应用,它的路由以Native为主,Flutter为辅。状态管理体系 | 应用框架基本上在React生态下有的状态管理,Flutter也有,同时有一些是Flutter独有的。开源的代表有:flutter_redux, google的BLoc,scoped_model,及闲鱼的fish-redux,它在真实的复杂场景下得到了非常好的验证。UI库体系目前已有不少UI库,包含常见的组件。基于核心UI表达层向下,这一层会更接近Native的体系,以Native生态为参照物,主要的几部分核心的一些基础中间件,如网络,图片,音视频,存储,埋点,监控等。目前和Native相比还是有非常大的差距。所以也导致了目前大部分这些问题的解决方案,都趋向于桥接的形态。通过复用Native能力来短期补齐Flutter能力不足的。但它不一定是未来的最佳的方案。一些重量级的基础组件,如WebView,MapView等。目前已经能通过PlatformView的形式,得到能力拓展。但是它有使用的局限性和性能上的损失。Flutter今年几个重要的突破点Code-Push在当下国内应用生态环境,热修复或者热部署能力在很多公司和团队做技术选型中,往往是其中非常重要的一个选项。如果有Google官方推出,不管是hotfix,还是dynamic-boundle都将极大的推动Flutter在国内的发展。而基于dart语言的特性判断,在Flutter上做code-push理论上会比目前任何Native的code-push方案有更强的能力。闲鱼团队一直和Flutter团队就这方便保持紧密的联系,在之前的验证中,目前在android端是可以支持的,但还留有一些瑕疵。Humming-Bird在跨平台之外,还有一层更高级别的诉求,多应用投放,打破应用之间的孤岛壁垒,实现更多的商业价值。而要完成多应用的投放,首选的是基于浏览器的方案。Humming-Bird方案为这样的设想,提供了可能。同时Humming-Bird也将大大扩张了Flutter的边界,吸引更多的开发者和厂商的加入,同时让面向终端的全栈解决方案成为可能。Dart语言也有可能成为javascript生态的更好的补充和演进。Flutter面向未来基础架构设计决定了一个软件的发展上限,它带来了更多的想象力。使用Flutter和Dart,既是Google为摆脱和Oracle纠缠多年的“Java 侵权案”提前下的一颗棋,也是Google为下一代操作系统Fusion下的一颗棋,是即Google通过chromium项目渐进的统一浏览器领域,着眼于更多的终端,为了一个更大终端生态的大一统做准备。这让Flutter和Dart充满了更高层次的可能。如果没有这些可能,Flutter的生命无疑是会短暂的,因为它还未能建立被广泛被认可的标准,就像我们终端里走过的那么多的技术一样,都是有限的解决了当下的诉求,但随着终端的更替,操作系统的演进,慢慢变成了明日黄花。而正是这些更多的可能,是Flutter持续演进的源泉,是Flutter相比其他的跨平台方案中最吸引人的部分。本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 19, 2019 · 1 min · jiezi

开发跨平台app推荐React Native还是flutter?

嗯。。。这个问题十分不好回答啊(捋下鱼须)。闲鱼作为flutter领域的先驱者,以及fish_redux、flutter_boost等当红flutter库的作者,当然是欢迎广大的开发者多多使用flutter相关技术栈 逃~:)。咳咳,不过呢,我们还是正经得聊一下React Native(下面简称RN)和flutter之前的异同:0x00 简单介绍一下React NativeReact Native是Facebook开源的一款基于react思想、使用JS、能够给移动平台带来native般体验的框架,官网最新的版本是0.5.9。flutterflutter来自Google,上层使用dart语言构建跨平台应用,通过平台相关的embedded层接入到使用c++编写的engine层,再通过skia库直接与GPU进行交互。通过对dart代码的AOT编译,拥有优异的计算(CPU)、渲染(GPU)性能。官网最新的版本为1.2。0x01 跨平台性开发者们使用跨平台技术栈,首要的目的是为了能够省事儿,所以跨平台能力是首先要被衡量的指标。Build native mobile apps using JavaScript and React这意味着开发者可以复用庞大的JavaScript生态和优雅的react思想来书写RN的代码,给开发提供很多的便利性。从实现原理上来说,RN进行完排版之后会把最终的渲染交给native view,这种方式带来的是如native般的UI性能,但同时也给给平台一致性带来了一些问题。除开渲染层的不一致,在iOS和Android没有使用同一个JavaScript虚拟机也会带来一些暗坑。手势的处理上两个平台不好统一,RN官方也没有提供一个抹平差异的库,虽然开源社区有react-native-gesture-handler。Beautiful native apps in record timeflutter官方的口气很大,说自己是”空前“的。是不是”空前“,我们得来评估一下。编程语言层面,flutter使用dart语言构建应用,这门语言对大多数人来说应该是比较陌生,好在dart的语法并不复杂,与Java等强类型oop语言非常相似,还加入了函数式的特性,使用起来还是挺方便的。flutter提供类似React思想的响应性UI编程模型,让UI开发变得更加fancy。原理上来说,flutter在各个平台上使用统一的vm(dart vm),自带GDI(skia)。skia是一个已经发展多年成熟度相当高的2D图形库,也是Android系统和Chrome一直在使用的图形库。flutter从逻辑计算到渲染绘图,都是自己的,使得它在跨平台一致性上有良好的表现。dart提供的AOT特性也可以保证应用在线上有一个好的性能表现。多平台支持RN目前支持iOS和Android两个平台,另外有个非官方的ReactNativeX的项目旨在让RN运行到其他平台。flutter早期支持iOS和Android,desktop的支持目前尚不完善。近期flutter团队发布了Hummingbird,旨在让flutter编写的应用可以运行在浏览器端。从多平台支持的角度看,两边差距不大。相比RN,flutter在desktop的支持上有些优势,但目前都是不怎么可用状态。0x02 开发便利性工具链RN在打包发布方面有被前端广泛使用的webpack支持,官方自己提供了基于浏览器的debug工具,与前端同学管用的调试方式并无二致。flutter基于iOS和Android已有的打包工具添加了flutter产物打包功能,同样debug工具也由官方自己提供,除了刚发布的基于浏览器的调试工具外,flutter团队提供的调试工具可以直接在Android Studio或者VScode这类IDE上直接使用。调试便利性JS的调试方式已经很成熟了,这里不多做展开。flutter在debug阶段可以使用集成于IDE插件中的hot reload功能做到亚秒级的新代码加载速度,十分适合与设计师坐在一起结(ya)对(li)编(tiao)程(shi) :)。第三方库在RN上你可以使用JS的大部分库,平台相关的plugin也相对丰富。flutter在这方面稍显欠缺,库的数量上无法与JS生态相比较。flutter/plugins项目提供了大量的平台相关插件供开发者使用,倒也是满足了日常开发的需求,另外dart pubs上的公开库数量也日趋上升。在混合开发和大型app业务框架上,闲鱼技术开源的flutter_boost提供了与native混合开发的可能,而fish_redux使得大型app中的复杂页面的开发在flutter中变得更加容易。0x03 未来的发展开发者选择一个技术,都是压了”身家性命“在上面,谁也不想刚入门就发现这门技术即将被淘汰。RN是个很好的项目,在发布之初给移动开发带来了一阵旋风。但不得不说,Airbnb宣布放弃使用RN技术栈对于整个社区有不小的打击,而文章中对原因的阐述也相当有说服力。flutter在1.0发布之后趋于成熟,被钦定为Google Fuchsia系统的应用层框架。从团队2019 roadmap中可以看到,flutter当前重点在于完善一些现有功能上的细节与bugfix,另外对于广受期待的动态化特性,flutter团队也在开发code push功能。从flutter团队目前的方向和笔者在闲鱼开发中实际使用的flutter的感受来看,整体上flutter在框架层面目前已经基本上稳定。从桌面端跨平台框架发展的历程来看,Java GUI从最初使用peer(对等设计模式)的AWT,到基于Java图形绘制接口性能巨慢无比的Swing,再到公认性能最好目前应用最广泛的基于目标平台绘制接口的SWT,我们可以从中窥见一些历史规律。peer(对等设计模式),即AWT中的一个控件,对应目标平台(如Windows)上的一个控件(是不是看起来跟RN有一些相似),最终AWT被放弃是因为peer模式传输层级过多造成效率低下,GUI部分为了保证可移植性只能保留各个平台公共的接口。SWT与QT(另一个被广泛使用的桌面端跨平台GUI框架),牺牲了一部分可移植性(主要是因为直接调用了目标平台的图形绘制接口),带来了GUI的高性能。flutter也采用了类似技术栈,skia来抹平各个平台的绘制接口差异,并向上提供统一的图形接口。从这个角度来说,无疑flutter可能会是一个更有未来的跨平台框架。0x04 写在最后当然Facebook官方对于RN正在进行重构,包括把大部分逻辑移动到c++层来减少线程切换的开销提升性能等。选择一个框架需要考虑的实际情况比框架的优劣比较更加重要,比如你的项目大小、开发人员构成等,RN和flutter作为目前移动平台上炙手可热的框架,两者并不是孰优孰劣的对立关系。纸上得来终觉浅,如果你是个对新技术感兴趣,抑或是希望在移动平台上有所突破的开发者,何不尝试一下Google最新的成果咧?本文作者:闲鱼技术-海猪阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 19, 2019 · 1 min · jiezi

码上用它开始Flutter混合开发——FlutterBoost

开源地址: https://github.com/alibaba/fl…为什么需要混合方案具有一定规模的App通常有一套成熟通用的基础库,尤其是阿里系App,一般需要依赖很多体系内的基础库。那么使用Flutter重新从头开发App的成本和风险都较高。所以在Native App进行渐进式迁移是Flutter技术在现有Native App进行应用的稳健型方式。闲鱼在实践中沉淀出一套自己的混合技术方案。在此过程中,我们跟Google Flutter团队进行着密切的沟通,听取了官方的一些建议,同时也针对我们业务具体情况进行方案的选型以及具体的实现。官方提出的混合方案基本原理Flutter技术链主要由C++实现的Flutter Engine和Dart实现的Framework组成(其配套的编译和构建工具我们这里不参与讨论)。Flutter Engine负责线程管理,Dart VM状态管理和Dart代码加载等工作。而Dart代码所实现的Framework则是业务接触到的主要API,诸如Widget等概念就是在Dart层面Framework内容。一个进程里面最多只会初始化一个Dart VM。然而一个进程可以有多个Flutter Engine,多个Engine实例共享同一个Dart VM。我们来看具体实现,在iOS上面每初始化一个FlutterViewController就会有一个引擎随之初始化,也就意味着会有新的线程(理论上线程可以复用)去跑Dart代码。Android类似的Activity也会有类似的效果。如果你启动多个引擎实例,注意此时Dart VM依然是共享的,只是不同Engine实例加载的代码跑在各自独立的Isolate。官方建议引擎深度共享在混合方案方面,我们跟Google讨论了可能的一些方案。Flutter官方给出的建议是从长期来看,我们应该支持在同一个引擎支持多窗口绘制的能力,至少在逻辑上做到FlutterViewController是共享同一个引擎的资源的。换句话说,我们希望所有绘制窗口共享同一个主Isolate。但官方给出的长期建议目前来说没有很好的支持。多引擎模式我们在混合方案中解决的主要问题是如何去处理交替出现的Flutter和Native页面。Google工程师给出了一个Keep It Simple的方案:对于连续的Flutter页面(Widget)只需要在当前FlutterViewController打开即可,对于间隔的Flutter页面我们初始化新的引擎。例如,我们进行下面一组导航操作:Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3 我们只需要在Flutter Page1和Flutter Page3创建不同的Flutter实例即可。这个方案的好处就是简单易懂,逻辑清晰,但是也有潜在的问题。如果一个Native页面一个Flutter页面一直交替进行的话,Flutter Engine的数量会线性增加,而Flutter Engine本身是一个比较重的对象。多引擎模式的问题冗余的资源问题.多引擎模式下每个引擎之间的Isolate是相互独立的。在逻辑上这并没有什么坏处,但是引擎底层其实是维护了图片缓存等比较消耗内存的对象。想象一下,每个引擎都维护自己一份图片缓存,内存压力将会非常大。插件注册的问题。插件依赖Messenger去传递消息,而目前Messenger是由FlutterViewController(Activity)去实现的。如果你有多个FlutterViewController,插件的注册和通信将会变得混乱难以维护,消息的传递的源头和目标也变得不可控。Flutter Widget和Native的页面差异化问题。Flutter的页面是Widget,Native的页面是VC。逻辑上来说我们希望消除Flutter页面与Naitve页面的差异,否则在进行页面埋点和其它一些统一操作的时候都会遇到额外的复杂度。增加页面之间通信的复杂度。如果所有Dart代码都运行在同一个引擎实例,它们共享一个Isolate,可以用统一的编程框架进行Widget之间的通信,多引擎实例也让这件事情更加复杂。因此,综合多方面考虑,我们没有采用多引擎混合方案。现状与思考前面我们提到多引擎存在一些实际问题,所以闲鱼目前采用的混合方案是共享同一个引擎的方案。这个方案基于这样一个事实:任何时候我们最多只能看到一个页面,当然有些特定的场景你可以看到多个ViewController,但是这些特殊场景我们这里不讨论。我们可以这样简单去理解这个方案:我们把共享的Flutter View当成一个画布,然后用一个Native的容器作为逻辑的页面。每次在打开一个容器的时候我们通过通信机制通知Flutter View绘制成当前的逻辑页面,然后将Flutter View放到当前容器里面。老方案在Dart侧维护了一个Navigator栈的结构。栈数据结构特点就是每次只能从栈顶去操作页面,每一次在查找逻辑页面的时候如果发现页面不在栈顶那么需要往回Pop。这样中途Pop掉的页面状态就丢失了。这个方案无法支持同时存在多个平级逻辑页面的情况,因为你在页面切换的时候必须从栈顶去操作,无法再保持状态的同时进行平级切换。举个例子:有两个页面A,B,当前B在栈顶。切换到A需要把B从栈顶Pop出去,此时B的状态丢失,如果想切回B,我们只能重新打开B之前页面的状态无法维持住。这也是老方案最大的一个局限。如在pop的过程当中,可能会把Flutter 官方的Dialog进行误杀。这也是一个问题。而且基于栈的操作我们依赖对Flutter框架的一个属性修改,这让这个方案具有了侵入性的特点。这也是我们需要解决的一个问题。具体细节,大家可以参考老方案开源项目地址:https://github.com/alibaba-flutter/hybrid_stack_manager新一代混合技术方案 FlutterBoost重构计划在闲鱼推进Flutter化过程当中,更加复杂的页面场景逐渐暴露了老方案的局限性和一些问题。所以我们启动了代号FlutterBoost(向C++ Boost致敬)的新混合技术方案。这次新的混合方案我们的主要目标有:可复用通用型混合方案支持更加复杂的混合模式。比如支持主页Tab这种情况无侵入性方案:不再依赖修改Flutter的方案支持通用页面生命周期统一明确的设计概念跟老方案类似,新的方案还是采用共享引擎的模式实现。主要思路是由Native容器Container通过消息驱动Flutter页面容器Container,从而达到Native Container与Flutter Container的同步目的。我们希望做到Flutter渲染的内容是由Naitve容器去驱动的。简单的理解,我们想做到把Flutter容器做成浏览器的感觉。填写一个页面地址,然后由容器去管理页面的绘制。在Native侧我们只需要关心如果初始化容器,然后设置容器对应的页面标志即可。主要概念Native层概念Container:Native容器,平台Controller,Activity,ViewControllerContainer Manager:容器的管理者Adaptor:Flutter是适配层Messaging:基于Channel的消息通信Dart层概念Container:Flutter用来容纳Widget的容器,具体实现为Navigator的派生类-Container Manager:Flutter容器的管理,提供show,remove等ApiCoordinator: 协调器,接受Messaging消息,负责调用Container Manager的状态管理。Messaging:基于Channel的消息通信关于页面的理解在Native和Flutter表示页面的对象和概念是不一致的。在Native,我们对于页面的概念一般是ViewController,Activity。而对于Flutter我们对于页面的概念是Widget。我们希望可统一页面的概念,或者说弱化抽象掉Flutter本身的Widget对应的页面概念。换句话说,当一个Native的页面容器存在的时候,FlutteBoost保证一定会有一个Widget作为容器的内容。所以我们在理解和进行路由操作的时候都应该以Native的容器为准,Flutter Widget依赖于Native页面容器的状态。那么在FlutterBoost的概念里说到页面的时候,我们指的是Native容器和它所附属的Widget。所有页面路由操作,打开或者关闭页面,实际上都是对Native页面容器的直接操作。无论路由请求来自何方,最终都会转发给Native去实现路由操作。这也是接入FlutterBoost的时候需要实现Platform协议的原因。另一方面,我们无法控制业务代码通过Flutter本身的Navigator去push新的Widget。对于业务不通过FlutterBoost而直接使用Navigator操作Widget的情况,包括Dialog这种非全屏Widget,我们建议是业务自己负责管理其状态。这种类型Widget不属于FlutterBoost所定义的页面概念。理解这里的页面概念,对于理解和使用FlutterBoost至关重要。与老方案主要差别前面我们提到老方案在Dart层维护单个Navigator栈结构用于Widget的切换。而新的方案则是在Dart侧引入了Container的概念,不再用栈的结构去维护现有的页面,而是通过扁平化key-value映射的形式去维护当前所有的页面,每个页面拥有一个唯一的id。这种结构很自然的支持了页面的查找和切换,不再受制于栈顶操作的问题,之前的一些由于pop导致的问题迎刃而解。同时也不再需要依赖修改Flutter源码的形式去进行实现,除去了实现的侵入性。那这是如何做到的呢?多Navigator的实现Flutter在底层提供了让你自定义Navigator的接口,我们自己实现了一个管理多个Navigator的对象。当前最多只会有一个可见的Flutter Navigator,这个Navigator所包含的页面也就是我们当前可见容器所对应的页面。Native容器与Flutter容器(Navigator)是一一对应的,生命周期也是同步的。当一个Native容器被创建的时候,Flutter的一个容器也被创建,它们通过相同的id关联起来。当Native的容器被销毁的时候,Flutter的容器也被销毁。Flutter容器的状态是跟随Native容器,这也就是我们说的Native驱动。由Manager统一管理切换当前在屏幕上展示的容器。我们用一个简单的例子描述一个新页面创建的过程:创建Native容器(iOS ViewController,Android Activity or Fragment)。Native容器通过消息机制通知Flutter Coordinator新的容器被创建。Flutter Container Manager进而得到通知,负责创建出对应的Flutter容器,并且在其中装载对应的Widget页面。当Native容器展示到屏幕上时,容器发消息给Flutter Coordinator通知要展示页面的id.Flutter Container Manager找到对应id的Flutter Container并将其设置为前台可见容器。这就是一个新页面创建的主要逻辑,销毁和进入后台等操作也类似有Native容器事件去进行驱动。总结目前FlutterBoost已经在生产环境支撑着在闲鱼客户端中所有的基于Flutter开发业务,为更加负复杂的混合场景提供了支持。同时也解决了一些历史遗留问题。我们在项目启动之初就希望FlutterBoost能够解决Native App混合模式接入Flutter这个通用问题。所以我们把它做成了一个可复用的Flutter插件,希望吸引更多感兴趣的朋友参与到Flutter社区的建设。我们的方案可能不是最好的,这个方案距离完美还有很大的距离,我们希望通过多分享交流以推动Flutter技术社区的发展与建设。我们更希望看到社区能够涌现出更加优秀的组件和方案。在有限篇幅中,我们分享了闲鱼在Flutter混合技术方案中积累的经验和代码。欢迎兴趣的同学能够积极与我们一起交流学习。扩展补充性能相关在两个Flutter页面进行切换的时候,因为我们只有一个Flutter View所以需要对上一个页面进行截图保存,如果Flutter页面多截图会占用大量内存。这里我们采用文件内存二级缓存策略,在内存中最多只保存2-3个截图,其余的写入文件按需加载。这样我们可以在保证用户体验的同时在内存方面也保持一个较为稳定的水平。页面渲染性能方面,Flutter的AOT优势展露无遗。在页面快速切换的时候,Flutter能够很灵敏的相应页面的切换,在逻辑上创造出一种Flutter多个页面的感觉。Release 1.0支持项目开始的时候我们基于闲鱼目前使用的Flutter版本进行开发,而后进行了Release 1.0兼容升级测试目前没有发现问题。接入只要是集成了Flutter的项目都可以用官方依赖的方式非常方便的以插件形式引入FlutterBoost,只需要对工程进行少量代码接入即可完成接入。详细接入文档,请参阅GitHub主页官方项目文档。现已开源目前,新一代混合栈已经在闲鱼全面应用。我们非常乐意将沉淀的技术回馈给社区。欢迎大家一起贡献,一起交流,携手共建Flutter社区。项目开源地址:https://github.com/alibaba/flutter_boost本文作者:闲鱼技术-福居阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 17, 2019 · 1 min · jiezi

Fish Redux中的Dispatch是怎么实现的?

零、前言我们在使用fish-redux构建应用的时候,界面代码(view)和事件的处理逻辑(reducer,effect)是完全解耦的,界面需要处理事件的时候将action分发给对应的事件处理逻辑去进行处理,而这个分发的过程就是下面要讲的dispatch, 通过本篇的内容,你可以更深刻的理解一个action是如何一步步去进行分发的。一、从example开始为了更好的理解action的dispatch过程,我们就先以todo_list_page中一条todo条目的勾选事件为例,来看点击后事件的传递过程,通过断点debug我们很容易就能够发现点击时候发生的一切,具体过程如下:用户点击勾选框,GestureDetector的onTap会被回调通过buildView传入的dispatch函数对doneAction进行分发,发现todo_component的effect中无法处理此doneAction,所以将其交给pageStore的dispatch继续进行分发pageStore的dispatch会将action交给reducer进行处理,故doneAction对应的_markDone会被执行,对state进行clone,并修改clone后的state的状态,然后将这个全新的state返回然后pageStore的dispatch会通知所有的listeners,其中负责界面重绘的_viewUpdater发现state发生变化,通知界面进行重绘更新二、Dispatch实现分析Dispatch在实现的过程中借鉴了Elm。Dispatch在fish-redux中的定义如下typedef Dispatch = void Function(Action action);本质上就是一个action的处理函数,接受一个action,然后对action进行分发。下面我门通过源码来进行详细的分析1.component中的dispatchbuildView函数传入的dispatch是对应的component的mainCtx中的dispatch,_mainCtx和componet的关系如下component -> ComponentWidget -> ComponentState -> _mainCtx -> _dispatch而 _mainCtx的初始化则是通过componet的createContext方法来创建的,顺着方法下去我们看到了dispatch的初始化// redux_component/context.dart DefaultContext初始化方法 DefaultContext({ @required this.factors, @required this.store, @required BuildContext buildContext, @required this.getState, }) : assert(factors != null), assert(store != null), assert(buildContext != null), assert(getState != null), _buildContext = buildContext { final OnAction onAction = factors.createHandlerOnAction(this); /// create Dispatch _dispatch = factors.createDispatch(onAction, this, store.dispatch); /// Register inter-component broadcast _onBroadcast = factors.createHandlerOnBroadcast(onAction, this, store.dispatch); registerOnDisposed(store.registerReceiver(_onBroadcast)); }context中的dispatch是通过factors来进行创建的,factors其实就是当前component,factors创建dispatch的时候传入了onAction函数,以及context自己和store的dispatch。onAction主要是进行Effect处理。这边还可以看到,进行context初始化的最后,还将自己的onAction包装注册到store的广播中去,这样就可以接收到别人发出的action广播。Component继承自Logic// redux_component/logic.dart @override Dispatch createDispatch( OnAction onAction, Context<T> ctx, Dispatch parentDispatch) { Dispatch dispatch = (Action action) { throw Exception( ‘Dispatching while appending your effect & onError to dispatch is not allowed.’); }; /// attach to store.dispatch dispatch = _applyOnAction<T>(onAction, ctx)( dispatch: (Action action) => dispatch(action), getState: () => ctx.state, )(parentDispatch); return dispatch; } static Middleware<T> _applyOnAction<T>(OnAction onAction, Context<T> ctx) { return ({Dispatch dispatch, Get<T> getState}) { return (Dispatch next) { return (Action action) { final Object result = onAction?.call(action); if (result != null && result != false) { return; } //skip-lifecycle-actions if (action.type is Lifecycle) { return; } if (!shouldBeInterruptedBeforeReducer(action)) { ctx.pageBroadcast(action); } next(action); }; }; }; }}上面分发的逻辑大概可以通过上图来表示通过onAction将action交给component对应的effect进行处理当effect无法处理此action,且此action非lifecycle-actions,且不需中断则广播给当前Page的其余所有effects最后就是继续将action分发给store的dispatch(parentDispatch传入的其实就是store.dispatch)2. store中的dispatch从store的创建代码我们可以看到store的dispatch的具体逻辑// redux/create_store.dart final Dispatch dispatch = (Action action) { _throwIfNot(action != null, ‘Expected the action to be non-null value.’); _throwIfNot( action.type != null, ‘Expected the action.type to be non-null value.’); _throwIfNot(!isDispatching, ‘Reducers may not dispatch actions.’); try { isDispatching = true; state = reducer(state, action); } finally { isDispatching = false; } final List<_VoidCallback> _notifyListeners = listeners.toList( growable: false, ); for (_VoidCallback listener in _notifyListeners) { listener(); } notifyController.add(state); };store的dispatch过程比较简单,主要就是进行reducer的调用,处理完成后通知监听者。3.middlewarePage继承自Component,增加了middleware机制,fish-redux的redux部分本身其实就对middleware做了支持,可以通过StoreEnhancer的方式将middlewares进行组装,合并到Store的dispatch函数中。middleware机制可以允许我们通过中间件的方式对redux的state做AOP处理,比如fish-redux自带的logMiddleware,可以对state的变化进行log,分别打印出state变化前和变化后的值。当Page配置了middleware之后,在创建pageStore的过程中会将配置的middleware传入,传入之后会对store的dispath进行增强加工,将middleware的处理函数串联到dispatch中。// redux_component/component.dart Widget buildPage(P param) { return wrapper(_PageWidget<T>( component: this, storeBuilder: () => createPageStore<T>( initState(param), reducer, applyMiddleware<T>(buildMiddleware(middleware)), ), )); }// redux_component/page_store.dartPageStore<T> createPageStore<T>(T preloadedState, Reducer<T> reducer, [StoreEnhancer<T> enhancer]) => _PageStore<T>(createStore(preloadedState, reducer, enhancer));// redux/create_store.dartStore<T> createStore<T>(T preloadedState, Reducer<T> reducer, [StoreEnhancer<T> enhancer]) => enhancer != null ? enhancer(_createStore)(preloadedState, reducer) : _createStore(preloadedState, reducer);所以这里可以看到,当传入enhancer时,createStore的工作被enhancer代理了,会返回一个经过enhancer处理过的store。而PageStore创建的时候传入的是中间件的enhancer。// redux/apply_middleware.dartStoreEnhancer<T> applyMiddleware<T>(List<Middleware<T>> middleware) { return middleware == null || middleware.isEmpty ? null : (StoreCreator<T> creator) => (T initState, Reducer<T> reducer) { assert(middleware != null && middleware.isNotEmpty); final Store<T> store = creator(initState, reducer); final Dispatch initialValue = store.dispatch; store.dispatch = (Action action) { throw Exception( ‘Dispatching while constructing your middleware is not allowed. ’ ‘Other middleware would not be applied to this dispatch.’); }; store.dispatch = middleware .map((Middleware<T> middleware) => middleware( dispatch: (Action action) => store.dispatch(action), getState: store.getState, )) .fold( initialValue, (Dispatch previousValue, Dispatch Function(Dispatch) element) => element(previousValue), ); return store; };}这里的逻辑其实就是将所有的middleware的处理函数都串到store的dispatch,这样当store进行dispatch的时候所有的中间件的处理函数也会被调用。下面为各个处理函数的执行顺序,首先还是component中的dispatch D1 会被执行,然后传递给store的dispatch,而此时store的dispatch已经经过中间件的增强,所以会执行中间件的处理函数,最终store的原始dispatch函数D2会被执行。三、总结通过上面的内容,现在我们可以知道一个action是如何一步步的派送给effect,reducer去进行处理的,我们也可以通过middleware的方式去跟踪state的变化,这样的扩展性给框架本身带来无限可能。本文作者:闲鱼技术-卢克阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 17, 2019 · 2 min · jiezi

flutter 播放帧动画

谢谢支持原文:http://tryenough.com/flutter-frame-animal本文是关于如何循环播放一连串的图片,形成动画效果。看下效果:你需要做的就是把UI提供的一系列图片传入到下列代码中,并设置宽高。工具类封装:import ‘package:flutter/material.dart’;class ImagesAnim extends StatefulWidget { final Map<int, Image> imageCaches; final double width; final double height; final Color backColor; ImagesAnim(this.imageCaches, this.width, this.height, this.backColor, {Key key}) : assert(imageCaches != null), super(key: key); @override State<StatefulWidget> createState() { return new _WOActionImageState(); }}class _WOActionImageState extends State<ImagesAnim> { bool _disposed; Duration _duration; int _imageIndex; Container _container; @override void initState() { super.initState(); _disposed = false; _duration = Duration(milliseconds: 800); _imageIndex = 1; _container = Container(height: widget.height, width: widget.width); _updateImage(); } void _updateImage() { if (_disposed || widget.imageCaches.isEmpty) { return; } setState(() { if (_imageIndex > widget.imageCaches.length) { _imageIndex = 1; } _container = Container( color: widget.backColor, child: widget.imageCaches[_imageIndex], height: widget.height, width: widget.width); _imageIndex++; }); Future.delayed(_duration, () { _updateImage(); }); } @override void dispose() { super.dispose(); _disposed = true; widget.imageCaches.clear(); } @override Widget build(BuildContext context) { return _container; }}谢谢支持原文:http://tryenough.com/flutter-frame-animal使用://imageCaches 是图片mapCenter( child:ImagesAnim(imageCaches, 100, 150, Colors.transparent),), ...

April 16, 2019 · 1 min · jiezi

手把手教你在Flutter项目优雅的使用ORM数据库--下篇

A orm database Flutter plugin.之前发了一篇文章《手把手教你在Flutter项目优雅的使用ORM数据库》,很多人咨询使用也提了一些宝贵的意见,说不希望要写lua,这样不够优雅,也增加了学习成本。细想了一下,确实是,对flutter项目开发来讲,最好就是纯flutter版的orm框架,于是我就写了一个flutter版的 orm插件flutter_orm_plugin ,使用的demo我放到github上了,大家可以下载来玩玩。下面我介绍一下flutter_orm_plugin提供的所有api。添加orm表flutter_orm_plugin中一个orm对应一个表,例如demo中Student表,它所在的db名字叫School,表名叫Student,它包含如下四个字段:studentId 在数据库中是Integer类型,主键,自增的。name 在数据库中是Text类型class 在数据库中是Text类型,是外键,关联的表是另一个表Class表score 在数据库中是Real类型创建这样的表的代码在demo的main.dart中 Map<String , Field> fields = new Map<String , Field>(); fields[“studentId”] = Field(FieldType.Integer, primaryKey: true , autoIncrement: true); fields[“name”] = Field(FieldType.Text); fields[“class”] = Field(FieldType.Text, foreignKey: true, to: “School_Class”); fields[“score”] = Field(FieldType.Real); FlutterOrmPlugin.createTable(“School”,“Student”,fields);数据库中某一列的数据通过Field类定义,我们先看看Field的定义就可以知道我们的orm对象支持哪些属性了class Field { final FieldType type;//类型包括Integer、Real、Blob、Char、Text、Boolean bool unique;//是否惟一 int maxLength; bool primaryKey;//是否主键 bool foreignKey;//是否外键 bool autoIncrement;//是否自增 String to;//关联外键表,以DBName_TableName命名 bool index;//是否有索引}插入数据单条插入 Map m = {“name”:“william”, “class”:“class1”, “score”:96.5}; FlutterOrmPlugin.saveOrm(“Student”, m);批量插入 List orms = new List(); for(int i = 0 ; i < 100 ; i++) { Map m = {“name”:name, “class”:className, “score”:score}; orms.add(m); } FlutterOrmPlugin.batchSaveOrms(“Student”, orms); 查询数据全部查询 Query(“Student”).all().then((List l) { }); 查询第一条 Query(“Student”).first().then((Map m) { }); 根据主键查询 Query(“Student”).primaryKey([1,3,5]).all().then((List l) { }); where条件查询 Query(“Student”).whereByColumFilters([WhereCondiction(“score”, WhereCondictionType.EQ_OR_MORE_THEN, 90)]).all().then((List l) { }); where sql 语句查询 Query(“Student”).whereBySql(“class in (?,?) and score > ?”, [“class1”,“class2”,90]).all().then((List l) { }); where 查询并排序 Query(“Student”).orderBy([“score desc”,]).all().then((List l) { }); 查询指定列 Query(“Student”).needColums([“studentId”,“name”]).all().then((List l) { }); group by 、having 查询 Query(“Student”).needColums([“class”]).groupBy([“class”]).havingByBindings(“avg(score) > ?”, [40]).orderBy([“avg(score)”]).all().then((List l) { }); 更新数据全部更新 Query(“Student”).update({“name”:“test all update”}); 根据主键更新 Query(“Student”).primaryKey([11]).update({“name”:“test update by primary key”}); 根据特定条件更新 Query(“Student”).whereByColumFilters([WhereCondiction(“studentId”, WhereCondictionType.LESS_THEN, 5),WhereCondiction(“score”, WhereCondictionType.EQ_OR_MORE_THEN, 0)]).update({“score”:100}); 根据自定义where sql更新 Query(“Student”).whereBySql(“studentId <= ? and score <= ?”, [5,100]).update({“score”:0}); 删除数据全部删除 Query(“Student”).delete(); 根据主键删除 Query(“Student”).primaryKey([1,3,5]).delete(); 根据条件删除 Query(“Student”).whereByColumFilters([WhereCondiction(“studentId”, WhereCondictionType.IN, [1,3,5])]).delete();根据自定义where sql删除 Query(“Student”).whereBySql(“studentId in (?,?,?)”, [1,3,5]).delete();联表查询inner join JoinCondiction c = new JoinCondiction(“Match”); c.type = JoinType.INNER; c.matchColumns = {“studentId”: “winnerId”}; Query(“Student”).join(c).all().then((List l) { }); left join JoinCondiction c = new JoinCondiction(“Match”); c.type = JoinType.LEFT; c.matchColumns = {“studentId”: “winnerId”}; Query(“Student”).join(c).all().then((List l) { }); 利用外键联表 JoinCondiction c = new JoinCondiction(“Class”); c.type = JoinType.INNER; Query(“Student”).join(c).all().then((List l) { }); where sql 联表 JoinCondiction c = new JoinCondiction(“Match”); c.type = JoinType.INNER; c.matchColumns = {“studentId”: “winnerId”}; Query(“Student”).join(c).whereBySql(“Student.score > ?”,[60]).all().then((List l) { }); 部分column 联表查询 JoinCondiction c = new JoinCondiction(“Match”); c.type = JoinType.INNER; c.matchColumns = {“studentId”: “winnerId”}; Query(“Student”).join(c).needColums([“name”,“score”]).all().then((List l) { }); group by 、having 联表查询 JoinCondiction c = new JoinCondiction(“Class”); c.type = JoinType.INNER; Query(“Student”).join(c).needColums([“class”]).groupBy([“Student.class”]).havingByBindings(“avg(Student.score) > ?”, [40]).all().then((List l) { }); order by 联表查询 JoinCondiction c = new JoinCondiction(“Class”); c.type = JoinType.INNER; Query(“Student”).join(c).orderBy([“Student.score desc”]).all().then((List l) { }); 使用介绍flutter_orm_plugin 已经发布到flutter 插件仓库。只要简单配置即可使用,在yaml文件中加上flutter_orm_plugin依赖以及orm框架所需要的lua源文件,flutter_orm_plugin会对所有lua代码进行封装,最终使用只需要关心dart接口,对lua是无感的。 flutter_orm_plugin: ^1.0.0 . . . assets: - packages/flutter_orm_plugin/lua/DB.lua - packages/flutter_orm_plugin/lua/orm/model.lua - packages/flutter_orm_plugin/lua/orm/cache.lua - packages/flutter_orm_plugin/lua/orm/dbData.lua - packages/flutter_orm_plugin/lua/orm/tools/fields.lua - packages/flutter_orm_plugin/lua/orm/tools/func.lua - packages/flutter_orm_plugin/lua/orm/class/fields.lua - packages/flutter_orm_plugin/lua/orm/class/global.lua - packages/flutter_orm_plugin/lua/orm/class/property.lua - packages/flutter_orm_plugin/lua/orm/class/query.lua - packages/flutter_orm_plugin/lua/orm/class/query_list.lua - packages/flutter_orm_plugin/lua/orm/class/select.lua - packages/flutter_orm_plugin/lua/orm/class/table.lua - packages/flutter_orm_plugin/lua/orm/class/type.lua 在ios项目podfile加上luakit 依赖source ‘https://github.com/williamwen1986/LuakitPod.git'source ‘https://github.com/williamwen1986/curl.git'...pod ‘curl’, ‘> 1.0.0’pod ‘LuakitPod’, ‘> 1.0.17’在android项目app的build.gradle加上luakit依赖repositories { maven { url “https://jitpack.io” }}…implementation ‘com.github.williamwen1986:LuakitJitpack:1.0.9’完成配置即可使用。 ...

April 15, 2019 · 2 min · jiezi

Flutter - Widget-Context-Stage

读前须知:此篇文章基本上是Widget - State - Context - InheritedWidget的翻译并且删减了部分我个人觉得没有意义的文字,保留下来的部分也不会逐字逐句精确翻译,所以其实强烈推荐阅读英文原文。以下文章里面除了第一张思维导图是本人所作之外,凡是出现的示例代码和图片都是上面提到的英文文章里面的。本人在这里对此表示感激和愧疚,如果涉及到侵权,本人会立即删掉整篇文章。All the example codes and images used in this article are from Widget - State - Context - InheritedWidget excepting the first one. If this is of tort, I will delete this article immediately.正文开始:Widget, State 和Context是每一个flutter开发者必须要完全理解的概念,但是具体来说要理解哪些知识呢?这篇文章会就以下几个知识点进行讲解:1: Stateful和Stateless widget的差别2:什么是Context3: 什么是State以及怎么使用4:一个context和他的state之间的关系5:InheritedWidget以及在在Widget树里面怎么传递信息6:rebuild的概念接下来文章会按照以下结构去展开:PS:由于此篇的篇幅很长,此思维导图里面的第二部分(左边的’怎样获取State‘)的篇幅也会很长,所以第二部分会放到下一篇文章讲解。一:基本概念解释1:什么是WidgetWidget即组件。在flutter里面几乎万物都是Widget,跟我们常说的component是同一个概念。2: 什么是Widget treeWidget按照树形结构组合的产物就是Widget tree。这个Widget tree的每个节点上又是一个Widget。包含其他组件的组件叫做父组件,被其他组件包含的组件叫做子组件。比如下面一段代码:@overrideWidget build(BuildContext){ return new Scaffold( appBar: new AppBar( title: new Text(widget.title), ), body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Text( ‘You have pushed the button this many times:’, ), new Text( ‘$_counter’, style: Theme.of(context).textTheme.display1, ), ], ), ), floatingActionButton: new FloatingActionButton( onPressed: _incrementCounter, tooltip: ‘Increment’, child: new Icon(Icons.add), ), );}上面的一段代码,如果我们用图像表示它的widget tree的话就如下图所示: 3:什么是Context一个context标识了一个widget在widget tree的结构中是在哪个地方被build的。简而言之就是一个context标明了一个widget是挂载在widget tree的那个节点的。一个context只属于一个widget。假如一个widget A有子组件,那么widget A的context会变成子组件的父context。以上文提到的widget tree为例子,假如我们画出它的context,如下图所示(一个颜色代表一个context):Context Visibility(上下文可见性)只在其自己的context或者在其父context可见。从以上描述,我们可以轻易地找到一个widget的父widget,例如:Scaffold > Center > Column > Text:context.ancestorWidgetOfExtractType(Scaffold) => 会找到第一个沿着Text的context一路向上而遇到的第一个Scaffold.从一个父组件,也能找到子组件,但是不建议这么做(稍后会讨论到)。4:Widget的分类在Flutter里面有2类widget:Stateless WidgetStateful Widget从字面意思上可以理解Stateless Widget就是无状态的组件,Stateful Widget是有状态的组件,接下来我们对二者做更具体的讲解。Stateless Widget有些组件只依赖于他们自己的配置信息,这些配置信息一般是由他们的父组件在build他们的时候提供。换句话说,这些组件一旦创建之后,就不用担心任何变化。这类型的组件就是Stateless Widget.典型的例子比如Text, Row, Column, Container等,对于这些组件来说在build的时候,我们只需要传一些参数给他们就好。一个Stateless Widget只能被build一次,意思就是一旦build,不会因为任何的事件或者用户行为而重新build。Stateless Widget lifecycle下面是一个典型的Stateless Widget的代码结构。如我们所见,我们传递一些阐述给它的构造函数,但是请记住,这些参数不会再之后被改变了,所以一般你也会看到这些参数是被定义为final的。class MyStatelessWidget extends StatelessWidget { MyStatelessWidget({ Key key, this.parameter, }): super(key:key); final parameter; @override Widget build(BuildContext context){ return new … }}虽然有另一个方法(createElement)可以被overridden,但是一般没人会用到它。唯一一个需要被override的方法就是build().Stateless widget的生命周期是直接而简单的,如下所示:初始化通过build()方法渲染Stateful Widget一些其他的组件所拥有的数据会在组件生命周期内产生变化,这些数据就变成了dynamic(动态的)的。这些被组件拥有的会在组件的生命周期内改变的数据的列表,我们叫做State。而拥有以上特点的组件,我们就叫做Stateful Widget。Stateful Widget的例子就好比一个用户可以选择的Checkboxes的列表或者一个会根据某种条件而disabled的Button。5: 什么是State一个State定义了一个StatefulWidget的“行为”部分。State包含了以下旨在与一个组件交互的:行为(behaviour)UI布局(layout)任何对State的改变都会导致这个组件的rebuild。6:State和Context之间的关系对Stateful Widgets而言,一个State是和一个Context是绑定的,而且这种绑定关系是永久的,一个State永远不会改变他的Context。即使一个组件的Context在Widget tree上发生了移动,这个State还是会依然和那个Context绑定在一起。非常重要的知识点:因为一个State对象和一个Context是绑定的,这就意味着这个State对象不能从其他的Context下直接被获取到(这一点之后会讨论到)7:StatefulWidget的生命周期(lifecycle)前面已经介绍和很多StatefulWidget的相关概念和基础知识,接下来我们来了解一下StatefulWidget的生命周期,这里不会介绍全部的生命周期,先只挑几个重要的,与本篇文章的主旨相关的几个来讲。先看下下面一个StatefulWidget的例子:class MyStatefulWidget extends StatefulWidget { MyStatefulWidget({ Key key, this.parameter, }): super(key: key); final parameter; @override _MyStatefulWidgetState createState() => new _MyStatefulWidgetState();}class _MyStatefulWidgetState extends State<MyStatefulWidget> { @override void initState(){ super.initState(); // Additional initialization of the State } @override void didChangeDependencies(){ super.didChangeDependencies(); // Additional code } @override void dispose(){ // Additional disposal code super.dispose(); } @override Widget build(BuildContext context){ return new … }}下面的这个图展示了(一个简化的版本)一个StatefuleWidget在创建的时候,内部的一些列行为。在这个图的右边你可以看到一个State对象的内部状态变化以及最右边的Context是在什么时候和State产出联系而因此变为可用的。initState()initState()是在构造函数之后的第一个被调用的生命周期方法。当你需要再初始化阶段做一个额外的操作的时候,你需要override它。典型的在initState()方法里额外的操作比如动画,或者某些数据准备。假如你override initState(),记得调用super.initState()并且这行代码要放在initState()方法体的最前面。意思就是你得让super.initState()执行了之后再去执行你额外的初始化工作。在这个阶段,一个context是存在的,但是你并不能真正地使用它,因为这时候context和state还没有完成绑定。一旦initState()执行完毕,State对象就初始化好了,context也就可以被使用了。initState()在整个生命周期内只会被调用一次。didChangeDependencies()didChangeDependencies()是在生命周期里第二个被调用的方法。这个阶段,context已经可以被使用了。假如你的组件是链接到了InheritedWidget,根据context你需要初始化一些listeners(监听者),通常你需要override这个方法。注意,如果某个组件是链接了InheritedWidget,那么这个组件每次重建(rebuild),didChangeDependencies()都会被调用。假如你要override这个方法,你应该首先调用super.didChangeDependencies().build()build()跟在didChangeDependencies()(和didUpdateWidget())之后被调用。这个方法就是用来构建你的组件的。每一次State对象发生变化(或者InheritedWidget需要通知它的注册者)时,build()方法都会被调用。通常,我们通过调用setState((){…})来改变State对象,强制build()被调用,从而重新build我们的widget。dispose()dispose()在这个组件被销毁的时候被调用。一般我们override这个方法,可以在组件被销毁的时候做一个清理工作。override dispose()记得调用super.dispose()并且把它放在方法体的最后。8: StatelessWidget和StatefulWidget之间的抉择既然在Flutter里面有StatelessWidget和StatefulWidget这两种类型的组件,那在这二者之间如何抉择呢?记住标准就是:在这个组件的生命周期内,是否有会变化的数据,这个组件是否需要rebuild?如果答案是yes,那你就需要一个StatefulWidget而不会StatelessWidget。9:StatefulWidget的2个组成部分组件的构造函数部分class MyStatefulWidget extends StatefulWidget { MyStatefulWidget({ Key key, this.color, }): super(key: key); final Color color; @override _MyStatefulWidgetState createState() => new _MyStatefulWidgetState();}这部分是一个StatefulWidget的public部分。这部分不会在一个组件的生命周期内发生改变,它只是接收一些参数以便它的State可以使用。比如上面这个例子里面的color这个参数。Widget State定义部分class _MyStatefulWidgetState extends State<MyStatefulWidget> { … @override Widget build(BuildContext context){ … }}_MyStatefulWidgetState是这个Widget在其生命周期内变化的部分,也是使得这个Widget能够rebuild的部分。_MyStatefulWidgetState通过widget.{name of the variable}可以获取存在MyStatefulWidget内的任意变量。例如这里,可以通过widget.color获取color变量。10:Widget的唯一标识-key在Flutter里面,每一个组件都是唯一标识的,这个唯一标识是在build的时候被定义的。这个唯一的标识就是组件的可选参数:Key. 加入key缺省了,系统会默认给你创建一个。在某些情形下,你必须强制制定key,以便你可以通过这个key获取到这个组件。你可以通过下面的一些helper来达到上面的目的:GlobalKey, LocalKey, UniqueKey或者ObjectKey。GlobalKey保证这个key在整个application里面都是唯一的。以下的例子就是保证myKey在整个application都是唯一的:GlobalKey myKey = new GlobalKey(); … @override Widget build(BuildContext context){ return new MyWidget( key: myKey ); }PS:由于此篇的篇幅已经够长,之前的思维导图里面的第二部分的篇幅也会很长,所以第二部分会放到下一篇文章讲解。 ...

April 14, 2019 · 2 min · jiezi

Flutter 如何调用Android和iOS原生代码

请大家支持我的网站:http://tryenough.com/flutter-tonative分3个大步骤:1.在flutter中调用原生方法2.在Android中实现被调用的方法3.在iOS中实现被调用的方法在flutter中调用原生方法场景,这里你希望调用原生方法告诉你一个bool值,这个值的意义你可以随意定,这里表示的意义是是否是中国用户。你可以在flutter中设计好要调用的方法名称,这里就叫isChinese请注意:在flutter中要调用原生代码需要通过通道传递消息,在flutter端就是MethodChannel。所以我们这里的做法是,在flutter 端创建一个自己命名的通道:const platform = const MethodChannel(“com.test/name”);这里的名字 com.test/name 你可以随便取。讲解一下:你可能有疑问了,我们自作主张在flutter端创建的通道,怎么就能告诉Android和iOS端到底该怎么调用呢?你这个问题问得极好,这里啊先告诉你,等下我们还要分别在两端创建和这个通道同名的通道。敲黑板了:此时你知道了,我们要在三端分别有一个通道了吧,而且这三个通道是同名的,所以就能连接起来了。不过这里还是先把flutter端的代码写完,然后我们再去分别设置android和iOS端的代码吧。go!请大家支持我的网站:http://tryenough.com/flutter-tonative我们在flutter中的代码如下:Future<bool> isEuropeUser() async { // Native channel const platform = const MethodChannel(“com.test/name”); //分析1 bool result = false; try { result = await platform.invokeMethod(“isChinese”); //分析2 } on PlatformException catch (e) { print(e.toString()); } return result; }还是分析一下:分析1: 创建一个我们自定义的channel。分析2: 用channel发送调用消息到原生端,调用方法是:isChinese好了,flutter端相信你也觉得很简单了,接下来我们来看下android端怎么搞。在Android中实现被调用的方法我建议你在Android studio编写Android端代码哦,因为这样有良好的代码提示和头文件引入。不过你要是有办法做到同样的效果,啥IDE俺都不在乎。在flutter项目文件夹里的Android文件夹中有一个 MainActivity.java文件,不要告诉我你找不到啊。我先告诉你等下就在MainActivity里注册我们的Android端插件。嘿嘿,现在先去写我们的Android端插件吧。代码我一次贴出来了,反正也不多。public class FlutterNativePlugin implements MethodChannel.MethodCallHandler { public static String CHANNEL = “com.test/name”; // 分析1 static MethodChannel channel; private Activity activity; private FlutterNativePlugin(Activity activity) { this.activity = activity; } public static void registerWith(PluginRegistry.Registrar registrar) { channel = new MethodChannel(registrar.messenger(), CHANNEL); FlutterNativePlugin instance = new FlutterNativePlugin(registrar.activity()); channel.setMethodCallHandler(instance); } @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { // 分析 2 if (methodCall.method.equals(“isChinese”)) { boolean isChinese = true; result.success(isEuropean); // 分析3 } else { result.notImplemented(); } }}请大家支持我的网站:http://tryenough.com/flutter-tonative分析:分析1: 注意这里的插件名字要和flutter中的一样分析2:onMethodCall这个方法是插件的回调,这里我们根据方法名isChinese判断调用的方法,然后实现我们的操作就行了。分析3:这里直接返回了true,因为这只是个例子,而你应该换成你自己的逻辑哦。我们的插件写好了,回到MainActivity.java中进行注册。看下代码:public class MainActivity extends FlutterActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); registerCustomPlugin(this); } private void registerCustomPlugin(PluginRegistry registrar) { FlutterNativePlugin.registerWith(registrar.registrarFor(FlutterNativePlugin.CHANNEL)); }}恭喜恭喜,Android端完成了。//////////////////////////////////////////////////////////////////////////////////////////////////////////接下来我们搞一下iOS端:请大家支持我的网站:http://tryenough.com/flutter-tonative在iOS中实现被调用的方法iOS中我建议你在xcode中编写代码哦。因为这样会有良好的提示。我先告诉你要改那些文件:用xcode打开iOS工程后,在Runner文件夹下有AppDelegate文件。我们等下就在这里进行注册我们的插件。那么我们先写我们的插件代码吧:FlutterNativePlugin.h#import <Foundation/Foundation.h>#import <Flutter/Flutter.h>NS_ASSUME_NONNULL_BEGIN@interface FlutterNativePlugin : NSObject <FlutterPlugin>@endNS_ASSUME_NONNULL_ENDFlutterNativePlugin.m#import “FlutterNativePlugin.h”#import “CountryUtils.h”@implementation FlutterNativePlugin+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>)registrar { FlutterMethodChannel channel = [FlutterMethodChannel methodChannelWithName:@“com.test/name” binaryMessenger:[registrar messenger]]; FlutterNativePlugin* instance = [[FlutterNativePlugin alloc] init]; [registrar addMethodCallDelegate:instance channel:channel];}- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@“isEuropeUser” isEqualToString:call.method]) { result([NSNumber numberWithBool:YES]); } else { result(FlutterMethodNotImplemented); }}@end分析:这里也是和android一个德行,分为注册和方法回调两部分。但是你可能发现了这里的通道是FlutterMethodChannel,这个不用大惊小怪,flutter也是用和Android上的MethodChannel不同类名类区分这两个平台的。只是名字不同而已。该在iOS上注册了:在 AppDelegate.m类的如下方法添加代码就行:- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; [FlutterNativePlugin registerWithRegistrar: [self registrarForPlugin:@“FlutterNativePlugin”]]; return [super application:application didFinishLaunchingWithOptions:launchOptions];}///////////////////////////////////////////////////////////////////////////////////恭喜你,iOS端也设置完了。运行你的应用查看能不能调用成功吧。祝你顺利。最后希望大家多多支持我的网站。因为我的文章一直会在我的网站上更新,而这里就不一定更新了。请大家支持我的网站:http://tryenough.com/flutter-tonative ...

April 11, 2019 · 1 min · jiezi

flutter routes

Flutter routerlib/routes.dartimport ‘package:flutter/material.dart’;import ‘package:flutter_sky/screens/home/index.dart’;import ‘package:flutter_sky/screens/profile/index.dart’;class Routes{ static final home = new Home(); static final profile = new Profile(); final routes = { ‘/’: (context) => home, ‘/home’: (context) => home, ‘/profile’: (context) => profile }; Routes(){ runApp(new MaterialApp( title: ‘flutter sky’,// initialRoute: ‘/’,// home: profile, routes: routes, )); }}上面是一个简单的路由配置代码,假设现在的APP有2个页面:home, profile。我们拿这个例子来讲一下flutter的routes配置一些注意的点:首先可以看到注掉的那2行代码,分别是MaterialApp的两个属性:initialRoute和home。// 声明:以下讨论都是基于不考虑onGenerateRoute和onUnknownRoute的存在initialRoute是启动APP的初始页面,也就是用户看到的第一个页面。如果这个属性没有给值,那么会去寻找路由表里面的’/’,或者MaterialApp的home属性。’/‘和MaterialApp的home属性路由表(也就是我们上面代码里面定义的routes变量)里面的’/’ 和MaterialApp的home属性,二者不能同时存在,但是必须有一个存在。当initialRoute没有设置或者MaterialApp的home属性都是定义了主页面,当initialRoute没有定义的时候,用户看到的就是’/‘或者MaterialApp的home多对应的页面当initialRoute设置了当initialRoute和’/‘或者MaterialApp的home属性同时存在的时候,initialRoute的优先级高于二者。意思就是如果initialRoute定义的页面和’/‘或者MaterialApp的home设置的页面不同时,用户看到的是initialRoute定义的页面。

April 11, 2019 · 1 min · jiezi

Flutter String格式化

请看原文: http://tryenough.com/flutter-stringformat在Android和iOS平台都有相应的方法进行字符串的格式化,但是在flutter中却没有直接提供在flutter上可以借助一个插件来进行格式化:sprintfimport ‘package:sprintf/sprintf.dart’;例子import ‘package:sprintf/sprintf.dart’;void main() { print(sprintf("%04i", [-42])); print(sprintf("%s %s", [“Hello”, “World”])); print(sprintf("%#04x", [10])); double seconds = 5.0; String name = ‘Dilki’; List<String> pets = [‘Cats’, ‘Dogs’]; String sentence1 = sprintf(‘Sends %2.2f seconds ago.’, [seconds]); String sentence2 = sprintf(‘Harry likes %s, I think %s likes %s.’, [pets[0], name, pets[1]]); print(sentence1); print(sentence2); }请看原文: http://tryenough.com/flutter-stringformat

April 9, 2019 · 1 min · jiezi

Flutter插件开发例子分享到facebook和twitter

这个活生生的例子会教你开发flutter插件,功能是封装Android和iOS端的分享到facebook和twitter的flutter接口。使用的分别是两端的系统分享功能,不需要集成facebook和twitter 的 sdk。 例子插件网址:https://pub.dartlang.org/packages/flutter_share_go#-readme-tab- 展示一下样式: ios 中分享到facebook: android中分享到facebook 开始开发插件 步骤一:创建插件项目 这里用的Android studio创建的项目,可以直接创建flutter plugin项目,你也可以用命令创建: flutter create --org com.example --template=plugin "plugin_name" 将上面的“plugin_name”换成你的插件名字就行了,我这个插件名字叫flutter_share_go,所以命令就是: ...

April 9, 2019 · 5 min · jiezi

在原生ios项目中集成flutter

概述本文不想写一个全篇步骤式的文章来描写怎么集成flutter,而是期望用一种探索的方式来追寻答案。原理分析我们首先看下flutter项目和一般原生项目的大概区别。为了跳转方便,原生项目的入口一般是UINavigationController。而我们看下flutter默认给我们创建的模板为:这里我们来看下flutter的引擎源码,看下这段代码做了什么工作,源码路径为:https://github.com/flutter/en…我们首先看下`FlutterAppDelegatehttps://github.com/flutter/en…- (instancetype)init { if (self = [super init]) { _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; } return self;}….- (BOOL)application:(UIApplication*)application willFinishLaunchingWithOptions:(NSDictionary*)launchOptions { return [_lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions];}….所以这里可以看到,FlutterAppDelegate完全是调用了FlutterPluginAppLifeCycleDelegate的所有方法。假设你的项目原先就有一个AppDelegate的实现类,那么可以参考FlutterAppDelegate的源码,创建一个FlutterPluginAppLifeCycleDelegate,并在所有方法中调用这个类实例的方法。原生项目中创建根ViewControler的方式可以使用StoryBoard,也可以使用代码创建。而flutter模板给我们创建的项目为StoryBoard的方式从这里我们可以发现,flutter默认项目模板是将FlutterViewController作为根ViewController。项目实战创建项目原理分析完毕,我们可以创建一个工程项目了.我们这里选择创建一个最常见的SingleViewApp改成不使用StoryBoard,而是代码创建根ViewController为了演示方便,我们创建一个controller修改一下启动代码:- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { // Override point for customization after application launch. self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds]; UIViewController main = [[MainViewController alloc]initWithNibName:@“MainViewController” bundle:nil]; UINavigationController root = [[UINavigationController alloc]initWithRootViewController:main]; self.window.backgroundColor = [UIColor whiteColor]; self.window.rootViewController = root; [self.window makeKeyAndVisible]; return YES;}在MainViewController中,我们摆上两个按钮:创建flutter模块我们使用flutter自带命令创建一个flutter模块项目flutter create -t module my_flutter把创建出来的所有文件一起拷贝到上面ios原生项目的同一级目录中:使用pod初始化一下项目:cd myprojectpod init这样就生成了Podfile我们打开修改一下,以便将flutter包括在里面platform :ios, ‘9.0’target ‘myproject’ doend#新添加的代码flutter_application_path = ‘../’eval(File.read(File.join(flutter_application_path, ‘.ios’, ‘Flutter’, ‘podhelper.rb’)), binding)运行下pod安装pod install我们可以看到,与刚才相比,新增加了workspace文件,我们关掉原来的项目,并打开workspace然后我们可以看到项目结构如下:编译一下:ld: ‘/Users/jzoom/SourceCode/myproject/myproject/DerivedData/myproject/Build/Products/Debug-iphoneos/FlutterPluginRegistrant/libFlutterPluginRegistrant.a(GeneratedPluginRegistrant.o)’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. file ‘/Users/jzoom/SourceCode/myproject/myproject/DerivedData/myproject/Build/Products/Debug-iphoneos/FlutterPluginRegistrant/libFlutterPluginRegistrant.a’ for architecture arm64clang: error: linker command failed with exit code 1 (use -v to see invocation)出现了这个错误打开项目编译配置,并搜索bit,出现下面结果:修改下Enable Bitcode为No此时编译ok。至此,在原生项目中配置flutter完毕,我们开始开发功能。修改AppDelegate由于我们的AppDelegate不是FlutterAppDelegate,所以我们按照前面分析的路子,改成如下://// AppDelegate.m// myproject//// Created by JZoom on 2019/4/9.// Copyright © 2019 JZoom. All rights reserved.//#import “AppDelegate.h”#import “GeneratedPluginRegistrant.h”#import <Flutter/Flutter.h>#import “MainViewController.h”@interface AppDelegate()<FlutterPluginRegistry>@end@implementation AppDelegate{ FlutterPluginAppLifeCycleDelegate* _lifeCycleDelegate;}- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { // Override point for customization after application launch. self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds]; UIViewController main = [[MainViewController alloc]initWithNibName:@“MainViewController” bundle:nil]; UINavigationController root = [[UINavigationController alloc]initWithRootViewController:main]; self.window.backgroundColor = [UIColor whiteColor]; self.window.rootViewController = root; [self.window makeKeyAndVisible]; [GeneratedPluginRegistrant registerWithRegistry:self]; return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];}- (instancetype)init { if (self = [super init]) { _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init]; } return self;}- (void)dealloc { _lifeCycleDelegate = nil;}- (BOOL)application:(UIApplication*)applicationwillFinishLaunchingWithOptions:(NSDictionary*)launchOptions { return [_lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions];}// Returns the key window’s rootViewController, if it’s a FlutterViewController.// Otherwise, returns nil.- (FlutterViewController*)rootFlutterViewController { UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController; if ([viewController isKindOfClass:[FlutterViewController class]]) { return (FlutterViewController*)viewController; } return nil;}- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { [super touchesBegan:touches withEvent:event]; // Pass status bar taps to key window Flutter rootViewController. if (self.rootFlutterViewController != nil) { [self.rootFlutterViewController handleStatusBarTouches:event]; }}- (void)applicationDidEnterBackground:(UIApplication*)application { [_lifeCycleDelegate applicationDidEnterBackground:application];}- (void)applicationWillEnterForeground:(UIApplication*)application { [_lifeCycleDelegate applicationWillEnterForeground:application];}- (void)applicationWillResignActive:(UIApplication*)application { [_lifeCycleDelegate applicationWillResignActive:application];}- (void)applicationDidBecomeActive:(UIApplication*)application { [_lifeCycleDelegate applicationDidBecomeActive:application];}- (void)applicationWillTerminate:(UIApplication*)application { [_lifeCycleDelegate applicationWillTerminate:application];}#pragma GCC diagnostic push#pragma GCC diagnostic ignored “-Wdeprecated-declarations”- (void)application:(UIApplication*)applicationdidRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings { [_lifeCycleDelegate application:applicationdidRegisterUserNotificationSettings:notificationSettings];}#pragma GCC diagnostic pop- (void)application:(UIApplication*)applicationdidRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { [_lifeCycleDelegate application:applicationdidRegisterForRemoteNotificationsWithDeviceToken:deviceToken];}- (void)application:(UIApplication*)applicationdidReceiveRemoteNotification:(NSDictionary*)userInfofetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { [_lifeCycleDelegate application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];}- (void)application:(UIApplication*)applicationdidReceiveLocalNotification:(UILocalNotification*)notification { [_lifeCycleDelegate application:application didReceiveLocalNotification:notification];}- (void)userNotificationCenter:(UNUserNotificationCenter*)center willPresentNotification:(UNNotification*)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandlerAPI_AVAILABLE(ios(10)) { if (@available(iOS 10.0, )) { [_lifeCycleDelegate userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler]; }}- (BOOL)application:(UIApplication)application openURL:(NSURL*)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>)options { return [_lifeCycleDelegate application:application openURL:url options:options];}- (BOOL)application:(UIApplication)application handleOpenURL:(NSURL*)url { return [_lifeCycleDelegate application:application handleOpenURL:url];}- (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation { return [_lifeCycleDelegate application:application openURL:url sourceApplication:sourceApplication annotation:annotation];}- (void)application:(UIApplication*)applicationperformActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) { [_lifeCycleDelegate application:application performActionForShortcutItem:shortcutItem completionHandler:completionHandler];}- (void)application:(UIApplication*)applicationhandleEventsForBackgroundURLSession:(nonnull NSString*)identifier completionHandler:(nonnull void (^)())completionHandler { [_lifeCycleDelegate application:applicationhandleEventsForBackgroundURLSession:identifier completionHandler:completionHandler];}- (void)application:(UIApplication*)applicationperformFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];}#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000- (BOOL)application:(UIApplication*)applicationcontinueUserActivity:(NSUserActivity*)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>>* __nullable restorableObjects))restorationHandler {#else - (BOOL)application:(UIApplication*)applicationcontinueUserActivity:(NSUserActivity*)userActivityrestorationHandler:(void (^)(NSArray* __nullable restorableObjects))restorationHandler {#endif return [_lifeCycleDelegate application:application continueUserActivity:userActivity restorationHandler:restorationHandler];} #pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController - (NSObject<FlutterPluginRegistrar>)registrarForPlugin:(NSString)pluginKey { UIViewController* rootViewController = _window.rootViewController; if ([rootViewController isKindOfClass:[FlutterViewController class]]) { return [[(FlutterViewController*)rootViewController pluginRegistry] registrarForPlugin:pluginKey]; } return nil;}- (BOOL)hasPlugin:(NSString*)pluginKey { UIViewController* rootViewController = _window.rootViewController; if ([rootViewController isKindOfClass:[FlutterViewController class]]) { return [[(FlutterViewController*)rootViewController pluginRegistry] hasPlugin:pluginKey]; } return false;}- (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey { UIViewController* rootViewController = _window.rootViewController; if ([rootViewController isKindOfClass:[FlutterViewController class]]) { return [[(FlutterViewController*)rootViewController pluginRegistry] valuePublishedByPlugin:pluginKey]; } return nil;}#pragma mark - FlutterAppLifeCycleProvider methods- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>)delegate { [_lifeCycleDelegate addDelegate:delegate];}@end创建FlutterViewController编辑下MainViewController- (IBAction)launchFlutter1:(id)sender { FlutterViewController c = [[FlutterViewController alloc]init]; [self.navigationController pushViewController:c animated:YES]; }编译下,运行点击按钮调取flutter视图,发现一片空白,并出现如下错误:2019-04-09 13:18:18.500285+0800 myproject[57815:1968395] [VERBOSE-1:callback_cache.cc(132)] Could not parse callback cache, aborting restore2019-04-09 13:18:36.554643+0800 myproject[57815:1968395] Failed to find assets path for “Frameworks/App.framework/flutter_assets"2019-04-09 13:18:36.658247+0800 myproject[57815:1969776] [VERBOSE-2:engine.cc(116)] Engine run configuration was invalid.2019-04-09 13:18:36.659545+0800 myproject[57815:1969776] [VERBOSE-2:FlutterEngine.mm(294)] Could not launch engine with configuration.2019-04-09 13:18:36.816199+0800 myproject[57815:1969793] flutter: Observatory listening on http://127.0.0.1:50167/我们看看和flutter自己创建的项目比,还差了什么如图:有三个地方,我们把这些文件copy一份放到我们的项目中,并且设置一下编译选项:修改下项目的配置,增加一个脚本/bin/sh “$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh” thin放到Copy Bundle Resources下面结果:进行优化在上面的步骤里面,我们通过直接文件拷贝将.ios目录下的flutter生成文件拷贝到了原生项目里面,显然我们不能每一次都手动这么做,我们可以添加一个命令来做这件事。rm -rf ${SOURCE_ROOT}/Flutter/Generated.xcconfigcp -ri ../.ios/Flutter/Generated.xcconfig ${SOURCE_ROOT}/Flutter/Generated.xcconfigrm -rf ${SOURCE_ROOT}/Flutter/App.frameworkcp -ri ../.ios/Flutter/App.framework ${SOURCE_ROOT}/Flutter/App.framework我们把这个命令放到前面去问题Q : 如何调用flutter的不同页面?A : 我们首先定义一下路由然后我们可以这么调用 /// flutter的路由视图 FlutterViewController* c = [[FlutterViewController alloc]init]; [c setInitialRoute:@“page2”]; [self.navigationController pushViewController:c animated:YES];Q : 如何在原生项目中调试flutter?A : 首先在命令行启动flutter的监听flutter attach如果有多台设备,需要选择一下设备flutter attach -d 设备标志然后就可以在xcode中启动调试运行项目改动代码之后按下键盘上面的r键就可以了。 ...

April 9, 2019 · 3 min · jiezi

燃烧我的卡路里 ---- Flutter瘦内存瘦包之图片组件

背景在电商类APP里,图片到现在为止仍然是最重要的信息承载媒介,不得不说逛淘宝的过程,其实就是一个看图片的过程。而商品详情页中的图片,通常是页面中内存占用最多的内容,占用了整个页面内存的超过 50%。闲鱼在Flutter化的过程中,选择了商品详情页作为第一个落地的场景。通过多版本的迭代完善,基于Flutter的详情页已经在闲鱼稳定运行。然而正因为详情页的图片量大,导致Flutter里图片相关的问题一直挥之不去。1:内存问题 — 连续push flutter界面内存累积2:安装包问题 — 过渡时期两份重复资源文件。3:寻址缓存问题 — 原有的寻址缓存策略无法复用。4:图片复用问题 — Native和Flutter重复下载相同图片。解决方案—-FXTexImage_V1为了解决这些问题,我们尝试着寻找一种新的思路,一种能够将flutter与native串联起来的思路。而之前做视频播放器的方案给了我们启发。熟悉Flutter的同学应该都知道,Flutter的视频组件是基于一个Flutter提供的一个叫“外接纹理”的技术实现的,关于flutter外接纹理,本人另外有一篇文章有更详细的论述,这里不再赘述。https://mp.weixin.qq.com/s/KkCsBvnRayvpXdI35J3fnw我们将每一张图片假想成一个:静态的视频。图片的内容由一个external_texture来负责显示,而这个external_texture则由native端提供具体的渲染数据。通过这种方案,我们便可以通过external_texture这座桥梁,将flutter作为native端图片的一个最终展示场所。而所有的下载、缓存、裁剪等逻辑都可以复用原来的native图片库。基于这个基本框架,我们形成了我们第一版本的图片渲染组件:FXTexImage—-V1。这个组件很好的解决了Flutter引入的安装包、图片缓存、图片复用等问题。但是图片最大的问题:内存问题,并没有得到解决。内存优化—-FXTexImage_V2为了用户体验,通常会有连续push若干个界面的场景(比如闲鱼的详情页,点击底部的推荐列表,可以一直往下push新的详情页),这种场景下,每一个界面都有大量的图片展示。所以在引入flutter以后,闲鱼在iPhone 6P等机型上通常只能push10个左右详情页就挂了。在考虑到在显示过程中,真正用户可见的页面,其实只有当前栈顶的两个页面,基于这个特征我们就做了优化逻辑:1:在push详情页过程中,我们只保留了当前展示页和当前页的前一页的图片资源,而之前的资源全部都做了释放(只是图片资源的释放,整个页面还有页面中的其他元素还是做了保留)。2:为了做到用户无感知,我们在pop过程中,会预先去加载当前界面下一个界面的图片资源。通过这种方式,理论上我们可以释放掉不可见的资源,从而保证在持续Push界面过程中内存缓慢增长,但是实践过程中发现内存仍然持续增长。经过排查,我们发现flutter 1.0版本以及0.8.2版本里,SurfaceTextureRegistry提供了release方法,这里将会把创建的SurfaceTexture进行释放。然而测试过程中发现,单单对SurfaceTexture释放,并没有完全释放内存,当反复创建对象时仍然会闪退。为此,我们在AndroidExternalTextureGL的析构函数中增加了纹理的释放glDeleteTextures逻辑。然而,AndroidExternalTextureGL的析构是在flutter的GPU线程调用的,而external_texture的release方法通常是在主线程,也就是PlatForm线程调用的。不同线程调用的问题就是会导致一个诡异的问题:推测是不同线程释放的逻辑影响了GL环境,导致文字渲染出了问题。所以,为了解决该问题,我们删除了SurfaceTextureRegistry的release方法里面SufaceTexture的释放逻辑,并且将SurfaceTexture的释放,放到AndroidExternalTextureGL析构阶段,通过Jni调用java方法实现资源释放。AndroidExternalTextureGL::~AndroidExternalTextureGL(){ if (state_ == AttachmentState::attached) { Detach(); if (texture_name_ != 0) { glDeleteTextures(1, &texture_name_); texture_name_ = 0; } } Release(); state_ = AttachmentState::detached;}void AndroidExternalTextureGL::Release() { JNIEnv* env = fml::jni::AttachCurrentThread(); fml::jni::ScopedJavaLocalRef<jobject> surfaceTexture = surface_texture_.get(env); if (!surfaceTexture.is_null()) { SurfaceTextureRelease(env, surfaceTexture.obj()); }}void SurfaceTextureRelease(JNIEnv* env, jobject obj) { env->CallVoidMethod(obj, g_release_method); FML_CHECK(CheckException(env));} g_release_method = env->GetMethodID(g_surface_texture_class->obj(), “release”, “()V”);CPU优化—-FXTexImage_V3通过外界纹理渲染图片+不可见页面资源释放,我们解决了上述提出的一系列问题,但是又引入了新的问题:CPU偏高,滑动帧率偏低。通过测试,在详情页滑动过程中,IOS和Android的CPU都比Flutter原生组件高10%以上,这个显然无法应用。经过排查,发现CPU高的原因是:IOS端: iOS的IOSExternalTextureGL模型是一个拉数据的模型,native端register一个CVPixelBuffer的生产者,当需要绘制时,都会调用一次这个生产者的copyPixelbuffer方法去拉一次数据。然后将拉到的CVPixelBuffer对象转换成GPU Texture。这里每一次转换都换造成CPU较大开销。并且这种拉数据的机制就要求这个生产者的必须一直保留着这个CVPixelBuffer对象(否则界面重刷以后,图片区域就显示白屏)。Android端: android 的数据存储在SurfaceTexture中。每一次external_texture layer需要绘制时候都会从SurfaceTexture中去update 数据到纹理中,由于SurfaceTexture使用基于EGLImage共享内存,所以虽然没有双份内存的问题,但是每一次update 都会带来较大的CPU开销。在之前外接纹理的文章中,我们提出了一种新的基于共享上下文的外接纹理方案。并在我们视频的拍摄和编辑中得到了很好的应用。该方案当初提出来,是为了解决视频数据从CPU -> GPU -> CPU -> GPU 输送的问题而提出来的。但是在图片这个场景下, 新的外接纹理方案下,一张图片在native端加载完成以后,立刻被转换成一个OpenGL的Texture,然后图片的资源马上被释放。当界面刷新时,对于同一张图片的重新渲染,IOSExternalTextureGL不需要再去做将数据(CVPixelBuffer或者SurfaceTexture)转换到Texture的逻辑,而是直接使用之前创建好的Texture。经过这一步优化,我们很好的限制了iOS的CPU和内存,Android的CPU。通过测试对比,V3版本的图片组件,相比于Flutter原生图片组件,在详情页正常滑动操作过程中,平均CPU高出3%左右,虽然仍差于原生组件,单相对是可以接受的。结果内存: 基于新图片组件,我们很好的限制住了连续push 下的内存增长速度,顺利的将iPhone 6P上的详情页push 最大数量从10个增加到了30个以上。在同一线上版本中,我们通过控制ABTest开关,测试新的图片渲染方案和Flutter自带图片组件方案的Abort率,发现新图片组件下闲鱼的Abort率降低20%。安装包: 新组件下,所有的资源组件与原来native资源共用,所有flutter期间新引入的资源出了gif图,全部删除,减少安装包900k+。并且后续可以不用继续新增。寻址策略: 复用native图片组件,基于阿里系自己的图片下载组件,这样可以做到随着集团组件升级版本,兼容版本过程中各种新的寻址方式和图片格式。图片复用:复用native图片组件,当图片地址命中缓存,可直接缓存加载,尺寸不一致时可以预先返回缓存图同时加载大图,这样大大增强详情页大图预览的浏览体验。遗留问题图片组件已经在闲鱼上全量部署,然而还是有一些问题没有得到很好的解决,上文提到过CPU比原生图片组件高3%左右,虽然用户没有感官体验,但是还是有优化空间。还有就是Flutter针对ExternalTexture的纹理渲染时没有开启抗锯齿,导致小图在大区域渲染时比原生组件效果要差。这里还需要继续排查原因。最后,FXTexImage组件还在持续优化中,当解决上述遗留问题以后便会在Github上开源。本文作者:闲鱼技术-炉军阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 8, 2019 · 1 min · jiezi

Flutter 添加背景图片

请支持原文:http://tryenough.com/flutter-backImageFlutter 中添加背景图片可以使用给Container添加decoration的方式。如下代码:body: Container( decoration: BoxDecoration( image: DecorationImage( image: AssetImage(“images/main_bg_with_blank.png”), fit: BoxFit.cover, ), ), child: Column(), ),上面这段代码可以给body添加一张背景图。然后在背景图之上再添加任意child(这里只是例子添加了一个空的Column)。

April 7, 2019 · 1 min · jiezi

Flutter国际化完整例子

请支持原文 http://tryenough.com/flutter-translationflutter国际化实现方案这里提供一份解决方案,和一份可以直接使用的demo。1.添加依赖库需要用到flutter_localizations包,在pubspec.yaml文件中添加如下依赖内容:dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter添加完记得执行获取这个库:在terminal中运行 flutter packages get, 或者在IntelliJ\Android Studio中点击’Packages get’2.添加多种语言的文件请支持原文 http://tryenough.com/flutter-translation在项目的lib同级目录下创建locale文件夹,并创建几个json文件,用来存储不同语言的翻译文案。如下文件结构:上图中创建了三种语言的json文件,你可以根据你的需要创建其他的语言json文件。添加翻译内容,如下:i18n_en.json:{ “about_page_slogan”:“Health starts here”}i18n_zh.json:{ “about_page_slogan”:“健康从这里开始”}记得在pubspec.yaml文件中添加这几个资源文件:3.创建一个工具类,保存当前支持的语言等信息请支持原文 http://tryenough.com/flutter-translationlocale_util.dartimport ‘package:flutter/material.dart’;typedef void LocaleChangeCallback(Locale locale);class LocaleUtil { // Support languages list final List<String> supportedLanguages = [’en’,‘zh’, ‘ja’]; // Support Locales list Iterable<Locale> supportedLocales() => supportedLanguages.map<Locale>((lang) => new Locale(lang, ‘’)); // Callback for manual locale changed LocaleChangeCallback onLocaleChanged; Locale locale; String languageCode; static final LocaleUtil _localeUtil = new LocaleUtil._internal(); factory LocaleUtil() { return _localeUtil; } LocaleUtil._internal(); /// 获取当前系统语言 String getLanguageCode() { if(languageCode == null) { return “en”; } return languageCode; }}LocaleUtil localeUtil = new LocaleUtil();3.添加翻译处理类文件translations.dart系统提供了LocalizationsDelegate类帮助我们监听系统语言的切换,所以我们可以继承LocalizationsDelegate类监听语言切换,并在切换时加载不同的json文件,来获取不同的语言文案。这里我们的translations工具类就是利用这样的原理来加载不同的语言文案:import ‘package:translation/locale_util.dart’;import ‘dart:async’;import ‘dart:convert’;import ‘package:flutter/material.dart’;import ‘package:flutter/services.dart’ show rootBundle;/// Class for Translate////// For example:////// import ‘package:workout/translations.dart’;////// dart/// For TextField content/// Translations.of(context).text("home_page_title");/// ////// dart/// For speak string/// Note: Tts will speak english if currentLanguage[# Tts's parameter] can't support////// Translations.of(context).speakText("home_page_title");/// ////// “home_page_title” is the key for text value///class Translations { Translations(Locale locale) { this.locale = locale; _localizedValues = null; } Locale locale; static Map<dynamic, dynamic> _localizedValues; static Map<dynamic, dynamic> _localizedValuesEn; // English map static Translations of(BuildContext context) { return Localizations.of<Translations>(context, Translations); } String text(String key) { try { String value = _localizedValues[key]; if(value == null || value.isEmpty) { return englishText(key); } else { return value; } } catch (e) { return englishText(key); } } String englishText(String key) { return localizedValuesEn[key] ?? ‘** $key not found’; } static Future<Translations> load(Locale locale) async { Translations translations = new Translations(locale); String jsonContent = await rootBundle.loadString(“locale/i18n${locale.languageCode}.json”); _localizedValues = json.decode(jsonContent); String enJsonContent = await rootBundle.loadString(“locale/i18n_en.json”); _localizedValuesEn = json.decode(enJsonContent); return translations; } get currentLanguage => locale.languageCode;}class TranslationsDelegate extends LocalizationsDelegate<Translations> { const TranslationsDelegate(); // Support languages @override bool isSupported(Locale locale) { localeUtil.languageCode = locale.languageCode; return localeUtil.supportedLanguages.contains(locale.languageCode); } @override Future<Translations> load(Locale locale) => Translations.load(locale); @override bool shouldReload(TranslationsDelegate old) => true;}// Delegate strong init a Translations instance when language was changedclass SpecificLocalizationDelegate extends LocalizationsDelegate<Translations> { final Locale overriddenLocale; const SpecificLocalizationDelegate(this.overriddenLocale); @override bool isSupported(Locale locale) => overriddenLocale != null; @override Future<Translations> load(Locale locale) => Translations.load(overriddenLocale); @override bool shouldReload(LocalizationsDelegate<Translations> old) => true;}上面的代码我们看到有两个继承自LocalizationsDelegate类的代理类,SpecificLocalizationDelegate类是提供一种让我们强制指定一种位置的方式,通过改变传进来的local来达到从新load新json的方式。如果你没有这种需求,可以只关心TranslationsDelegate这种默认方式。5.在main方法中将代理设置好请支持原文 http://tryenough.com/flutter-translation要想监听系统的代理,就需要设置系统位置代理。方法如下:在main.dart方法中设置:import ‘package:translation/tanslations.dart’;import ‘package:translation/locale_util.dart’;import ‘package:flutter/material.dart’;import ‘package:flutter_localizations/flutter_localizations.dart’;void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: ‘My Application’, theme: new ThemeData( primarySwatch: Colors.blue, ), localizationsDelegates: [ const TranslationsDelegate(), GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], supportedLocales: localeUtil.supportedLocales(), home: MyHomePage(), ); }}class MyHomePage extends StatefulWidget { _MyHomePageState createState() => new _MyHomePageState();}class _MyHomePageState extends State<MyHomePage>{ @override Widget build(BuildContext context){ return new Scaffold( appBar: new AppBar( title: new Text(Translations.of(context).text(“about_page_slogan”)), ), body: Center( child: Text(Translations.of(context).text(“about_page_slogan”)), ), ); }}请支持原文 http://tryenough.com/flutter-translationdemo下载地址:http://tryenough.com/flutter-translation ...

April 5, 2019 · 2 min · jiezi

移动端开发新趋势Flutter

该文章属于<简书 — 刘小壮>原创,转载请注明:<简书 — 刘小壮> https://www.jianshu.com/p/1a90adc09e99介绍Flutter是Google开发的新一代跨平台方案,Flutter可以实现写一份代码同时运行在iOS和Android设备上,并且提供很好的性能体验。Flutter使用Dart作为开发语言,这是一门简洁、强类型的编程语言。Flutter对于iOS和Android设备,提供了两套视觉库,可以针对不同的平台有不同的展示效果。Flutter原本是为了解决Web开发中的一些问题,而开发的一套精简版Web框架,拥有独立的渲染引擎和开发语言,但后来逐渐演变为移动端开发框架。正是由于Dart当初的定位是为了替代JS成为Web框架,所以Dart的语法更接近于JS语法。例如定义对象构建方法,以及实例化对象的方式等。在Google刚推出Flutter时,其发展很缓慢,终于在18年发布第一个Bate版之后迎来了爆发性增长,发布第一个Release版时增长速度更快。可以从Github上Star数据看出来这个增长的过程。在19年最新的Flutter 1.2版本中,已经开放Web支持的Beta版。目前已经有不少大型项目接入Flutter,阿里的咸鱼、头条的抖音、腾讯的NOW直播,都将Flutter当做应用程序的开发语言。除此之外,还有一些其他中小型公司也在做。整体架构Flutter可以理解为开发SDK或者工具包,其通过Dart作为开发语言,并且提供Material和Cupertino两套视觉控件,视图或其他和视图相关的类,都以Widget的形式表现。Flutter有自己的渲染引擎,并不依赖原生平台的渲染。Flutter还包含一个用C++实现的Engine,渲染也是包含在其中的。EngineFlutter是一套全新的跨平台方案,Flutter并不像React Native那样,依赖原生应用的渲染,而是自己有自己的渲染引擎,并使用Dart当做Flutter的开发语言。Flutter整体框架分为两层,底层是通过C++实现的引擎部分,Skia是Flutter的渲染引擎,负责跨平台的图形渲染。Dart作为Flutter的开发语言,在C++引擎上层是Dart的Framework。Flutter不仅仅提供了一套视觉库,在Flutter整体框架中包含各个层级阶段的库。例如实现一个游戏功能,上面一些游戏控件可以用上层视觉库,底层游戏可以直接基于Flutter的底层库进行开发,而不需要调用原生应用的底层库。Flutter的底层库是基于Open GL实现的,所以Open GL可以做的Flutter都可以。视觉库在上层Framework中包含两套视觉库,符合Android风格的Material,和符合iOS风格的Cupertino。也可以在此基础上,封装自己风格的系统组件。Cupertino是一套iOS风格的视觉库,包含iOS的导航栏、button、alertView等。Flutter对不同硬件平台有不同的兼容,例如同样的Material代码运行在iOS和Android不同平台上,有一些平台特有的显示和交互,Flutter依然对其进行了区分适配。例如滑动ScrollView时,iOS平台是有回弹效果的,而Android平台则是阻尼效果。例如iOS的导航栏标题是居中的,Android导航栏标题是向左的,等等。这些Flutter都做了区分兼容。除了Flutter为我们做的一些适配外,有一些控件是需要我们自己做适配的,例如AlertView,在Android和iOS两个平台下的表现就是不同的。这些iOS特性的控件都定义在Cupertino中,所以建议在进行App开发时,对一些控件进行上层封装。例如AlertView则对其进行一个二次封装,控件内部进行设备判断并选择不同的视觉库,这样可以保证各个平台的效果。虽然Flutter对于iOS和Android两个平台,开发有cupertino和material两个视觉库,但实际开发过程中的选择,应该使用material当做视觉库。因为Flutter对iOS的支持并不是很好,主要对Android平台支持比较好,material中的UI控件要比cupertino多好几倍。DartDart是Google在2011年推出的一款应用于Web开发的编程语言,Dart刚推出的时候,定位是替代JS做前端开发,后来逐步扩展到移动端和服务端。Dart是Flutter的开发语言,Flutter必须遵循Dart的语言特性。在此基础上,也会有自己的东西,例如Flutter的上层Framework,自己的渲染引擎等。可以说,Dart只是Flutter的一部分。Dart是强类型的,对定义的变量不需要声明其类型,Flutter会对其进行类型推导。如果不想使用类型推导,也可以自己声明指定的类型。Hot ReloadFlutter支持亚秒级热重载,Android Studio和VSCode都支持Hot Reload的特性。但需要区分的是,热重载和热更新是不同的两个概念,热重载是在运行调试状态下,将新代码直接更新到执行中的二进制。而热更新是在上线后,通过Runtime或其他方式,改变现有执行逻辑。AOT、JITFlutter支持AOT(Ahead of time)和JIT(Just in time)两种编译模式,JIT模式支持在运行过程中进行Hot Reload。刷新过程是一个增量的过程,由系统对本次和上次的代码做一次snapshot,将新的代码注入到DartVM中进行刷新。但有时会不能进行Hot Reload,此时进行一次全量的Hot Reload即可。而AOT模式则是在运行前预先编译好,这样在每次运行过程中就不需要进行分析、编译,此模式的运行速度是最快的。Flutter同时采用了两种方案,在开发阶段采用JIT模式进行开发,在release阶段采用AOT模式,将代码打包为二进制进行发布。在开发原生应用时,每次修改代码后都需要重新编译,并且运行到硬件设备上。由于Flutter支持Hot Reload,可以进行热重载,对项目的开发效率有很大的提升。由于Flutter实现机制支持JIT的原因,理论上来说是支持热更新以及服务器下发代码的。可以从服务器。但是由于这样会使性能变差,而且还有审核的问题,所以Flutter并没有采用这种方案。实现原理Flutter的热重载是基于State的,也就是我们在代码中经常出现的setState方法,通过这个来修改后,会执行相应的build方法,这就是热重载的基本过程。Flutter的hot reload的实现源码在下面路径中,在此路径中包含run_cold.dart和run_hot.dart两个文件,前者负责冷启动,后者负责热重载。~/flutter/packages/flutter_tools/lib/src/run_hot.dart热重载的代码实现在run_hot.dart文件中,有HotRunner来负责具体代码执行。当Flutter进行热重载时,会调用restart函数,函数内部会传入一个fullRestart的bool类型变量。热重载分为全量和非全量,fullRestart参数就是表示是否全量。以非全量热重载为例,函数的fullRestart会传入false,根据传入false参数,下面是部分核心代码。Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async { if (fullRestart) { // ….. } else { final bool reloadOnTopOfSnapshot = _runningFromSnapshot; final String progressPrefix = reloadOnTopOfSnapshot ? ‘Initializing’ : ‘Performing’; final Status status = logger.startProgress( ‘$progressPrefix hot reload…’, progressId: ‘hot.reload’ ); OperationResult result; try { result = await _reloadSources(pause: pauseAfterRestart, reason: reason); } finally { status.cancel(); } }}调用restart函数后,内部会调用_reloadSources函数,去执行内部逻辑。下面是大概逻辑执行流程。在_reloadSources函数内部,会调用_updateDevFS函数,函数内部会扫描修改的文件,并将文件修改前后进行对比,随后会将被改动的代码生成一个kernel files文件。随后会通过HTTP Server将生成的kernel files文件发送给Dart VM虚拟机,虚拟机拿到kernel文件后会调用_reloadSources函数进行资源重载,将kernel文件注入正在运行的Dart VM中。当资源重载完成后,会调用RPC接口触发Widgets的重绘。跨平台方案对比现在市面上RN、Weex的技术方案基本一样,所以这里就以RN来代表类似的跨平台方案。Flutter是基于GPU进行渲染的,而RN则将渲染交给原生平台,而自己只是负责通过JSCore将视图组织起来,并处理业务逻辑。所以在渲染效果和性能这块,Flutter的性能比RN要强很多。跨平台方案一般都需要对各个平台进行平台适配,也就是创建各自平台的适配层,RN的平台适配层要比Flutter要大很多。因为从技术实现来说,RN是通过JSCore引擎进行原生代码调用的,和原生代码交互很多,所以需要更多的适配。而Flutter则只需要对各自平台独有的特性进行适配即可,例如调用系统相册、粘贴板等。Flutter技术实现是基于更底层实现的,对平台依赖度不是很高,相对来说,RN对平台的依赖度是很高的。所以RN未来的技术升级,包括扩展之类的,都会受到很大的限制。而Flutter未来的潜力将会很大,可以做很多技术改进。Widget在Flutter中将显示以及和显示相关的部分,都统一定义为widget,下面列举一些widget包含的类型:用于显示的视图,例如ListView、Text、Container等。用来操作视图,例如Transform等动画相关。视图布局相关,例如Center、Expanded、Column等。在Flutter中,所有的视图都是由Widget组成,Label、AppBar、ViewController等。在Flutter的设计中,组合的优先级要大于继承,整体视图类结构继承层级很浅但单层很多类。如果想定制或封装一些控件,也应该以组合为主,而不是继承。在iOS开发中,我也经常采用这种设计方案,组合大于继承。因为如果继承层级过多的话,一个是不便于阅读代码,还有就是不好维护代码。例如底层需要改一个通用的样式,但这个类的继承层级比较复杂,这样改动的话影响范围就比较大,会将一些不需要改的也改掉,这时候就会发现继承很鸡肋。但在iOS中有Category的概念,这也是一种组合的方式,可以通过将一些公共的东西放在Category中,使继承的方便性和组合的灵活性达到一个平衡。Flutter中并没有单独的布局文件,例如iOS的XIB这种,代码都在Widget中定义。和UIView的区别在于,Widget只是负责描述视图,并不参与视图的渲染。UIView也是负责描述视图,而UIView的layer则负责渲染操作,这是二者的区别。了解Widget在应用程序启动时,main方法接收一个Widget当做主页面,所以任何一个Widget都可以当做根视图。一般都是传一个MaterialApp,也可以传一个Container当做根视图,这都是被允许的。在Flutter应用中,和界面显示及用户交互的对象都是由Widget构成的,例如视图、动画、手势等。Widget分为StatelessWidget和StatefulWidget两种,分别是无状态和有状态的Widget。StatefulWidget本质上也是无状态的,其通过State来处理Widget的状态,以达到有状态,State出现在整个StatefulWidget的生命周期中。当构建一个Widget时,可以通过其build获得构建流程,在构建流程中可以加入自己的定制操作,例如对其设置title或视图等。return Scaffold( appBar: AppBar( title: Text(‘ListView Demo’), ), body: ListView.builder( itemCount: dataList.length, itemBuilder: (BuildContext context, int index) { return Text(dataList[index]); }, ),);有些Widget在构建时,也提供一些参数来帮助构建,例如构建一个ListView时,会将index返回给build方法,来区别构建的Cell,以及构建的上下文context。itemBuilder: (BuildContext context, int index) { return Text(dataList[index]);}StatelessWidgetStatelessWidget是一种静态Widget,即创建后自身就不能再进行改变。在创建一个StatelessWidget后,需要重写build函数。每个静态Widget都会有一个build函数,在创建视图对象时会调用此方法。同样的,此函数也接收一个Widget类型的返回值。class RectangleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Center ( // UI Code ); }}StatefulWidgetWidget本质上是不可被改变的,但StatefulWidget将状态拆分到State中去管理,当数据发生改变时由State去处理视图的改变。下面是创建一个动态Widget,当创建一个动态Widget需要配合一个State,并且需要重写createState方法。重写此函数后,指定一个Widget对应的State并初始化。下面例子中,在StatefulWidget的父类中包含一个Key类型的key变量,这是无论静态Widget还是动态Widget都具备的参数。在动态Widget中定义了自己的成员变量title,并在自定义的初始化方法中传入,通过下面DynamicWidget类的构造方法,并不需要在内部手动进行title的赋值,title即为传入的值,是由系统完成的。class DynamicWidget extends StatefulWidget { DynamicWidget({Key key, this.title}) : super (key : key); final String title; @override DynamicWidgetState createState() => new DynamicWidgetState();}由于上面动态Widget定义了初始化方法,在调用动态Widget时可以直接用自定义初始化方法即可。DynamicWidget(key: ‘key’, title: ’title’);StateStatefulWidget的改变是由State来完成的,State中需要重写build方法,在build中进行视图组织。StatefulWidget是一种响应式视图改变的方式,数据源和视图产生绑定关系,由数据源驱动视图的改变。改变StatefulWidget的数据源时,需要调用setState方法,并将数据源改变的操作写在里面。使用动态Widget后,是不需要我们手动去刷新视图的。系统在setState方法调用后,会重新调用对应Widget的build方法,重新绘制某个Widget。下面的代码示例中添加了一个float按钮,并给按钮设置了一个回调函数_onPressAction,这样在每次触发按钮事件时都会调用此函数。counter是一个整型变量并和Text相关联,当counter的值在setState方法中改变时,Text Widget也会跟着变化。class DynamicWidgetState extends State<DynamicWidget> { int counter = 0; void _onPressAction() { setState(() { counter++; }); } @override Widget build(BuildContext context) { return new Scaffold( body: Center( child: Text(‘Button tapped $_counter.’) ), floatingActionButton: FloatingActionButton( onPressed: _onPressAction, tooltip: ‘Increment’, child: Icon(Icons.add) ) ); } }主要Widget在iOS中有UINavigationController的概念,其并不负责显示,而是负责控制各个页面的跳转操作。在Flutter中可以将MaterialApp理解为iOS的导航控制器,其包含一个navigationBar以及导航栈,这和iOS是一样的。在iOS中除了用来显示的视图外,视图还有对应的UIViewController。在Flutter中并没有专门用来管理视图并且和View一对一的类,但从显示的角度来说,有类似的类Scaffold,其包含控制器的appBar,也可以通过body设置一个widget当做其视图。themetheme是Flutter提供的界面风格API,MaterialApp提供有theme属性,可以在MaterialApp中设置全局样式,这样可以统一整个应用的风格。new MaterialApp( title: title, theme: new ThemeData( brightness: Brightness.dark, primaryColor: Colors.lightBlue[800], accentColor: Colors.cyan[600], ));如果不想使用系统默认主题,可以将对应的控件或试图用Theme包起来,并将Theme当做Widget赋值给其他Widget。new Theme( data: new ThemeData( accentColor: Colors.yellow, ), child: new FloatingActionButton( onPressed: () {}, child: new Icon(Icons.add), ),);有时MaterialApp设定的统一风格,并不能满足某个Widget的要求,可能还需要有其他的外观变化,可以通过Theme.of传入当前的BuildContext,来对theme进行扩展。Flutter会根据传入的context,顺着Widget树查找最近的Theme,并对Theme复制一份防止影响原有的Theme,并对其进行扩展。new Theme( data: Theme.of(context).copyWith(accentColor: Colors.yellow), child: new FloatingActionButton( onPressed: null, child: new Icon(Icons.add), ),);网络请求Flutter中可以通过async、await组合使用,进行网络请求。Flutter中的网络请求大体有三种:系统自带的HttpClient网络请求,缺点是代码量相对而言比较多,而且对post请求支持不是很好。三方库http.dart,请求简单。三方库dio,请求简单。http网络库http网络库定义在http.dart中,内部代码定义很全,包括HttpStatus、HttpHeaders、Cookie等很多基础信息,有助于我们了解http请求协议。因为是三方库,所以需要在pubspec.yaml中加入下面的引用。http: ‘>=0.11.3+12’下面是http.dart的请求示例代码,可以看到请求很简单,真正的请求代码其实就两行。生成一个Client请求对象,调用client实例的get方法(如果是post则调用post方法),并用Response对象去接收请求结果即可。通过async修饰发起请求的方法,表示这是一个异步操作,并在请求代码的前面加入await,修饰这里的代码需要等待数据返回,需要过一段时间后再处理。请求回来的数据默认是json字符串,需要对其进行decode并解析为数据对象才可以使用,这里使用系统自带的convert库进行解析,并解析为数组。import ‘package:http/http.dart’ as http;class RequestDemoState extends State<MyHomePage> { List dataList = []; @override void initState() { super.initState(); loadData(); } // 发起网络请求 loadData() async{ String requestURL = ‘https://jsonplaceholder.typicode.com/posts'; Client client = Client(); Response response = await client.get(requestURL); String jsonString = response.body; setState(() { // 数据解析 dataList = json.decode(jsonString); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title) ), body: ListView.builder( itemCount: dataList.length, itemBuilder: (BuildContext context, int index) { return Text(dataList[index][’title’]); }, ), ); }}在调用Client进行post数据请求时,需要传入一个字典进去,Client会通过将字典当做post的from表单。 void requestData() async { var params = Map<String, String>(); params[“username”] = “lxz”; params[“password”] = “123456”; var client = http.Client(); var response = await client.post(url_post, body: params); _content = response.body;}dio网络库dio库的调用方式和http库类似,这里不过多介绍。dio库相对于http库强大的在于,dio库提供了更好的Cookie管理、文件的上传下载、fromData表单等处理。所以,如果对网络库需求比较复杂的话,还是建议使用dio。// 引入外部依赖dio: ^1.0.9数据解析convert系统自带有convert解析库,在使用时直接import即可。convert类似于iOS自带的JSON解析类NSJSONSerialization,可以直接将json字符串解析为字典或数组。import ‘dart:convert’;// 解析代码dataList = json.decode(jsonString);但是,我们在项目中使用时,一般都不会直接使用字典取值,这是一种很不好的做法。一般都会将字典或数组转换为模型对象,在项目中使用模型对象。可以定义类似Model.dart这样的模型类,并在模型类中进行数据解析,对外直接暴露公共变量来让外界获取值。自动序列化但如果定义模型类的话,一个是要在代码内部写取值和赋值代码,这些都需要手动完成。另外如果当服务端字段发生改变后,客户端也需要跟着进行改变,所以这种方式并不是很灵活。可以采用json序列化的三方库json_serializable,此库可以将一个类标示为自动JSON序列化的类,并对类提供JSON和对象相互转换的能力。也可以通过命令行开启一个watch,当类中的变量定义发生改变时,相关代码自动发生改变。首先引入下面的三个库,其中包括依赖库一个,以及调试库两个。dependencies: json_annotation: ^2.0.0dev_dependencies: build_runner: ^1.0.0 json_serializable: ^2.0.0定义一个模型文件,例如这里叫做User.dart文件,并在内部定义一个User的模型类。随后引入json_annotation的依赖,通过@JsonSerializable()标示此类需要被json_serializable进行合成。定义的User类包含两部分,实例变量和两个转换函数。在下面定义json转换函数时,需要注意函数命名一定要按照下面格式命名,否则不能正常生成user.g.dart文件。import ‘package:json_annotation/json_annotation.dart’;// 定义合成后的新文件为user.g.dartpart ‘user.g.dart’;@JsonSerializable()class User { String name; int age; String email; factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); Map<String, dynamic> toJson() => _$UserToJson(this);}下面就是user.dart指定生成的user.g.dart文件,其中包含JSON和对象相互转换的代码。part of ‘user.dart’;User _$UserFromJson(Map<String, dynamic> json) { return User( json[’name’] as String, json[‘age’] as int, json[’email’] as String);}Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{ ’name’: instance.name, ‘age’: instance.age, ’email’: instance.email };有的时候服务端返回的参数名和本地的关键字冲突,或者命名不规范,导致本地定义和服务器字段不同的情况。这种情况可以通过@JsonKey关键字,来修饰json字段匹配新的本地变量。除此之外,也可以做其他修饰,例如变量不能为空等。@JsonKey(name: ‘id’)final int user_id;现在项目中依然是报错的,随后我们在flutter工程的根目录文件夹下,运行下面命令。flutter packages pub run build_runner watch此命令的好处在于,其会在后台监听模型类的定义,当模型类定义发生改变后,会自动修改本地源码以适配新的定义。以文中User类为例,当User.dart文件发生改变后,使用Cmd+s保存文件,随后VSCode会将自定改变user.g.dart文件的定义,以适配新的变量定义。系统文件主要文件iOS文件:iOS工程文件Android:Android工程文件lib:Flutter的dart代码assets:资源文件夹,例如font、image等都可以放在里面.gitignore:git忽略文件packages这是一个系统文件,Flutter通过.packages文件来管理一些系统依赖库,例如material、cupertino、widgets、animation、gesture等系统库就在里面,这些主要的系统库由.packages下的flutter统一管理,源码都在flutter/lib/scr目录下。除此之外,还有一些其他的系统库或系统资源都在.packages中。yaml文件在Flutter中通过pubspec.yaml文件来管理外部引用,包含本地资源文件、字体文件、依赖库等依赖,以及应用的一些配置信息。这些配置在项目中时,需要注意代码对其的问题,否则会导致加载失败。当修改yaml文件的依赖信息后,需要执行flutter get packages命令更新本地文件。但并不需要这么麻烦,可以直接Cmd+s保存文件,VSCode编译器会自动更新依赖。// 项目配置信息name: WeChatdescription: Tencent WeChat App.version: 1.0.0+1// 常规依赖dependencies: flutter:125864 sdk: flutter cupertino_icons: ^0.1.2 english_words: ^3.1.0// 开发依赖dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true // 图片依赖 assets: - assets/images/ic_file_transfer.png - assets/images/ic_fengchao.png // 字体依赖 fonts: - family: appIconFont fonts: - asset: assets/fonts/iconfont.ttfFlutter开发启动函数和大多数编程语言一样,dart也包含一个main方法,是Flutter程序执行的主入口,在main方法中写的代码就是在程序启动时执行的代码。main方法中会执行runApp方法,runApp方法类似于iOS的UIApplicationMain方法,runApp函数接收一个Widget用来做应用程序的显示。void main() { runApp() // code}生命周期在iOS中通过AppDelegate可以获取应用程序的生命周期回调,在Flutter中也可以获取到。可以通过向Binding添加一个Observer,并实现didChangeAppLifecycleState方法,来监听指定事件的到来。但是由于Flutter提供的状态有限,在iOS平台只能监听三种状态,下面是示例代码。class LifeCycleDemoState extends State<MyHomePage> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); switch (state) { case AppLifecycleState.inactive: print(‘Application Lifecycle inactive’); break; case AppLifecycleState.paused: print(‘Application Lifecycle paused’); break; case AppLifecycleState.resumed: print(‘Application Lifecycle resumed’); break; default: print(‘Application Lifecycle other’); } }}矩阵变换在Flutter中是支持矩阵变化的,例如rotate、scale等方式。Flutter的矩阵变换由Widget完成,需要进行矩阵变换的视图,在外面包一层Transform Widget即可,内部可以设置其变换方式。child: Container( child: Transform( child: Container( child: Text( “Lorem ipsum”, style: TextStyle(color: Colors.orange[300], fontSize: 12.0), textAlign: TextAlign.center, ), decoration: BoxDecoration( color: Colors.red[400], ), padding: EdgeInsets.all(16.0), ), alignment: Alignment.center, transform: Matrix4.identity() ..rotateZ(15 * 3.1415927 / 180), ), width: 320.0, height: 240.0, color: Colors.grey[300],)在Transform中可以通过transform指定其矩阵变换方式,通过alignment指定变换的锚点。页面导航在iOS中可以通过UINavigationController对页面进行管理,控制页面间的push、pop跳转。Flutter中使用Navigator和Routers来实现类似UINavigationController的功能,Navigator负责管理导航栈,包含push、pop的操作,可以把UIViewController看做一个Routers,Routers被Navigator管理着。Navigator的跳转方式分为两种,一种是直接跳转到某个Widget页面,另一种是为MaterialApp构建一个map,通过key来跳转对应的Widget页面。map的格式是key : context的形式。void main() { runApp(MaterialApp( home: MyAppHome(), // becomes the route named ‘/’ routes: <String, WidgetBuilder> { ‘/a’: (BuildContext context) => MyPage(title: ‘page A’), ‘/b’: (BuildContext context) => MyPage(title: ‘page B’), ‘/c’: (BuildContext context) => MyPage(title: ‘page C’), }, ));}跳转时通过pushNamed指定map中的key,即可跳转到对应的Widget。如果需要从push出来的页面获取参数,可以通过await修饰push操作,这样即可在新页面pop的时候将参数返回到当前页面。Navigator.of(context).pushNamed(’/b’);Map coordinates = await Navigator.of(context).pushNamed(’/location’);Navigator.of(context).pop({“lat”:43.821757,“long”:-79.226392});编码规范VSCode有很好的语法检查,如果有命名不规范等问题,都会以警告的形式表现出来。驼峰命名法,方法名、变量名等,都以首字母小写的驼峰命名法。类名也是驼峰命名法,但类名首字母大写。文件名,文件命名以下划线进行区分,不使用驼峰命名法。Flutter中创建Widget对象,可以用new修饰,也可以不用。child: new Container( child: Text( ‘Hello World’, style: TextStyle(color: Colors.orange, fontSize: 15.0) ))函数中可以定义可选参数,以及必要参数。下面是一个函数定义,这里定义了一个必要参数url,以及一个Map类型的可选参数headers。Future<Response> get(url, {Map<String, String> headers});Dart中在函数定义前加下划线,则表示是私有方法或变量。Dart通过import引入外部引用,除此之外也可以通过下面的语法单独引入文件中的某部分。import “dart:collection” show HashMap, IterableBase;=>调用在Dart中经常可以看到=>的调用方式,这种调用方式类似于一种语法糖,下面是一些常用的调用方式。当进行函数调用时,可以将普通函数调用转换为=>的调用方式,例如下面第一个示例。在此基础上,如果调用函数只有一个参数,可以将其改为第二个示例的方式,也就是可以省略调用的括号,直接写参数名。(单一参数) => {函数声明}elements.map((element) => { return element.length;});单一参数 => {函数声明}elements.map(element => { return element.length;});当只有一个返回值,并且没有逻辑处理时,可以直接省略return,返回数值。(参数1, 参数2, …, 参数N) => 表达式elements.map(element => element.length);当调用的函数中没有参数时,可以直接省略参数,写一对空括号即可。() => {函数实现}小技巧代码重构VSCode支持对Dart语言进行重构,一般作用范围都是在函数内小范围的。例如在创建Widget对象的地方,将鼠标焦点放在这里,当前行的最前面会有提示。点击提示后会有下面两个选项:Extract Local Variable 将当前Widget及其子Widget创建的代码,剥离到一个变量中,并在当前位置使用这个变量。Extract Method 将当前Widget及其子Widget创建的代码,封装到一个函数中,并在当前位置调用此函数。除此之外,将鼠标焦点放在方法的一行,点击最前面的提示,会出现下面两个选项:Convert to expression body 将当前函数转换为一个表达式。Convert to async function body 将当前函数转换为一个异步线程中执行的代码。附加效果在Dart中添加任何附加效果,例如动画效果或矩阵转换,除了直接给Widget子类的属性赋值外,就是在被当前Widget外面包一层,就可以使当前Widget拥有对应的效果。// 动画效果floatingActionButton: FloatingActionButton( tooltip: ‘Fade’, child: Icon(Icons.brush), onPressed: () { controller.forward(); },),// 矩阵转换Transform( child: Container( child: Text( “Lorem ipsum”, style: TextStyle(color: Colors.orange[300], fontSize: 12.0), textAlign: TextAlign.center, ) ), alignment: Alignment.center, transform: Matrix4.identity() ..rotateZ(15 * 3.1415927 / 180),),快捷键(VSCode)Cmd + Shift + p:可以进行快速搜索。需要注意的是,默认是带有一个>的,这样搜索结果主要是dart代码。如果想搜索其他配置文件,或者安装插件等操作,需要把>去掉。Cmd + Shift + o:可以在某个文件中搜索某个类,但前提是需要提前进入这个文件。例如进入framework.dart,搜索StatefulWidget类。注意点使用Flutter要注意代码缩进,如果缩进有问题可能会影响最后的结果,尤其是在.yaml中写配置文件的时候。因为Flutter是开源的,所以遇到问题后可以进入源码中,找解决方案。在代码中要注意标点符号的使用,例如第二个创建Stack的代码,如果上面是以逗号结尾,则后面的创建会失败,如果上面是以分号结尾则没问题。Widget unreadMsgText = Container( width: Constants.UnreadMsgNotifyDotSize, height: Constants.UnreadMsgNotifyDotSize, child: Text( conversation.unreadMsgCount.toString(), style: TextStyle( color: Color(AppColors.UnreadMsgNotifyTextColor), fontSize: 12.0 ), ), ); avatarContainer = Stack( overflow: Overflow.visible, children: <Widget>[ avatar ], ); ...

April 5, 2019 · 4 min · jiezi

打通前后端逻辑,客户端Flutter代码一天上线

一、前沿 随着闲鱼的业务快速增长,运营类的需求也越来越多,其中不乏有很多界面修改或运营坑位的需求。闲鱼的版本现在是每2周一个版本,如何快速迭代产品,跳过窗口期来满足这些需求?另外,闲鱼客户端的包体也变的很大,企业包的大小,iOS已经到了94.3M,Android也到了53.5M。Android的包体大小,相比2016年,已经增长了近1倍,怎么能将包体大小降下来?首先想到的是如何动态化的解决此类问题。 对于原生的能力的动态化,Android平台各公司都有很完善的动态化方案,甚至Google还提供了Android App Bundles让开发者们更好地支持动态化。由于Apple官方担忧动态化的风险,因此并不太支持动态化。因此动态化能力就会考虑跟Web结合,从一开始基于 WebView 的 Hybrid 方案 PhoneGap、Titanium,到现在与原生相结合的 React Native 、Weex。 但Native和JavaScript Context之间的通讯,频繁的交互就成了程序的性能瓶颈。于此同时随着闲鱼Flutter技术的推广,已经有10多个页面用Flutter实现,上面提到的几种方式都不适合Flutter场景,如何解决这个问题Flutter的动态化的问题?二、动态方案我们最初调研了Google的动态化方案CodePush。2.1 CodePush CodePush是谷歌官方推出的动态化方案,目前只有在Android上面实现了。Dart VM在执行的时候,加载isolate_snapshot_data 和isolate_snapshot_instr 2个文件,通过动态更改这些文件,就达到动态更新的目的。官方的Flutter源码当中,已经有相关的提交来做动态更新的内容,具体内容可以参考 ResourceExtractor.java。 根据官方给出的Guide,我们这边也做了相关的测试,patch的包体大小会很大(939kb)。为了降低包体大小,还可以通过增量的修改snapshot文件的方式来更新。通过bsdiff生成的snapshot的差异文件,2个文件分别可以缩小到48kb和870kb。 目前看来,CodePush还不能做到很好的工程化。而且如何管理patch文件,需要制定baseline和patch文件的规则。2.2 动态模板 动态模板,就是通过定义一套DSL,在端侧解析动态的创建View来实现动态化,比如LuaViewSDK、Tangram-iOS和Tangram-Android。这些方案都是创建的Native的View,如果想在Flutter里面实现,需要创建Texture来桥接;Native端渲染完成之后,再将纹理贴在Flutter的容器里面,实现成本很高,性能也有待商榷,不适合闲鱼的场景。 所以我们提出了闲鱼自己的Flutter动态化方案,前面已经有同事介绍过方案的原理:《做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现》,下面看下具体的实现细节。三、模板编译自定义一套DSL,维护成本较高,怎么能不自定义DSL来实现模板下发?闲鱼的方案就是直接将Dart文件转化成模板,这样模板文件也可以快速沉淀到端侧。3.1 模板规范 先来看下一个完整的模板文件,以新版我的页面为例,这个是一个列表结构,每个区块都是一个独立的Widget,现在我们期望将“卖在闲鱼”这个区块动态渲染,对这个区块拆分之后,需要3个子控件:头部、菜单栏、提示栏;因为这3部分界面有些逻辑处理,所以先把他们的逻辑内置。内置的子控件分别是MenuTitleWidget、MenuItemWidget和HintItemWidget,编写的模板如下:@overrideWidget build(BuildContext context) { return new Container( child: new Column( children: <Widget>[ new MenuTitleWidget(data), // 头部 new Column( // 菜单栏 children: <Widget>[ new Row( children: <Widget>[ new MenuItemWidget(data.menus[0]), new MenuItemWidget(data.menus[1]), new MenuItemWidget(data.menus[2]), ], ) ], ), new Container( // 提示栏 child: new HintItemWidget(data.hints[0])), ], ), );}中间省略了样式描述,可以看到写模板文件就跟普通的widget写法一样,但是有几点要注意:每个Widget都需要用new或const来修饰数据访问以data开头,数组形式以[]访问,字典形式以.访问 模板写好之后,就要考虑怎么在端上渲染,早期版本是直接在端侧解析文件,但是考虑到性能和稳定性,还是放在前期先编译好,然后下发到端侧。3.2 编译流程 编译模板就要用到Dart的Analyzer库,通过parseCompilationUnit函数直接将Dart源码解析成为以CompilationUnit为Root节点的AST树中,它包含了Dart源文件的语法和语义信息。接下来的目标就是将CompilationUnit转换成为一个JSON格式。 上面的模板解析出来build函数孩子节点是ReturnStatementImpl,它又包含了一个子节点InstanceCreationExpressionImpl,对应模板里面的new Container(…),它的孩子节点中,我们最关心的就是ConstructorNameImpl和ArgumentListImpl节点。ConstructorNameImpl标识创建节点的名称,ArgumentListImpl标识创建参数,参数包含了参数列表和变量参数。定义如下结构体,来存储这些信息:class ConstructorNode { // 创建节点的名称 String constructorName; // 参数列表 List<dynamic> argumentsList = <dynamic>[]; // 变量参数 Map<String, dynamic> arguments = <String, dynamic>{};}递归遍历整棵树,就可以得到一个ConstructorNode树,以下代码是解析单个Node的参数:ArgumentList argumentList = astNode;for (Expression exp in argumentList.arguments) { if (exp is NamedExpression) { NamedExpression namedExp = exp; final String name = ASTUtils.getNodeString(namedExp.name); if (name == ‘children’) { continue; } /// 是函数 if (namedExp.expression is FunctionExpression) { currentNode.arguments[name] = FunctionExpressionParser.parse(namedExp.expression); } else { /// 不是函数 currentNode.arguments[name] = ASTUtils.getNodeString(namedExp.expression); } } else if (exp is PropertyAccess) { PropertyAccess propertyAccess = exp; final String name = ASTUtils.getNodeString(propertyAccess); currentNode.argumentsList.add(name); } else if (exp is StringInterpolation) { StringInterpolation stringInterpolation = exp; final String name = ASTUtils.getNodeString(stringInterpolation); currentNode.argumentsList.add(name); } else if (exp is IntegerLiteral) { final IntegerLiteral integerLiteral = exp; currentNode.argumentsList.add(integerLiteral.value); } else { final String name = ASTUtils.getNodeString(exp); currentNode.argumentsList.add(name); }}端侧拿到这个ConstructorNode节点树之后,就可以根据Widget的名称和参数,来生成一棵Widget树。四、渲染引擎端侧拿到编译好的模板JSON后,就是解析模板并创建Widget。先看下,整个工程的框架和工作流:工作流程:开发人员编写dart文件,编译上传到CDN端侧拿到模板列表,并在端侧存库业务方直接下发对应的模板id和模板数据Flutter侧再通过桥接获取到模板,并创建Widget树对于Native测,主要负责模板的管理,通过桥接输出到Flutter侧。4.1 模板获取模板获取分为2部分,Native部分和Flutter部分;Native主要负责模板的管理,包括下载、降级、缓存等。程序启动的时候,会先获取模板列表,业务方需要自己实现,Native层获取到模板列表会先存储在本地数据库中。Flutter侧业务代码用到模板的时候,再通过桥接获取模板信息,就是我们前面提到的JSON格式的信息,Flutter也会有缓存,已减少Flutter和Native的交互。4.2 Widget创建Flutter侧当拿到JSON格式的,先解析出ConstructorNode树,然后递归创建Widget。创建每个Widget的过程,就是解析节点中的argumentsList和arguments 并做数据绑定。例如,创建HintItemWidget需要传入提示的数据内容,new HintItemWidget(data.hints[0]),在解析argumentsList时,会通过key-path的方式从原始数据中解析出特定的值。解析出来的值都会存储在WidgetCreateParam里面,当递归遍历每个创建节点,每个widget都可以从WidgetCreateParam里面解析出需要的参数。/// 构建widget用的参数class WidgetCreateParam { String constructorName; /// 构建的名称 dynamic context; /// 构建的上下文 Map<String, dynamic> arguments = <String, dynamic>{}; /// 字典参数 List<dynamic> argumentsList = <dynamic>[]; /// 列表参数 dynamic data; /// 原始数据} 通过以上的逻辑,就可以将ConstructorNode树转换为一棵Widget树,再交给Flutter Framework去渲染。至此,我们已经能将模板解析出来,并渲染到界面上,交互事件应该怎么处理?4.3 事件处理在写交互的时候,一般都会通过GestureDector、InkWell等来处理点击事件。交互事件怎么做动态化? 以InkWell组件为例,定义它的onTap函数为openURL(data.hints[0].href, data.hints[0].params)。在创建InkWell时,会以OpenURL作为事件ID,查找对应的处理函数,当用户点击的时候,会解析出对应的参数列表并传递过去,代码如下:…final List<dynamic> tList = <dynamic>[];// 解析出参数列表exp.argumentsList.forEach((dynamic arg) { if (arg is String) { final dynamic value = valueFromPath(arg, param.data); if (value != null) { tList.add(value); } else { tList.add(arg); } } else { tList.add(arg); }});// 找到对应的处理函数final dynamic handler = TeslaEventManager.sharedInstance().eventHandler(exp.actionName);if (handler != null) { handler(tList);}…五、 效果新版我的页面添加了动态化渲染能力之后,如果有需求新添加一种组件类型,就可以直接编译发布模板,服务端下发新的数据内容,就可以渲染出来了;动态化能力有了,大家会关心渲染性能怎么样。5.1 帧率在加了动态加载逻辑之后,已经开放了2个动态卡片,下图是新版本我的页面近半个月的的帧率数据:从上图可以看到,帧率并没有降低,基本保持在55-60帧左右,后续可以多添加动态的卡片,观察下效果。注:因为我的页面会有本地的一些业务判断,从其他页面回到我的tab,都会刷新界面,所以帧率会有损耗。 从实现上分析,因为每个卡片,都需要遍历ConstructorNode树来创建,而且每个构建都需要解析出里面的参数,这块可以做一些优化,比如缓存相同的Widget,只需要映射出数据内容并做数据绑定。5.2 失败率现在监控了渲染的逻辑,如果本地没有对应的Widget创建函数,会主动抛Error。监控数据显示,渲染的流程中,还没有异常的情况,后续还需要对桥接层和native层加错误埋点。六、展望 基于Flutter动态模板,之前需要走发版的Flutter需求,都可以来动态化更改。而且以上逻辑都是基于Flutter原生的体系,学习和维护成本都很低,动态的代码也可以快速的沉淀到端侧。 另外,闲鱼正在研究UI2Code的黑科技,不了解的老铁,可以参考闲鱼大神的这篇文章《重磅系列文章!UI2CODE智能生成Flutter代码——整体设计篇》。可以设想下,如果有个需求,需要动态的显示一个组件,UED出了视觉稿,通过UI2Code转换成Dart文件,再通过这个系统转换成动态模板,下发到端侧就可以直接渲染出来,程序员都不需要写代码了,做到自动化运营,看来以后程序员失业也不是没有可能了。 基于Flutter的Widget,还可以拓展更多个性化的组件,比如内置动画组件,就可以动态化下发动画了,更多好玩的东西等待大家来一起探索。参考文献https://github.com/flutter/flutter/issues/14330https://www.dartlang.org/https://mp.weixin.qq.com/s/4s6MaiuW4VoHr_7f0S_vuQhttps://github.com/flutter/engine本文作者:闲鱼技术-景松阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 4, 2019 · 2 min · jiezi

Flutter中 TTS(播放文本功能)的使用

请支持原文:http://tryenough.com/flutter-tts需求在flutter中指定一段文字,播放语音。实现1.添加库引用我们这里使用Dart的 tts库,首先在配置文件中添加这个库的引用:在pubspec.yaml文件中添加如下代码引用:dependencies: tts: ^1.0.2执行命令,获取该库:flutter packages get请支持原文:http://tryenough.com/flutter-tts使用时引入头文件:import ‘package:tts/tts.dart’;2.创建tts_helper类,作为使用tts的帮助类import ‘package:tts/tts.dart’;import ‘dart:async’;import ‘dart:io’;/// Singleton tool class for tts/// Use TtsHelper step:////// Method : #isLanguageAvailable judge language, here language is _languageMap’s values like “en-US”,instead of the type of ’en’ etc..////// Method : #getTtsLanguage help you convert “en” to “en-US”.////// Method : #setLanguage help you set Language , but “en-US” is default value////// use example:/// TtsHelper.instance.speak(“speech content”);/// or/// TtsHelper.instance.setLanguageAndSpeak(“speech content”, “en-US”);/// …class TtsHelper { // Locale to tss language map static final Map<String, String> _languageMap = { ’en’: “en-US”, ‘zh’: “zh-CN”, “ar”: “ar-SA”, “cs”: “cs-CZ”, “da”: “da-DK”, “de”: “de-DE”, “el”: “el-GR”, “es”: “es-ES”, “fi”: “fi-FI”, “fr”: “fr-CA”, “he”: “he-IL”, “hi”: “hi-IN”, “hu”: “hu-HU”, “id”: “id-ID”, “it”: “it-IT”, “ja”: “ja-JP”, “ko”: “ko-KR”, “nl”: “nl-BE”, “no”: “no-NO”, “pl”: “pl-PL”, “pt”: “pt-BR”, “ro”: “ro-RO”, “ru”: “ru-RU”, “sk”: “sk-SK”, “sv”: “sv-SE”, “th”: “th-TH”, “tr”: “tr-TR”, ’en-US’: “en-US”, ‘zh-CN’: “zh-CN”, “ar-SA”: “ar-SA”, “cs-CZ”: “cs-CZ”, “da-DK”: “da-DK”, “de-DE”: “de-DE”, “el-GR”: “el-GR”, “es-ES”: “es-ES”, “fi-FI”: “fi-FI”, “fr-CA”: “fr-CA”, “he-IL”: “he-IL”, “hi-IN”: “hi-IN”, “hu-HU”: “hu-HU”, “id-ID”: “id-ID”, “it-IT”: “it-IT”, “ja-JP”: “ja-JP”, “ko-KR”: “ko-KR”, “nl-BE”: “nl-BE”, “no-NO”: “no-NO”, “pl-PL”: “pl-PL”, “pt-BR”: “pt-BR”, “ro-RO”: “ro-RO”, “ru-RU”: “ru-RU”, “sk-SK”: “sk-SK”, “sv-SE”: “sv-SE”, “th-TH”: “th-TH”, “tr-TR”: “tr-TR”, }; static final String _defaultL = “en-US”; List<String> _languages; static TtsHelper _instance; static TtsHelper get instance => _getInstance(); factory TtsHelper() =>_getInstance(); static TtsHelper _getInstance() { if (_instance == null) { _instance = new TtsHelper._internal(); } return _instance; } TtsHelper._internal() { // Initialize _initPlatformState(); } _initPlatformState() async { _languages = await Tts.getAvailableLanguages(); // If getAvailableLanguages is null, add “en-US” to _languages. if (_languages == null) { _languages = [_defaultL]; } // Default set en-US language _setLanguage(_defaultL); } String _getTtsLanguage(String localeStr) { if(localeStr == null || localeStr.isEmpty || !_languageMap.containsKey(localeStr)) { return _defaultL; } return _languageMap[localeStr]; } // Return whether the result if set language is successful Future<bool> _setLanguage (String lang) async { String language = _getTtsLanguage(lang); if (language == null || language.isEmpty) { language = _defaultL; } if(Platform.isIOS && !_languages.contains(language)) { return false; } final bool isSet = await Tts.setLanguage(language); return isSet; } // Returns whether the supported language is supported Future<bool> _isLanguageAvailable (String language) async { final bool isSupport = await Tts.isLanguageAvailable(language); return isSupport; } void speak (String text) async { if (text == null || text.isEmpty) { return;} Tts.speak(text); } void setLanguageAndSpeak(String text, String language) async { String ttsL = _getTtsLanguage(language); var setResult = await _setLanguage(ttsL); if(setResult != null) { var available = await _isLanguageAvailable(ttsL); if(available != null) { speak(text); } } }}请支持原文:http://tryenough.com/flutter-tts3.添加测试页面import ‘package:flutter_tts/tts_helper.dart’;import ‘package:flutter/material.dart’;class VoiceSetPage extends StatefulWidget { VoiceSetPage({Key key, this.title}) : super(key: key); final String title; @override _VoiceSetPageState createState() => _VoiceSetPageState();}class VoiceSetPageState extends State<VoiceSetPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blue, title: Text(widget.title), elevation: 5.0, // shadow the bottom of AppBar ), body: Center( child: ListView( children: <Widget>[ ListTile( title: Text( ’test vioce’, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0), ), onTap: () { showAlertDialog(context); TtsHelper.instance.setLanguageAndSpeak(“你好我是声音播放器”, “zh”); }, ), Divider( height: 1, ) ], ), ), ); }}void showAlertDialog(BuildContext context) { NavigatorState navigator = context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>()); debugPrint(“navigator is null?” + (navigator == null).toString()); showDialog( context: context, builder: () => new AlertDialog( title: new Text(“Dialog Title”), content: new Text(“This is my content”), actions: <Widget>[ new FlatButton( child: new Text(“CANCEL”), onPressed: () { Navigator.of(context).pop(); }, ), new FlatButton( child: new Text(“OK”), onPressed: () { Navigator.of(context).pop(); }, ) ]));}4.在main中添加测试入口import ‘package:flutter/material.dart’;import ‘package:flutter_tts/voice_set_page.dart’;void main() => runApp(MyApp());class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: ‘Flutter Demo’, theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: ‘Flutter Demo Home Page’), ); }}class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> { void _incrementCounter() { Navigator.push(context, MaterialPageRoute(builder: (context) => VoiceSetPage(title: “Setting”))); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( ‘点击浮动按钮跳转到语音测试页’, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: ‘跳转’, child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); }}测试可用,希望能帮助到你。请支持原文:http://tryenough.com/flutter-tts完成demo下载地址:http://tryenough.com/flutter-tts ...

April 4, 2019 · 4 min · jiezi

Flutter自定义实现神奇的卡片切换视图

前言这一段时间,Flutter的势头是越来越猛了,作为一个Android程序猿,我自然也是想要赶紧尝试一把。在学习到动画的这部分后,为了加深对Flutter动画实现的理解,我决定把之前写的一个卡片切换效果的开源小项目,用Flutter“翻译”一遍。废话不多说,先来看看效果吧:AndroidiOSGithub地址:https://github.com/BakerJQ/Fl…思路首先,关于卡片的层叠效果,在原Android项目中,是通过Scale差异以及TranslationY来体现的,Flutter可以继续采用这种方式。其次,对于自定义卡片的内容,原Android项目是通过Adapter实现,对于Flutter,则可以采用IndexedWidgetBuilder实现。最后,就是自定义动效的实现,原Android项目是通过一个0到1的ValueAnimator来定义动画的展示过程,而Flutter中,正好有与之对应的Animation和AnimationController,如此我们就可以直接自定义一个动画过程中,具体的视图展示方式。组件总览由于卡片视图需要根据动画情况进行渲染,所以显然是一个StatefulWidget。同时,我们给出三种基本的动画模式:enum AnimType { TO_FRONT,//被选中的卡片通过自定义动效移至第一,其他的卡片通过通用动效补位 SWITCH,//选中的卡片和第一张卡片互换位置,并都是自定义动效 TO_END,//第一张图片通过自定义动效移至最后,其他卡片通过通用动效补位}并通过Helper和Controller来处理所有的动画逻辑其中Controller由构造方法传入InfiniteCards({ @required this.controller, this.width, this.height, this.background,});Helper在initState中进行构建,并初始化,同时将Helper绑定给Controller:@overridevoid initState() { … _helper = AnimHelper( controller: widget.controller, //传入动画更新监听,动画时调用setState进行实时渲染 listenerForSetState: () { setState(() {}); }); _helper.init(this, context); if (widget.controller != null) { widget.controller.animHelper = _helper; }}而build过程中,则通过Helper返回具体的Widget列表,而Stack则是为了实现层叠效果。Widget build(BuildContext context) { … return Container( … child: Stack( children: _helper.getCardList(_width, _height), ), );}如此,基本的初始化等操作就算是完成了。下面我们来看看Controller和Helper都是怎么工作的。Controller我们先来看看Controller所包含的内容:class InfiniteCardsController { //卡片构造器 IndexedWidgetBuilder _itemBuilder; //卡片个数 int _itemCount; //动画时长 Duration _animDuration; //点击卡片是否触发切换动画 bool _clickItemToSwitch; //动画Transform AnimTransform _transformToFront,_transformToBack,…; //排序Transform ZIndexTransform _zIndexTransformCommon,…; //动画类型 AnimType _animType; //曲线定义(类Android插值器) Curve _curve; //helper AnimHelper _animHelper; … void anim(int index) { _animHelper.anim(index); } void reset(…) { … //重设各参数 setControllerParams(); _animHelper.reset(); … }}由此可以看到,Controller基本上就是作为参数配置器和Helper的方法代理的存在。由此童鞋们肯定就知道了,对于动效的自定义和动效的触发等操作,都是通过Controller来完成,demo如下://构建Controller_controller = InfiniteCardsController( itemBuilder: _renderItem, itemCount: 5, animType: AnimType.SWITCH,);//调用reset_controller.reset( itemCount: 4, animType: AnimType.TO_FRONT, transformToBack: _customToBackTransform,);//调用展示下一张卡片动画_controller.reset(animType: AnimType.TO_END);_controller.next();关于具体的自定义,我们稍后再聊,咱们先来看看Helper。HelperHelper是整个动画效果实现的核心类,我们先看几个它所包含的核心成员:class AnimHelper { final InfiniteCardsController controller; //切换动画 AnimationController _animationController; Animation<double> _animation; //卡片列表 List<CardItem> _cardList = new List(); //需要向后切换的卡片,和需要向前切换的卡片 CardItem _cardToBack, _cardToFront; //需要向后切换的卡片位置,和需要向前切换的卡片位置 int _positionToBack, _positionToFront;}现在我们来看看,如果要触发一个切换动画,这些成员是如何相互配合的。当选中一张卡片进行切换时,这张卡片就是需要向前切换的卡片(ToFront),而第一张卡片,就是需要向后切换的卡片(ToBack)。void _cardAnim(int index, CardItem card) { //记录要切换的卡片 _cardToFront = card; _cardToBack = _cardList[0]; _positionToBack = 0; _positionToFront = index; //触发动画 _animationController.forward(from: 0.0);}由于设置了AnimationListener,在动画过程中,setState就会被调用,如此就会触发Widget的build,从而触发Helper的getCardList方法。我们来看看在切换动画的过程中,是如何返回卡片Widget列表的。List<Widget> getCardList(double width, double height) { for (int i = 0; i < controller.itemCount; i++) { … if (_isSwitchAnim) { //处理切换动画 _switchTransform(width, height, i); } … } //根据zIndex进行排序渲染 List<CardItem> copy = List.from(_cardList); copy.sort((card1, card2) { return card1.zIndex < card2.zIndex ? 1 : -1; }); return copy.map((card) { return card.transformWidget; }).toList();}如上代码所示,先进行动画处理,后根据zIndex进行排序,因为要保证在前面的后渲染。而动画是如何处理的呢,以切换到前面的卡片为例:void _toFrontTransform(double width, double height, int fromPosition, int toPosition) { CardItem cardItem = _cardList[fromPosition]; controller.zIndexTransformToFront( cardItem, _animation.value, _getCurveValue(_animation.value), width, height, fromPosition, toPosition); cardItem.transformWidget = controller.transformToFront( cardItem.widget, _animation.value, _getCurveValue(_animation.value), width, height, fromPosition, toPosition); }原来,正是在这一步,Helper通过Controller中配置的自定义动画方法,得到了卡片的Widget。由此,动画展示的基本流程就描述完了,下面我们进入最关键的部分–如何自定义动画。自定义动画我们以通用动画为例,来看看自定义动画的主要流程。首先,AnimTransform为如下方法的定义:typedef AnimTransform = Transform Function( Widget item,//卡片原始Widget double fraction,//动画执行的系数 double curveFraction,//曲线转换后的系数 double cardHeight,//整体高度 double cardWidth,//整体宽度 int fromPosition,//卡片开始位置 int toPosition);//卡片要移动到的位置该方法返回的是一个Transform,专门用于处理视图变换的Widget,而我们要做的,就是根据传入的参数,构建相应系数下的Widget。以DefaultCommonTransform为例:Transform _defaultCommonTransform(Widget item, double fraction, double curveFraction, double cardHeight, double cardWidth, int fromPosition, int toPosition) //需要跨越的卡片数量{ int positionCount = fromPosition - toPosition; //以0.8做为第一张的缩放尺寸,每向后一张缩小0.1 //(0.8 - 0.1 * fromPosition) = 当前位置的缩放尺寸 //(0.1 * fraction * positionCount) = 移动过程中需要改变的缩放尺寸 double scale = (0.8 - 0.1 * fromPosition) + (0.1 * fraction * positionCount); //在Y方向的偏移量,每向后一张,向上偏移卡片宽度的0.02 //-cardHeight * (0.8 - scale) * 0.5 对卡片做整体居中处理 double translationY = -cardHeight * (0.8 - scale) * 0.5 - cardHeight * (0.02 * fromPosition - 0.02 * fraction * positionCount); //返回缩放后,进行Y方向偏移的Widget return Transform.translate( offset: Offset(0, translationY), child: Transform.scale( scale: scale, child: item, ), );}对于向第一位移动的选中卡片,也是同理,只不过是根据该卡片对应的转换器来进行自定义动画的转换。最后的效果,就像演示图中第一次点击,图片向前翻转到第一位的效果一样。总结由于Flutter采用的是声明式的视图构建方式,在编码初期,多少会受到原生编码方式的思维影响,而觉得很难受。但是在熟悉了之后,就会发现其实很多思想都是共通的,比如Animation,比如插值器的概念等等。另外,研读源码仍然是最有效的解决问题的方式,比如相比Android中直接对ScrollView进行animateTo操作,在Flutter中需要通过ScrollController进行animateTo操作,正是这一点让我找到了在Flutter中实现InfiniteCards效果的方法。更具体的Demo请前往Github的Flutter-InfiniteCards Repo,欢迎大家star和提issue。再次贴一下Github地址:https://github.com/BakerJQ/Fl… ...

April 3, 2019 · 2 min · jiezi

推荐一款 Flutter Push 推送功能插件

又到了推荐好插件的时候了。开发 APP 避免不了使用「推送」功能。比如,新上架一个商品,或者最新的一条体育新闻,实时推送给用户。比较了几家推送平台,貌似「极光」出了 Flutter 插件,所以就拿它试试手,顺便记录下整个推送功能开发流程。说到「推送」,自然有推送端和接收端,接收端自然包括 Android 端和 iOS 端。demo引入插件:flutter_jpush: ^0.0.4在 main.dart 加入初始化代码:void _initJPush() async { await FlutterJPush.startup(); print(“初始化jpush成功”); // 获取 registrationID var registrationID =await FlutterJPush.getRegistrationID(); print(registrationID); // 注册接收和打开 Notification() _initNotification();}void _initNotification() async { FlutterJPush.addReceiveNotificationListener( (JPushNotification notification) { print(“收到推送提醒: $notification”); } ); FlutterJPush.addReceiveOpenNotificationListener( (JPushNotification notification) { print(“打开了推送提醒: $notification”); } );}Android 配置在极光后台创建应用,生成 appkey 等信息,Android 配置好说,添加包名即可。在项目 Android 工程 build.gradle 代码中,增加配置信息:defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId “com..” minSdkVersion 16 targetSdkVersion 27 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner “android.support.test.runner.AndroidJUnitRunner” manifestPlaceholders = [ JPUSH_PKGNAME : applicationId, JPUSH_APPKEY : “****”, // 极光上注册的包名对应的appkey. JPUSH_CHANNEL : “developer-default”, ] }好了,我们先在极光后台编写一条消息通知,看看效果。当 APP 处于打开状态,通过命令好 log,我们能看到「收到推送提醒」:同时,我们在通知栏上也能收到这条消息推送通知:打开这条通知后,执行的是「addReceiveOpenNotificationListener」就是这么简单。iOS 配置如何申请证书和签权,具体看极光的说明:https://docs.jiguang.cn/jpush/client/iOS/ios_cer_guide/需要注意的是,先在 Xcode 那打开「Push Notifications」在「iOS」工程下,添加极光配置信息:增加#include “FlutterJPushPlugin.h"增加[self startupJPush:launchOptions appKey:@“你的key” channel:@“你的渠道” isProduction:是否生产版本];好了,配置之后,dart 端还是上面的同样代码,还是利用极光的后台,推送一条测试通知,看看效果:打开该通知后,也执行 print 了:服务器编程推送只要消息能到达客户端,那具体怎么使用,或者打开客户端跳转到具体页面,这些工作就好说了,此处就没必要展开说了。剩下的就是后台接口推送通知了,总不能每次都要在「极光」后台做推送吧!所以我们需要借助「极光」提供的接口了。极光提供了多语言服务端 SDK,基本可以满足我们的集成需要了。我还是以 Laravel 为案例,简要说一说集成。1. composer.json 文件中添加 jpush 依赖.“jpush/jpush”: “^3.5"2. 写一个 demo 命令行推送服务:Artisan::command(‘jpush’, function () { $client = new \JPush\Client($app_key, $master_secret);})->describe(‘jpush’);3. 发送一个通知试试:$client->push() ->setPlatform(‘all’) ->addAllAudience() ->setNotificationAlert(‘你好, 极光推送’) ->send();执行命令:php artisan jpush 看看:okey,到目前为止,通过简单的例子,就可以把从服务端到客户端走通 Push 流程。注:服务端 SDK 参考https://github.com/jpush/jpush-api-php-client/blob/master/doc/api.md#push-api总结如果知道怎么结合原生 Android 和 iOS 插件集成到 Flutter 上,那使用极光推送,也可以不需要官方提供的 Flutter 插件,相信你也能写。相反地,使用官方提供的 Flutter 插件和集成文档,可以让我们快速的完成 push 通知功能,可以让我们更聚焦于我们的产品逻辑和功能上。 ...

March 31, 2019 · 1 min · jiezi

【2019-03-29】记录过去一周看过觉得很好的文章

跨平台抓包工具,亲自测试并使用,总的来说就是配置非常简单好用,值得拥有Mobile Debug官方网站(代理抓包/移动端H5调试/请求劫持/HTTPS支持/Hosts管理/WebSocket数据捕获/跨平一个可以更好排版微信公众号工具微信公众号工具一个专门收集web的面试的栏目,很多也很全,无聊的时候刷一刷看一看自己还有那些还不会的工作日每天一道前端大厂面试题,祝大家天天进步,一年后会看到不一样的自己。本文主要是将flutter在iOS和安卓中的实践以及遇到的问题,flutter是Google的又一个跨平台框架,主要是解决安卓和iOS不同系统带来的多成本和开发效率问题,目前国内已经又不少团队入坑,有兴趣可以玩玩。flutter实战Xcode 10.2发布,是和swift 5一起发的,主要是支持swift 5的开发 ,同时也新增了不少特性,解决了大量已存在问题。xcode 10.2这是网站有很多Mac系统应用,都是免费的,如果你在使用Mac,但是有些应用的付费的你又不想出钱,不妨来这里找找看有没有替代品或者是免费的。mac 免费下载ifunmac作者认为:良好的架构和优秀的实现。就像一个大的项目会拆分成很多模块一样,想要提高自己的编程能力也要拆分成很多小模块去达成。比如你的觉得你的命名不好,代码可读性差,你就去找这方面相关的资料去针对性的学习。可以看看《编写可读代码的艺术》《Clean code》。如果你觉得自己模块抽象能力不好,学习一下面向对象、设计模式之类的。如果本身这些具体模块的好坏自己不了解,直接学习一个优质项目也是囫囵吞枣。一个优质的项目应该具有什么特点swift5版本 正式更新,这是一个大版本更新。主要有:不再包含swift标准库的动态链接”瘦身app“;新增语言特性“#”解决字符串分隔符来带的转译混乱问题;在标准库中新增了simd类型和基本操作符以及set、dictionary等等;Swift5 更新预览Dio 是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等。目前Dio在pub上综合得分100分,排名已上榜pub首页(All Tab下) !同时Dio也是Github上最受欢迎的Flutter第三方库,项目地址:Dio-Github。Flutter Http库Dio 2.1正式发布flutter是一个由Google开发的跨平台框架,主要是解决iOS和安卓开发时由于使用不同不的语言导致开发成本和开发效率低的问题,当然也是顺应潮流,因为大前端是web端目前的大势,如果你先学习flutter不妨到这里的flutter光网看看,写个 hello worldflutter中文官网

March 29, 2019 · 1 min · jiezi

flutter之本地存储shared_preferences的超简单demo

说明记录flutter的本地存储插件shared_preferences的简单实用方法这是一个类似于web的localstorage的插件,在app上运行时,关闭app时,并不会自动清除掉值,第二次打开时值还在,区别于provide(flutter的状态管理插件)引入插件这里实用的是0.5.0的版本,有更新的可以实用更新的版本使用方法1、引入shared_preferences插件2、存储方法3、取出方法4、销毁方法直接上代码import ‘package:flutter/material.dart’;//1、引入shared_preferences插件import ‘package:shared_preferences/shared_preferences.dart’;void main(){ runApp( MyApp());}class MyApp extends StatelessWidget{ @override Widget build(BuildContext context){ return MaterialApp( home: Scaffold( appBar: AppBar( title: Text(“flutter provide”), ), body: Container( child: Column( children: <Widget>[ SaveData() ], ), ), ), ); }}class SaveData extends StatefulWidget{ _SaveDataState createState() => _SaveDataState();}class _SaveDataState extends State<SaveData>{ int _aaa ; _setData() async{ //2、存储的方法 SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setInt(“save_test”, 10); } _getData() async{ //3、取出的方法 SharedPreferences prefs =await SharedPreferences.getInstance(); setState(() { _aaa = prefs.getInt(“save_test”); }); } _removeData() async{ //4、销毁的方法 SharedPreferences prefs =await SharedPreferences.getInstance(); setState(() { prefs.remove(“save_test”); }); } @override Widget build(BuildContext context){ return Container( child: Column( children: <Widget>[ Text("$_aaa"), RaisedButton( onPressed: _setData, child: Text(“设置值为10”), ), RaisedButton( onPressed: _getData, child: Text(“获取值”), ), RaisedButton( onPressed: _removeData, child: Text(“清楚值”), ) ], ), ); }}添加依赖插件,复制上面代码到项目,直接运行 ...

March 29, 2019 · 1 min · jiezi

flutter之状态管理provide的超简单demo

业务流程图简单的业务流程图,如果有用过vuex,都是类似的东西,换汤不换药如何使用1、引入provide依赖2、新建状态仓库3、触发状态改变4、页面引用创库变量● 引入provide依赖● 新建状态仓库在lib目录下新建provide文件夹,在provide文件夹下新建创库文件counter.dartimport ‘package:flutter/material.dart’;class Counter with ChangeNotifier{ int value = 0; add(){ value++; notifyListeners(); //通知引用该变量地方的改变值 } subtract(){ value–; notifyListeners(); //通知引用该变量地方的改变值 }}●触发状态改变和页面引用import ‘package:flutter/material.dart’;import ‘package:provide/provide.dart’;import ‘./provide/counter.dart’;void main(){ //main函数里面引用provide var counter = Counter(); var providers =Providers(); providers..provide(Provider<Counter>.value(counter)); runApp(ProviderNode(child: MyApp(),providers: providers,));}class MyApp extends StatelessWidget{ @override Widget build(BuildContext context){ return MaterialApp( title: “flutter provide”, home: Scaffold( appBar: AppBar( title: Text(“flutter provide”), ), body: Container( child:Column( children: <Widget>[ GetProvideValue(), AddButton(), SubButton(), ], ) ), ), ); }}//获取provide状态里面的值class GetProvideValue extends StatelessWidget{ @override Widget build(BuildContext context){ return Container( child: Provide<Counter>( //在其他页面也是用同样的方法可以引用到provide里面的参数 builder: (context,child,counter){ return Text( “${counter.value}” ); }, ), ); }}//改变provide状态的值,调用provide里面的方法class AddButton extends StatelessWidget{ @override Widget build(BuildContext context){ return RaisedButton( onPressed: (){ Provide.value<Counter>(context).add(); }, child: Text(“增加”), ); }}//改变provide状态的值,调用provide里面的方法class SubButton extends StatelessWidget{ @override Widget build(BuildContext context){ return RaisedButton( onPressed: (){ Provide.value<Counter>(context).subtract(); }, child: Text(“减少”), ); }}新建完项目,直接把上面2段代码复制就可以运行了 ...

March 27, 2019 · 1 min · jiezi

Flutter实践

一、概览1.Flutter

March 27, 2019 · 1 min · jiezi

Flutter 实现原理及在马蜂窝的跨平台开发实践

一直以来,跨平台开发都是困扰移动客户端开发的难题。在马蜂窝旅游 App 很多业务场景里,我们尝试过一些主流的跨平台开发解决方案,比如 WebView 和 React Native,来提升开发效率和用户体验。但这两种方式也带来了新的问题。比如使用 WebView 跨平台方式,优点确实非常明显。基于 WebView 的框架集成了当下 Web 开发的诸多优势:丰富的控件库、动态化、良好的技术社区、测试自动化等等。但是缺点也同样明显:渲染效率和 JavaScript 的执行能力都比较差,使页面的加载速度和用户体验都不尽如人意。而使用以 React Native(简称 RN)为代表的框架时,维护又成了大难题。RN 使用类 HTML+JS 的 UI 创建逻辑,生成对应的原生页面,将页面的渲染工作交给了系统,所以渲染效率有很大的优势。但由于 RN 代码是通过 JS 桥接的方式转换为原生的控件,所以受各个系统间的差异影响非常大,虽然可以开发一套代码,但对各个平台的适配却非常的繁琐和麻烦。为什么是 Flutter2018 年 12 月初,Google 正式发布了开源跨平台 UI 框架 Flutter 1.0 Release 版本,马蜂窝电商客户端团队进行了调研与实践,发现 Flutter 能很好的帮助我们解决开发中遇到的问题。跨平台开发,针对 Android 与 iOS 的风格设计了两套设计语言的控件实现(Material & Cupertino)。这样不但能够节约人力成本,而且在用户体验上更好的适配 App 运行的平台。重写了一套跨平台的 UI 框架,渲染引擎是依靠 Skia 图形库实现。Flutter 中的控件树直接由渲染引擎和高性能本地 ARM 代码直接绘制,不需要通过中间对象(Web 应用中的虚拟 DOM 和真实 DOM,原生 App 中的虚拟控件和平台控件)来绘制,使它有接近原生页面的性能,帮助我们提供更好的用户体验。同时支持 JIT 和 AOT 编译。JIT 编译方式使其在开发阶段有个备受欢迎的功能——热重载(HotReload),这样在开发时可以省去构建的过程,提高开发效率。而在 Release 运行阶段采用 AOT 的编译方式,使执行效率非常高,让 Release 版本发挥更好的性能。于是,电商客户端团队决定探索 Flutter 在跨平台开发中的新可能,并率先应用于商家端 App 中。在本文中,我们将结合 Flutter 在马蜂窝商家端 App 中的应用实践,探讨 Flutter 架构的实现原理,有何优势,以及如何帮助我们解决问题。Flutter 架构和实现原理Flutter 使用 Dart 语言开发,主要有以下几点原因:Dart 一般情况下是运行 DartVM 上,但是也可以编译为 ARM 代码直接运行在硬件上。Dart 同时支持 AOT 和 JIT 两种编译方式,可以更好的提高开发以及 App 的执行效率。Dart 可以利用独特的隔离区(Isolate)实现多线程。而且不共享内存,可以实现无锁快速分配。分代垃圾回收,非常适合 UI 框架中常见的大量 Widgets 对象创建和销毁的优化。在为创建的对象分配内存时,Dart 是在现有的堆上移动指针,保证内存的增长是程线性的,于是就省了查找可用内存的过程。Dart 主要由 Google 负责开发和维护。目前 Dart 最新版本已经是 2.2,针对 App 和 Web 开发做了很多优化。并且对于大多数的开发者而言,Dart 的学习成本非常低。Flutter 架构也是采用的分层设计。从下到上依次为:Embedder(嵌入器)、Engine、Framework。<center>图 1: Flutter 分层架构图</center>Embedder是嵌入层,做好这一层的适配 Flutter 基本可以嵌入到任何平台上去; Engine层主要包含 Skia、Dart 和 Text。Skia 是开源的二位图形库;Dart 部分主要包括 runtime、Garbage Collection、编译模式支持等;Text 是文本渲染。Framework在最上层。我们的应用围绕 Framework 层来构建,因此也是本文要介绍的重点。Framework1.【Foundation】在最底层,主要定义底层工具类和方法,以提供给其他层使用。2.【Animation】是动画相关的类,可以基于此创建补间动画(Tween Animation)和物理原理动画(Physics-based Animation),类似 Android 的 ValueAnimator 和 iOS 的 Core Animation。3.【Painting】封装了 Flutter Engine 提供的绘制接口,例如绘制缩放图像、插值生成阴影、绘制盒模型边框等。4.【Gesture】提供处理手势识别和交互的功能。5.【Rendering】是框架中的渲染库。控件的渲染主要包括三个阶段:布局(Layout)、绘制(Paint)、合成(Composite)。从下图可以看到,Flutter 流水线包括 7 个步骤。<center>图 2: Flutter 流水线</center>首先是获取到用户的操作,然后你的应用会因此显示一些动画,接着 Flutter 开始构建 Widget 对象。Widget 对象构建完成后进入渲染阶段,这个阶段主要包括三步:布局元素:决定页面元素在屏幕上的位置和大小;绘制阶段:将页面元素绘制成它们应有的样式;合成阶段:按照绘制规则将之前两个步骤的产物组合在一起。最后的光栅化由 Engine 层来完成。在渲染阶段,控件树(widget)会转换成对应的渲染对象(RenderObject)树,在 Rendering 层进行布局和绘制。在布局时 Flutter 深度优先遍历渲染对象树。数据流的传递方式是从上到下传递约束,从下到上传递大小。也就是说,父节点会将自己的约束传递给子节点,子节点根据接收到的约束来计算自己的大小,然后将自己的尺寸返回给父节点。整个过程中,位置信息由父节点来控制,子节点并不关心自己所在的位置,而父节点也不关心子节点具体长什么样子。<center>图 3: 数据流传递方式</center>为了防止因子节点发生变化而导致的整个控件树重绘,Flutter 加入了一个机制——Relayout Boundary,在一些特定的情形下 Relayout Boundary 会被自动创建,不需要开发者手动添加。例如,控件被设置了固定大小(tight constraint)、控件忽略所有子视图尺寸对自己的影响、控件自动占满父控件所提供的空间等等。很好理解,就是控件大小不会影响其他控件时,就没必要重新布局整个控件树。有了这个机制后,无论子树发生什么样的变化,处理范围都只在子树上。<center>图 4: Relayout Boundary 机制</center>在确定每个空间的位置和大小之后,就进入绘制阶段。绘制节点的时候也是深度遍历绘制节点树,然后把不同的 RenderObject 绘制到不同的图层上。这时有可能出现一种特殊情况,如下图所示节点 2 在绘制子节点 4 时,由于其节点 4 需要单独绘制到一个图层上(如 video),因此绿色图层上面多了个黄色的图层。之后再需要绘制其他内容(标记 5)就需要再增加一个图层(红色)。再接下来要绘制节点 1 的右子树(标记 6),也会被绘制到红色图层上。所以如果 2 号节点发生改变就会改变红色图层上的内容,因此也影响到了毫不相干的 6 号节点。<center>图 5: 绘制节点与图层的关系</center>为了避免这种情况,Flutter 的设计者这里基于 Relayout Boundary 的思想增加了Repaint Boundary。在绘制页面时候如果遇见 Repaint Boundary 就会强制切换图层。如下图所示,在从上到下遍历控件树遇到 Repaint Boundary 会重新绘制到新的图层(深蓝色),在从下到上返回的时候又遇到 Repaint Boundary,于是又增加一个新的图层(浅蓝色)。<center>图 6: Repaint Boundary 机制</center>这样,即使发生重绘也不会对其他子树产生影响。比如在 Scrollview 上,当滚动的时候发生内容重绘,如果在 Scrollview 以外的地方不需要重绘就可以使用 Repaint Boundary。Repaint Boundary 并不会像 Relayout Boundary 一样自动生成,而是需要我们自己来加入到控件树中。6.【Widget】控件层。所有控件的基类都是 Widget,Widget 的数据都是只读的, 不能改变。所以每次需要更新页面时都需要重新创建一个新的控件树。每一个 Widget 会通过一个 RenderObjectElement 对应到一个渲染节点(RenderObject),可以简单理解为 Widget 中只存储了页面元素的信息,而真正负责布局、渲染的是 RenderObject。在页面更新重新生成控件树时,RenderObjectElement 树会尽量保持重用。由于 RenderObjectElement 持有对应的 RenderObject,所有 RenderObject 树也会尽可能的被重用。如图所示就是三棵树之间的关系。在这张图里我们把形状当做渲染节点的类型,颜色是它的属性,即形状不同就是不同的渲染节点,而颜色不同只是同一对象的属性的不同。<center>图 7:Widget、Element 和 Render 之间的关系</center>如果想把方形的颜色换成黄色,将圆形的颜色变成红色,由于控件是不能被修改的,需要重新生成两个新的控件 Rectangle yellow 和 Circle red。由于只是修改了颜色属性,所以 Element 和 RenderObject 都被重用,而之前的控件树会被释放回收。<center>图 8: 示例</center>那么如果把红色圆形变成三角形又会怎样呢?由于这里发生变化的是类型,所以对应的 Element 节点和 RenderObject 节点都需要重新创建。但是由于黄色方形没有发生改变,所以其对应的 Element 节点和 RenderObject 节点没有发生变化。<center>图 9: 示例</center>7. 最后是【Material】 & 【Cupertino】,这是在 Widget 层之上框架为开发者提供的基于两套设计语言实现的 UI 控件,可以帮助我们的 App 在不同平台上提供接近原生的用户体验。Flutter 在马蜂窝商家端App 中的应用实践<center>图 10: 马蜂窝商家端使用 Flutter 开发的页面</center>开发方式:Flutter + Native由于商家端已经是一款成熟的 App,不可能创建一个新的 Flutter 工程全部重新开发,因此我们选择 Native 与 Flutter 混编的方案来实现。 在了解 Native 与 Flutter 混编方案前,首先我们需要了解在 Flutter 工程中,通常有以下 4 种工程类型:1. Flutter Application标准的 Flutter App 工程,包含标准的 Dart 层与 Native 平台层。2. Flutter ModuleFlutter 组件工程,仅包含 Dart 层实现,Native 平台层子工程为通过 Flutter 自动生成的隐藏工程(.ios /.android)。3. Flutter PluginFlutter 平台插件工程,包含 Dart 层与 Native 平台层的实现。4. Flutter PackageFlutter 纯 Dart 插件工程,仅包含 Dart 层的实现,往往定义一些公共 Widget。了解了 Flutter 工程类型后,我们来看下官方提供的一种混编方案(https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps),即在现有工程下创建Flutter Module 工程,以本地依赖的方式集成到现有的 Native 工程中。官方集成方案(以 iOS 为例)a. 在工程目录创建 FlutterModule,创建后,工程目录大致如下:b. 在 Podfile 文件中添加以下代码:flutter_application_path = ‘../flutter_Moudule/‘该脚本主要负责:pod 引入 Flutter.Framework 以及 FlutterPluginRegistrant 注册入口pod 引入 Flutter 第三方 plugin在每一个 pod 库的配置文件中写入对 Generated.xcconfig 文件的导入修改 pod 库的 ENABLE_BITCODE = NO(因为 Flutter 现在不支持 bitcode)c. 在 iOS 构建阶段 Build Phases 中注入构建时需要执行的 xcode_backend.sh (位于 FlutterSDK/packages/flutter_tools/bin) 脚本:"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build该脚本主要负责:构建 App.framework 以及 Flutter.framework 产物根据编译模式(debug/profile/release)导入对应的产物编译 flutter_asset 资源把以上产物 copy 到对应的构建产物中d. 与 Native 通信方案一:改造 AppDelegate 继承自 FlutterAppDelegate方案二:AppDelegate 实现 FlutterAppLifeCycleProvider 协议,生命周期由 FlutterPluginAppLifeCycleDelegate 传递给 Flutter以上就是官方提供的集成方案。我们最终没有选择此方案的原因,是它直接依赖于 FlutterModule 工程以及 Flutter 环境,使 Native 开发同学无法脱离 Flutter 环境开发,影响正常的开发流程,团队合作成本较大;而且会影响正常的打包流程。(目前 Flutter 团队正在重构嵌入 Native 工程的方式)最终我们选择另一种方案来解决以上的问题:远端依赖产物。<center>图 11 :远端依赖产物</center>iOS 集成方案通过对官方混编方案的研究,我们了解到 iOS 工程最终依赖的其实是 FlutterModule 工程构建出的产物(Framework,Asset,Plugin),只需将产物导出并 push 到远端仓库,iOS 工程通过远端依赖产物即可。依赖产物目录结构如下:App.framework: Flutter 工程产物(包含 Flutter 工程的代码,Debug 模式下它是个空壳,代码在 flutter_assets 中)。Flutter.framework:Flutter 引擎库。与编译模式(debug/profile/release)以及 CPU 架构(arm*, i386, x86_64)相匹配。lib.a & .h 头文件: FlutterPlugin 静态库(包含在 iOS 端的实现)。flutter_assets: 包含 Flutter 工程字体,图片等资源。在 Flutter1.2 版本中,被打包到 App.framework 中。Android 集成方案Android Nativite 集成是通过 Gradle 远程依赖 Flutter 工程产物的方式完成的,以下是具体的集成流程。a.创建 Flutter 标准工程$ flutter create flutter_demo默认使用 Java 代码,如果增加 Kotlin 支持,使用如下命令:$ flutter create -a kotlin flutter_demob.修改工程的默认配置修改 app module 工程的 build.gradle 配置 apply plugin: ‘com.android.application’ => apply plugin: ‘com.android.library’,并移除 applicationId 配置修改 root 工程的 build.gradle 配置在集成过程中 Flutter 依赖了三方 Plugins 后,遇到 Plugins 的代码没有被打进 Library 中的问题。通过以下配置解决(这种方式略显粗暴,后续的优化方案正在调研)。subprojects { project.buildDir = “${rootProject.buildDir}/app”}app module 增加 maven 打包配置c. 生成 Android Flutter 产物$ cd android$ ./gradlew uploadArchives官方默认的构建脚本在 Flutter 1.0.0 版本存在 Bug——最终的产物中会缺少 flutter_shared/icudtl.dat 文件,导致 App Crash。目前的解决方式是将这个文件复制到工程的 assets 下(在 Flutter 最新 1.2.1 版本中这个 Bug 已被修复,但是 1.2.1 版本又出现了一个 UI 渲染的问题,所以只能继续使用 1.0.0 版本)。d.Android Native 平台工程集成,增加下面依赖配置即可,不会影响 Native 平台开发的同学implementation ‘com.mfw.app:MerchantFlutter:0.0.5-beta’Flutter 和 iOS、Android 的交互使用平台通道(Platform Channels)在 Flutter 工程和宿主(Native 工程)之间传递消息,主要是通过 MethodChannel 进行方法的调用,如下图所示:<center>图 12 :Flutter 与 iOS、Android 交互</center>为了确保用户界面不会挂起,消息和响应是异步传递的,需要用 async 修饰方法,await 修饰调用语句。Flutter 工程和宿主工程通过在 Channel 构造函数中传递 Channel 名称进行关联。单个应用中使用的所有 Channel 名称必须是唯一的; 可以在 Channel 名称前加一个唯一的「域名前缀」。Flutter 与 Native 性能对比我们分别使用 Native 和 Flutter 开发了两个列表页,以下是页面效果和性能对比:iOS 对比(机型 6P 系统 10.3.3):Flutter 页面:iOS Native 页面:可以看到,从使用和直观感受都没有太大的差别。于是我们采集了一些其他方面的数据。Flutter 页面:iOS Native 页面:另外我们还对比了商家端接入 Flutter 前后包体积的大小:39Mb → 44MB在 iOS 机型上,流畅度上没有什么差异。从数值上来看,Flutter 在 内存跟 GPU/CPU 使用率上比原生略高。Demo 中并没有对 Flutter 做更多的优化,可以看出 Flutter 整体来说还是可以做出接近于原生的页面。下面是 Flutter 与 Android 的性能对比。Flutter 页面:Android Native 页面:从以上两张对比图可以看出,不考虑其他因素,单纯从性能角度来说,原生要优于 Flutter,但是差距并不大,而且 Flutter 具有的跨平台开发和热重载等特点极大地节省了开发效率。并且,未来的热修复特性更是值得期待。混合栈管理首先先介绍下 Flutter 路由的管理:Flutter 管理页面有两个概念:Route 和 Navigator。Navigator 是一个路由管理的 Widget(Flutter 中万物皆 Widget),它通过一个栈来管理一个路由 Widget 集合。通常当前屏幕显示的页面就是栈顶的路由。路由 (Route) 在移动开发中通常指页面(Page),这跟 web 开发中单页应用的 Route 概念意义是相同的,Route 在 Android 中通常指一个 Activity,在 iOS 中指一个 ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。这和原生开发类似,无论是 Android 还是 iOS,导航管理都会维护一个路由栈,路由入栈 (push) 操作对应打开一个新页面,路由出栈 (pop) 操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。<center>图 14 :Flutter 路由管理</center>如果是纯 Flutter 工程,页面栈无需我们进行管理,但是引入到 Native 工程内,就需要考虑如何管理混合栈。并且需要解决以下几个问题:1. 保证 Flutter 页面与 Native 页面之间的跳转从用户体验上没有任何差异2. 页面资源化(马蜂窝特有的业务逻辑)3. 保证生命周期完整性,处理相关打点事件上报4. 资源性能问题参考了业界内的解决方法,以及项目自身的实际场景,我们选择类似于 H5 在 Navite 中嵌入的方式,统一通过 openURL 跳转到一个 Native 页面(FlutterContainerVC),Native 页面通过 addChildViewController 方式添加 FlutterViewController(负责 Flutter 页面渲染),同时通过 channel 同步 Native 页面与 Flutter 页面。每一次的 push/pop 由 Native 发起,同时通过 channel 保持 Native 与 Flutter 页面同步——在 Native 中跳转 Flutter 页面与跳转原生无差异一个 Flutter 页面对应一个 Native 页面(FlutterContainerVC)——解决页面资源化FlutterContainerVC 通过 addChildViewController 对单例 FlutterViewController 进行复用——保证生命周期完整性,处理相关打点事件上报由于每一个 FlutterViewController(提供 Flutter 视图的实现)会启动三个线程,分别是 UI 线程、GPU 线程和 IO 线程,使用单例 FlutterViewController 可以减少对资源的占用——解决资源性能问题Flutter 应用总结Flutter 一经发布就很受关注,除了 iOS 和 Android 的开发者,很多前端工程师也都非常看好 Flutter 未来的发展前景。相信也有很多公司的团队已经投入到研究和实践中了。不过 Flutter 也有很多不足的地方,值得我们注意:虽然 1.2 版本已经发布,但是目前没有达到完全稳定状态,1.2 发布完了就出现了控件渲染的问题。加上 Dart 语言生态小,学习资料可能不够丰富。关于动态化的支持,目前 Flutter 还不支持线上动态性。如果要在 Android 上实现动态性相对容易些,iOS 由于审核原因要实现动态性可能成本很高。Flutter 中目前拿来就用的能力只有 UI 控件和 Dart 本身提供能力,对于平台级别的能力还需要通过 channel 的方式来扩展。已有工程迁移比较复杂,以前沉淀的 UI 控件,需要重新再实现一套。最后一点比较有争议,Flutter 不会从程序中拆分出额外的模板或布局语言,如 JSX 或 XM L,也不需要单独的可视布局工具。有的人认为配合 HotReload 功能使用非常方便,但我们发现这样代码会有非常多的嵌套,阅读起来有些吃力。目前阿里的闲鱼开发团队已经将 Flutter 用于大型实践,并应用在了比较重要的场景(如产品详情页),为后来者提供了良好的借鉴。马蜂窝的移动客户端团队关于 Flutter 的探索才刚刚起步,前面还有很多的问题需要我们一点一点去解决。不过无论从 Google 对其的重视程度,还是我们从实践中看到的这些优点,都让我们对 Flutter 充满信心,也希望在未来我们可以利用它创造更多的价值和奇迹。路途虽远,犹可期许。本文作者:马蜂窝电商研发客户端团队。(马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,禁止商业用途,谢谢配合。)参考文献:Flutter’s Layered Designhttps://www.youtube.com/watch?v=dkyY9WCGMi0 Flutter’s Rendering Pipelinehttps://www.youtube.com/watch?v=UUfXWzp0-DU&t=1955s Flutter 原理与美团的实践https://juejin.im/post/5b6d59476fb9a04fe91aa778#comment 关注马蜂窝技术,找到更多你想要的内容 ...

March 26, 2019 · 4 min · jiezi

Flutter入门实战教程:从0到1仿写web版掘金App (完结)

前言准确的说,这是去年十一月份就写好的教程,虽然迟迟未上线(拖了半年),但是非常感谢购买的老铁们虽然心中很不爽,但是回头想想,也是的确写的比较仓促,但是当时自己在写的过程中,的确能学到很多东西,所以很想分享出来,至于说内容不过关,我也无话可说,也非常接受不上线的结果。虽然我并不能确定,在去年,这位运营小哥所说的该如何写是否真的知道 Flutter 应有的学习路线。即便如此,有各种老铁们的支持,还是心里面还是暖暖的。掘金是一个很好的平台,合作的过程中虽然发生的不愉快,但是依旧不妨碍分享呀运行效果正文这里我也是直接拿小册介绍来了作者介绍前环球网全栈开发工程师,现某厂高级前端工程师。技术主要涉猎pc及无线。GitHub上千star项目《React-Express-Blog-Demo》作者,环球网新版官方Android客户端第一版主要开发者Alibaba/FlutterGo 主要开发者热爱技术,热爱分享。一起学习,一起进步GitHub地址:https://github.com/Nealyang介绍之前有介绍过关于flutter的入门文章:flutter从入门到寄几玩儿,文章底部写到会继续写demo给大家练习。由于之前在GitHub上写过类似React练习项目:React-Express-Blog-Demo ,所以最近笔者也一直在思考,是否可以同样,用flutter一步一步来带领大家一起学习使用fluter呢。技术终归就是工具,没有实操是绝对的纸上谈兵。鉴于网上太多关于flutter的系列入门文章,其中有掘金翻译的fluter中文网其实就是一个很好的入门资料。讲了这么多,我们这里稍微介绍下flutter。2018年6月21日Google发布Flutter首个release预览版,作为Google baba大力推出的一种全新的响应式,跨平台,高性能的移动开发框架。Flutter是一个跨平台的移动UI框架,旨在帮助开发者使用一套代码开发高性能、高保真的Android和iOS应用。flutter优点主要包括:跨平台开源Hot Reload、响应式框架、及其丰富的控件以及开发工具灵活的界面设计以及控件组合借助可以移植的GPU加速的渲染引擎以及高性能ARM代码运行时已达到高质量的用户体验而且,他是Google BaBa开源的,并且目前阿里巴巴集团内部也是flutter风气正茂,所以我们完全有理由相信flutter未来,一定会有自己的舞台。下图来自developers.googleblog.com官网从StackOverflow统计来的数据回到这本小册,笔者将从flutter基础到一步一步实现web版掘金来带大家感受flutter的魅力。旨在让大家熟练使用flutter来完成自己想做的APP开发。当然,笔者深知授人鱼不如授人以渔。所以文章中,会介绍笔者遇到的问题,以及思考的过程。方便大家借鉴与思考。实战项目并非 1:1 还原web版掘金,小册旨在带领大家使用flutter完成App开发,解决问题思路以及widget查找、使用的介绍。对于flutter的环境搭建以及后续的上线并未涉及。后面可加群一起讨论。文章随着项目的编写,一边开发一遍写文章,后面开发过程中可能会遇到之前不合理的地方可能会回头修改编写你会学到什么?入门flutter、学习Dart ,掌握一门新技术掌握flutter、dart中开发技巧以及解决问题的方式常用Widget的使用并可独立完成界面编写flutter中路由的使用以及flutter package的查找和使用学会网络请求、上拉刷新等常规App具有的功能开发独立开发App适宜人群前端、客户端开发者有一定的编程基础且看过flutter官网基础教程有激情、有热情、愿意花时间去探索新知识教程内容全篇地址:Nealyang/personalBlog项目源码地址:Nealyang/flutterFlutter入门实战:从0到1仿写web版掘金AppDart基础介绍flutter入门以及常用Widget介绍“flutter”数据model及json处理首页List UI编写fluro介绍以及路由配置网络请求下拉刷新 & 加载更多webView for Detail驻足思考、总结沸点 UI & 功能 编写(上)沸点 UI & 功能 编写(下)小册 UI & 功能 编写开源库、活动 UI & 功能 编写登陆功能 & App响应感言再次感谢支持我的小伙伴招聘国际惯例,总是要来一波广告的嘛阿里巴巴春招开始啦~欢迎各路2020届小鲜肉加入我们!快快扫码加入我们吧扫描下方二维码,内推直通车!!!

March 20, 2019 · 1 min · jiezi

Flutter UI APP 低调上线

项目仓库 目前项目组件还在不断更新中…… 当github仓库更新后 教程会实时更新到app里面 apk下载 欢迎对组件疑问提出你的issue项目背景在经历了REACT NATIVE 弃用潮的大环境里,越来越多人寻找着可替代方案 Flutter 应该是目前比较热门的一项技术 —— 高效的开发效率,一套代码可以支持 Android/iOS 双端运行,Google 新的操作系统 Fuchsia 的默认 UI Toolkit 等等,都吸引了开发者社区大量的关注。 目前中国环境使用Flutter开发的案例并不多,作为一支尝鲜的团队,我们利用flutter很好地推动项目快速运转,也整理了部分开发经验!项目优势从开发到开源整理项目课程,我们经历了几次的迭代,汇集了react的开发经验,架构出适合开源教程的开源App动态更新,我们通过md的更新PR 可以动态实现app内文档更新,可以持续动态更新而不需要重新发版多语言埋点,为了更好跟国际接轨,我们实现了通过JSON达到了多语言切换的效果多主题预埋点,我们通过统一管理样式的方式进行编码,方便后续进行多主题切换Scope Model数据管理应用,达到了UI,数据,控制分离的目的预览最后感谢前期Efox 团队在完成项目的同时能够牺牲休息时间对项目作出的贡献 感谢我们的用户,感谢每一个提issue和pr的人 感谢每一个帮助过我们的人 需要获取更多动态的同学,可以加到仓库QQ群一起交流 希望一起参与建设中文社区的同学可以查看开发者如何参与完善控件

March 18, 2019 · 1 min · jiezi

码上用它开始Flutter混合开发——FlutterBoost

开源地址:https://github.com/alibaba/flutter_boost为什么要混合方案具有一定规模的App通常有一套成熟通用的基础库,尤其是阿里系App,一般需要依赖很多体系内的基础库。那么使用Flutter重新从头开发App的成本和风险都较高。所以在Native App进行渐进式迁移是Flutter技术在现有Native App进行应用的稳健型方式。闲鱼在实践中沉淀出一套自己的混合技术方案。在此过程中,我们跟Google Flutter团队进行着密切的沟通,听取了官方的一些建议,同时也针对我们业务具体情况进行方案的选型以及具体的实现。官方提出的混合方案1基本原理Flutter技术链主要由C++实现的Flutter Engine和Dart实现的Framework组成(其配套的编译和构建工具我们这里不参与讨论)。Flutter Engine负责线程管理,Dart VM状态管理和Dart代码加载等工作。而Dart代码所实现的Framework则是业务接触到的主要API,诸如Widget等概念就是在Dart层面Framework内容。一个进程里面最多只会初始化一个Dart VM。然而一个进程可以有多个Flutter Engine,多个Engine实例共享同一个Dart VM。我们来看具体实现,在iOS上面每初始化一个FlutterViewController就会有一个引擎随之初始化,也就意味着会有新的线程(理论上线程可以复用)去跑Dart代码。Android类似的Activity也会有类似的效果。如果你启动多个引擎实例,注意此时Dart VM依然是共享的,只是不同Engine实例加载的代码跑在各自独立的Isolate。2官方建议引擎深度共享在混合方案方面,我们跟Google讨论了可能的一些方案。Flutter官方给出的建议是从长期来看,我们应该支持在同一个引擎支持多窗口绘制的能力,至少在逻辑上做到FlutterViewController是共享同一个引擎的资源的。换句话说,我们希望所有绘制窗口共享同一个主Isolate。但官方给出的长期建议目前来说没有很好的支持。多引擎模式我们在混合方案中解决的主要问题是如何去处理交替出现的Flutter和Native页面。Google工程师给出了一个Keep It Simple的方案:对于连续的Flutter页面(Widget)只需要在当前FlutterViewController打开即可,对于间隔的Flutter页面我们初始化新的引擎。例如,我们进行下面一组导航操作:我们只需要在Flutter Page1和Flutter Page3创建不同的Flutter实例即可。这个方案的好处就是简单易懂,逻辑清晰,但是也有潜在的问题。如果一个Native页面一个Flutter页面一直交替进行的话,Flutter Engine的数量会线性增加,而Flutter Engine本身是一个比较重的对象。多引擎模式的问题冗余的资源问题.多引擎模式下每个引擎之间的Isolate是相互独立的。在逻辑上这并没有什么坏处,但是引擎底层其实是维护了图片缓存等比较消耗内存的对象。想象一下,每个引擎都维护自己一份图片缓存,内存压力将会非常大。插件注册的问题。插件依赖Messenger去传递消息,而目前Messenger是由FlutterViewController(Activity)去实现的。如果你有多个FlutterViewController,插件的注册和通信将会变得混乱难以维护,消息的传递的源头和目标也变得不可控。Flutter Widget和Native的页面差异化问题。Flutter的页面是Widget,Native的页面是VC。逻辑上来说我们希望消除Flutter页面与Naitve页面的差异,否则在进行页面埋点和其它一些统一操作的时候都会遇到额外的复杂度。增加页面之间通信的复杂度。如果所有Dart代码都运行在同一个引擎实例,它们共享一个Isolate,可以用统一的编程框架进行Widget之间的通信,多引擎实例也让这件事情更加复杂。因此,综合多方面考虑,我们没有采用多引擎混合方案。现状及思考前面我们提到多引擎存在一些实际问题,所以闲鱼目前采用的混合方案是共享同一个引擎的方案。这个方案基于这样一个事实:任何时候我们最多只能看到一个页面,当然有些特定的场景你可以看到多个ViewController,但是这些特殊场景我们这里不讨论。我们可以这样简单去理解这个方案:我们把共享的Flutter View当成一个画布,然后用一个Native的容器作为逻辑的页面。每次在打开一个容器的时候我们通过通信机制通知Flutter View绘制成当前的逻辑页面,然后将Flutter View放到当前容器里面。这个方案无法支持同时存在多个平级逻辑页面的情况,因为你在页面切换的时候必须从栈顶去操作,无法再保持状态的同时进行平级切换。举个例子:有两个页面A,B,当前B在栈顶。切换到A需要把B从栈顶Pop出去,此时B的状态丢失,如果想切回B,我们只能重新打开B之前页面的状态无法维持住。如在pop的过程当中,可能会把Flutter 官方的Dialog进行误杀。而且基于栈的操作我们依赖对Flutter框架的一个属性修改,这让这个方案具有了侵入性的特点。具体细节,大家可以参考老方案开源项目地址:https://github.com/alibaba-flutter/hybridstackmanager新一代混合技术方案 FlutterBoost1重构计划在闲鱼推进Flutter化过程当中,更加复杂的页面场景逐渐暴露了老方案的局限性和一些问题。所以我们启动了代号FlutterBoost(向C++ Boost库致敬)的新混合技术方案。这次新的混合方案我们的主要目标有:可复用通用型混合方案支持更加复杂的混合模式,比如支持主页Tab这种情况无侵入性方案:不再依赖修改Flutter的方案支持通用页面生命周期统一明确的设计概念跟老方案类似,新的方案还是采用共享引擎的模式实现。主要思路是由Native容器Container通过消息驱动Flutter页面容器Container,从而达到Native Container与Flutter Container的同步目的。我们希望做到Flutter渲染的内容是由Naitve容器去驱动的。简单的理解,我们想做到把Flutter容器做成浏览器的感觉。填写一个页面地址,然后由容器去管理页面的绘制。在Native侧我们只需要关心如果初始化容器,然后设置容器对应的页面标志即可。2主要概念3Native层概念Container:Native容器,平台Controller,Activity,ViewControllerContainer Manager:容器的管理者Adaptor:Flutter是适配层Messaging:基于Channel的消息通信4Dart层概念Container:Flutter用来容纳Widget的容器,具体实现为Navigator的派生类-Container Manager:Flutter容器的管理,提供show,remove等ApiCoordinator: 协调器,接受Messaging消息,负责调用Container Manager的状态管理。Messaging:基于Channel的消息通信5关于页面的理解在Native和Flutter表示页面的对象和概念是不一致的。在Native,我们对于页面的概念一般是ViewController,Activity。而对于Flutter我们对于页面的概念是Widget。我们希望可统一页面的概念,或者说弱化抽象掉Flutter本身的Widget对应的页面概念。换句话说,当一个Native的页面容器存在的时候,FlutteBoost保证一定会有一个Widget作为容器的内容。所以我们在理解和进行路由操作的时候都应该以Native的容器为准,Flutter Widget依赖于Native页面容器的状态。那么在FlutterBoost的概念里说到页面的时候,我们指的是Native容器和它所附属的Widget。所有页面路由操作,打开或者关闭页面,实际上都是对Native页面容器的直接操作。无论路由请求来自何方,最终都会转发给Native去实现路由操作。这也是接入FlutterBoost的时候需要实现Platform协议的原因。另一方面,我们无法控制业务代码通过Flutter本身的Navigator去push新的Widget。对于业务不通过FlutterBoost而直接使用Navigator操作Widget的情况,包括Dialog这种非全屏Widget,我们建议是业务自己负责管理其状态。这种类型Widget不属于FlutterBoost所定义的页面概念。理解这里的页面概念,对于理解和使用FlutterBoost至关重要。6与老方案主要差别前面我们提到老方案在Dart层维护单个Navigator栈结构用于Widget的切换。而新的方案则是在Dart侧引入了Container的概念,不再用栈的结构去维护现有的页面,而是通过扁平化key-value映射的形式去维护当前所有的页面,每个页面拥有一个唯一的id。这种结构很自然的支持了页面的查找和切换,不再受制于栈顶操作的问题,之前的一些由于pop导致的问题迎刃而解。也不需要依赖修改Flutter源码的形式去进行页面栈操作,去掉了实现的侵入性。实际上我们引入的Container就是Navigator的,也就是说一个Native的容器对应了一个Navigator。那这是如何做到的呢?7多Navigator的实现Flutter在底层提供了让你自定义Navigator的接口,我们自己实现了一个管理多个Navigator的对象。当前最多只会有一个可见的Flutter Navigator,这个Navigator所包含的页面也就是我们当前可见容器所对应的页面。Native容器与Flutter容器(Navigator)是一一对应的,生命周期也是同步的。当一个Native容器被创建的时候,Flutter的一个容器也被创建,它们通过相同的id关联起来。当Native的容器被销毁的时候,Flutter的容器也被销毁。Flutter容器的状态是跟随Native容器,这也就是我们说的Native驱动。由Manager统一管理切换当前在屏幕上展示的容器。我们用一个简单的例子描述一个新页面创建的过程:创建Native容器(iOS ViewController,Android Activity or Fragment)。Native容器通过消息机制通知Flutter Coordinator新的容器被创建。Flutter Container Manager进而得到通知,负责创建出对应的Flutter容器,并且在其中装载对应的Widget页面。当Native容器展示到屏幕上时,容器发消息给Flutter Coordinator通知要展示页面的id.Flutter Container Manager找到对应id的Flutter Container并将其设置为前台可见容器。这就是一个新页面创建的主要逻辑,销毁和进入后台等操作也类似有Native容器事件去进行驱动。总结目前FlutterBoost已经在生产环境支撑着在闲鱼客户端中所有的基于Flutter开发业务,为更加负复杂的混合场景提供了支持,稳定为亿级用户提供服务。我们在项目启动之初就希望FlutterBoost能够解决Native App混合模式接入Flutter这个通用问题。所以我们把它做成了一个可复用的Flutter插件,希望吸引更多感兴趣的朋友参与到Flutter社区的建设。在有限篇幅中,我们分享了闲鱼在Flutter混合技术方案中积累的经验和代码。欢迎兴趣的同学能够积极与我们一起交流学习。扩展补充1性能相关在两个Flutter页面进行切换的时候,因为我们只有一个Flutter View所以需要对上一个页面进行截图保存,如果Flutter页面多截图会占用大量内存。这里我们采用文件内存二级缓存策略,在内存中最多只保存2-3个截图,其余的写入文件按需加载。这样我们可以在保证用户体验的同时在内存方面也保持一个较为稳定的水平。页面渲染性能方面,Flutter的AOT优势展露无遗。在页面快速切换的时候,Flutter能够很灵敏的相应页面的切换,在逻辑上创造出一种Flutter多个页面的感觉。2Release1.0的支持项目开始的时候我们基于闲鱼目前使用的Flutter版本进行开发,而后进行了Release 1.0兼容升级测试目前没有发现问题。3接入只要是集成了Flutter的项目都可以用官方依赖的方式非常方便的以插件形式引入FlutterBoost,只需要对工程进行少量代码接入即可完成接入。详细接入文档,请参阅GitHub主页官方项目文档。4现已开源目前,新一代混合栈已经在闲鱼全面应用。我们非常乐意将沉淀的技术回馈给社区。欢迎大家一起贡献,一起交流,携手共建Flutter社区。项目开源地址:https://github.com/alibaba/flutter_boost本文作者:福居阅读原文本文来自云栖社区合作伙伴“闲鱼技术”,如需转载请联系原作者。

March 18, 2019 · 1 min · jiezi

vol.1 Flutter Coding Dojo

Flutter 开发者的道场,练习基本招式。精选 Stack Overflow 网站 flutter、dart 标签下的常见问题,总结为实用、简短的招式。Flutter 发布以来,受到越来越多的开发者和组织关注和使用。快速上手开发,需要了解 Dart 语言特性、Flutter 框架 SDK 及 API、惯用法、异常处理、辅助开发的工具使用等…。隐藏调试模式横幅。 return MaterialApp( title: ‘Flutter Demo’, debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: ‘Flutter Demo Home Page’), );打印日志调试应用。import ‘package:flutter/foundation.dart’;print(’logs’);debugPrint(’logs’);当日志输出过多时,Android 系统可能会丢弃一些日志行,为了避免这种情况使用 Flutter foundation 库提供的 debugPrint() 方法。指定 App 用户界面可以显示的方向集[…]。SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);获取手机屏幕宽高。double width = MediaQuery.of(context).size.width;double height = MediaQuery.of(context).size.height;获取 App 可以显示的矩形边界,排除被系统 UI(状态栏)或硬件凹槽遮挡部分。EdgeInsets devicePadding = MediaQuery.of(context).padding;添加、显示图片资源。Image.asset(‘images/cat.png’)确保在 pubspec.yaml 文件中显式标识资源路径,否则将会抛出异常。flutter: assets: - images/cat.pngflutter: ══╡ EXCEPTION CAUGHT BY IMAGE RESOURCE SERVICE ╞══flutter: The following assertion was thrown resolving an image codec:flutter: Unable to load asset: assets/heart_icon.png注意:在 .yaml 类型的文件中,正确的空格缩进至关重要。导航到新页面(Route)然后返回。Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => MyPage());Navigator.pop(context);使用自定义 Widget 替换红屏 ErrorWidget。ErrorWidget.builder = (FlutterErrorDetails details) { return Container();}确保 setState() 方法不会在 dispose() 方法之后调用。if (this.mounted){ setState((){ //Your state change code goes here });}setState() 方法可能会抛出异常:throw FlutterError( ‘setState() called after dispose(): $this\n’ ‘This error happens if you call setState() on a State object for a widget that ’ ’no longer appears in the widget tree (e.g., whose parent widget no longer ’ ‘includes the widget in its build). This error can occur when code calls ’ ‘setState() from a timer or an animation callback. The preferred solution is ’ ’to cancel the timer or stop listening to the animation in the dispose() ’ ‘callback. Another solution is to check the “mounted” property of this ’ ‘object before calling setState() to ensure the object is still in the ’ ’tree.\n’ ‘This error might indicate a memory leak if setState() is being called ’ ‘because another object is retaining a reference to this State object ’ ‘after it has been removed from the tree. To avoid memory leaks, ’ ‘consider breaking the reference to this object during dispose().’ );Dart 单例设计模式,使用工厂构造函数。class Singleton { static final Singleton _singleton = Singleton._internal(); factory Singleton() { return _singleton; } Singleton._internal();} ...

March 16, 2019 · 2 min · jiezi

移动跨平台框架Flutter介绍和学习线路

Flutter简介Flutter是一款移动应用程序SDK,一份代码可以同时生成iOS和Android两个高性能、高保真的应用程序。Flutter目标是使开发人员能够交付在不同平台上都感觉自然流畅的高性能应用程序。我们兼容滚动行为、排版、图标等方面的差异。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。Flutter历史说到Flutter,可能很多小伙伴都会以为它是新兴的的移动开发框架,其实不然,Flutter的历史最早可以追溯到2014年10月,其前身是Google内部孵化的Sky项目。不过,随着Flutter热度的上升,特别是2018年Flutter陆续发布了Beta版和Flutter1.0,给很多小伙伴造成了一个误区:认为Flutter是最近新兴的一个开发框架。最近,Google又发布了1.2正式版,并且官方也发布了今年的开发路线(参考Flutter 2019 产品路线图),可以预见,Flutter将在2019年迎来真正的爆发和成长。为了方便读者对Flutter有一个更深的了解,下面来看一下Fluter的历史:2014.10 - Flutter的前身Sky在GitHub上开源;2015.10 - 经过一年的开源,Sky正式改名为Flutter;2017.5 - Google I/O正式向外界公布了Flutter,这个时候Flutter才正式进去大家的视野;2018.6 - 距5月Google I/O 1个月的时间,Flutter1.0预览版;2018.12 - Flutter1.0发布,它的发布将大家对Flutter的学习和研究推到了一个新的起点;2019.2 - Flutter1.2发布主要增加对web的支持。通过Flutter的历史,可以看出Flutter正在逐渐的走向成熟和壮大,它的生态圈也在不断的发展,所以现在学习Flutter是一个非常的好时机。Flutter原理相比React Native和Weex,Flutter实现跨平台采用了更为彻底的方案(参考移动跨平台技术方案总结)。它既没有采用WebView也没有采用JavaScript,而是自己实现了一台UI框架,然后直接系统更底层渲染系统上画UI。所以它采用的开发语言不是JS,而Dart(Dart是面向对象的、类定义的、单继承的语言。它的语法类似C语言,可以转译为JavaScript,支持接口(interfaces)、混入(mixins)、抽象类(abstract classes)、具体化泛型(reified generics)、可选类型(optional typing)和sound type syste)。据称Dart语言可以编译成原生代码,直接跟原生通信,其原理模型图如下:同时,Flutter将UI组件和渲染器从平台移动到应用程序中,这使得它们可以自定义和可扩展。Flutter唯一要求系统提供的是canvas,以便定制的UI组件可以出现在设备的屏幕上,以及访问事件(触摸,定时器等)和服务(位置、相机等)。这是Flutter可以做到跨平台而且高效的关键。另外Flutter学习了RN的UI编程方式,引入了状态机,更新UI时只更新最小改变区域。系统的UI框架可以取代,但是系统提供的一些服务是无法取代的。Flutter在跟系统service通信方式,采用的是一种类似插件式的方式,或者有点像远程过程调用RPC方式,这种方式据说也要比RN的桥接方式高效。关于RN和Flutter到底谁更优秀,有兴趣的读者可以关注下官方的撕逼大战React Native 团队怎么看待 Flutter 的。Flutter 和 React Native 底层框架对比React-Native、Weex 核心是通过 Javascript 开发,执行时需要 Javascript 解释器,UI 是通过原生控件渲染。Flutter 与用于构建移动应用程序的其它大多数框架不同,因为 Flutter 既不使用 WebView,也不使用操作系统的原生控件。 相反,Flutter 使用自己的高性能渲染引擎来绘 制 widget。Flutter 使用 C、C ++、Dart 和 Skia(2D渲染引擎)构建。Skia 是一个 2D的绘图引擎库,其前身是一个向量绘图软件,Chrome 和 Android 均采用 Skia 作为绘图引擎。Android 自带了 Skia,所以 Flutter Android SDK要比 iOS SDK小很多。在 ReactNative 中,引入了虚拟 DOM 来减少DOM的回流和重绘,系统将虚拟 DOM 与真正的 DOM 进行比较,生成一组最小的更改,然后执行这些更改,以更新真正的 DOM。最后,平台重新绘制真实的 DOM 到画布中。React Native 是移动开发的一大进步,并且是 Flutter 的灵感来源,但 Flutter 更进一步。 在 Flutter 中,UI 组件和渲染器已经从平台中集成到用户的应用程序中。没有系统 UI 组件可以操作,所以原来虚拟控件树的地方现在是真实的控件树,Flutter 渲染 UI 控件树并将其绘制到平台画布上。如果说非要比较 Flutter 和 React Native的优势,可以参考下面几点:UI 一致性Flutter 因为是自己做的渲染,因此在iOS和Android的效果基本完全一致。 React Native存在将RN控件转换为对应平台原生控件的过程,存在一定的差异(如之前在调研里提到过的Button在iOS和Android下面显示效果不一样)。动态化技术Flutter使用的Dart语言,支持AOT和JIT两种模式,在Dev时候,通过JIT可以实现热重载,开发者可以即时的看到代码修改的效果。而在Release Build的时候,通过AOT事先编译,来最大化的优化性能。因此目前Flutter不支持代码的热更新,不过在Flutter 2019 产品路线图)可以看到这方面的消息。ReactNative 的代码通过加载 JSBundle.js执行,JSBundle.js可以保存在本地,也可以通过远程加载。目前有很多RN的热更新方案供选择。App体积Flutter iOS空项目 30M左右,Android空项目 7M左右。 (iOS需要额外集成Skia) React Native iOS空项目 3M左右,Android20M左右。(Android会加入OKHttp导致体积增大)Flutter 部分的底层功能在 Android 系统上已经有实现,因此 Android 上适配要好(RN在 Android 上有可能遇到兼容性问题)。Flutter的优势运行效率上,Flutter和ReactNative都可以达到理论上的60帧的刷新率,来实现「Native般的流畅体验」,Flutter是全Native在执行,基于底层代码(Android 上为 C++ with NDK,iOS 上为 C++ with LLVM),而ReactNative是Native控件 + JavaScript代码,实际性能上,Flutter应该优于ReactNative,据官方文档,Flutter可以在支持的设备上达到120FPS,而ReactNative的文档上,只提到了可以达到60FPS。兼容性上,Flutter 提供的 widget 都是基于 skia来实现和精心定制的,与具体平台没关,所以能保持很高的跨 os 跨 os version 的兼容性。 Flutter 从更基础的层去抹平平台差异,站在了更宽广、更可控的一个基础平台上去演变和发展。 Flutter 官方提供了大部分 Material Design 控件的实现(甚至比 Android Design Support 实现的更多)。Flutter开发语言Dart为什么要使用Dart语言学习Flutter就不得不提到Dart,那Flutter和Dart有什么关系?确实有关系,早期的Flutter团队评估了十多种语言,并选择了Dart,因为它符合他们构建用户界面的方式,读者可以去八卦下为什么要使用Dart语言的推文。Dart能成为Flutter不可或缺的一部分,根本原因还是因为其具有以下特性:1)Dart是AOT(Ahead Of Time)编译的,编译成快速、可预测的本地代码,使Flutter几乎都可以使用Dart编写。这不仅使Flutter变得更快,而且几乎所有的东西(包括所有的小部件)都可以定制;2)Dart也可以JIT(Just In Time)编译,开发周期异常快,工作流颠覆常规(包括Flutter流行的亚秒级有状态热重载);3)Dart可以更轻松地创建以60fps运行的流畅动画和转场。Dart可以在没有锁的情况下进行对象分配和垃圾回收。就像JavaScript一样,Dart避免了抢占式调度和共享内存(因而也不需要锁)。由于Flutter应用程序被编译为本地代码,因此它们不需要在领域之间建立缓慢的桥梁(例如,JavaScript到本地代码)。它的启动速度也快得多;4)Dart使Flutter不需要单独的声明式布局语言,如JSX或XML,或单独的可视化界面构建器,因为Dart的声明式编程布局易于阅读和可视化。所有的布局使用一种语言,聚集在一处,Flutter很容易提供高级工具,使布局更简单;5)开发人员发现Dart特别容易学习,因为它具有静态和动态语言用户都熟悉的特性。编译与执行历史上,计算机语言分为两组:静态语言(例如,Fortran和C,其中变量类型是在编译时静态指定的)和动态语言(例如,Smalltalk和JavaScript,其中变量的类型可以在运行时改变)。静态语言通常编译成目标机器的本地机器代码(或汇编代码)程序,该程序在运行时直接由硬件执行。动态语言由解释器执行,不产生机器语言代码。当然,事情后来变得复杂得多。虚拟机(VM)的概念开始流行,它其实只是一个高级的解释器,用软件模拟硬件设备。虚拟机使语言移植到新的硬件平台更容易。因此,VM的输入语言常常是中间语言。例如,一种编程语言(如Java)被编译成中间语言(字节码),然后在VM(JVM)中执行。另外,现在有即时(JIT)编译器。JIT编译器在程序执行期间运行,即时编译代码。原先在程序创建期间(运行时之前)执行的编译器现在称为AOT编译器。一般来说,只有静态语言才适合AOT编译为本地机器代码,因为机器语言通常需要知道数据的类型,而动态语言中的类型事先并不确定。因此,动态语言通常被解释或JIT编译。在开发过程中AOT编译,开发周期(从更改程序到能够执行程序以查看更改结果的时间)总是很慢。但是AOT编译产生的程序可以更可预测地执行,并且运行时不需要停下来分析和编译。AOT编译的程序也更快地开始执行(因为它们已经被编译)。相反,JIT编译提供了更快的开发周期,但可能导致执行速度较慢或时快时慢。特别是,JIT编译器启动较慢,因为当程序开始运行时,JIT编译器必须在代码执行之前进行分析和编译。研究表明,如果开始执行需要超过几秒钟,许多人将放弃应用。Dart的编译与执行在创造Dart之前,Dart团队成员在高级编译器和虚拟机上做了开创性的工作,包括动态语言(如JavaScript的V8引擎和Smalltalk的Strongtalk)以及静态语言(如用于Java的Hotspot编译器)。他们利用这些经验使Dart在编译和执行方面非常灵活。Dart是同时非常适合AOT编译和JIT编译的少数语言之一(也许是唯一的“主流”语言)。支持这两种编译方式为Dart和(特别是)Flutter提供了显著的优势。JIT编译在开发过程中使用,编译器速度特别快。然后,当一个应用程序准备发布时,它被AOT编译。因此,借助先进的工具和编译器,Dart具有两全其美的优势:极快的开发周期、快速的执行速度和极短启动时间。Dart在编译和执行方面的灵活性并不止于此。例如,Dart可以编译成JavaScript,所以浏览器可以执行。这允许在移动应用和网络应用之间重复使用代码。开发人员报告他们的移动和网络应用程序之间的代码重用率高达70%。通过将Dart编译为本地代码,或者编译为JavaScript并将其与node.js一起使用,Dart也可以在服务器上使用。最后,Dart还提供了一个独立的虚拟机(本质上就像解释器一样),虚拟机使用Dart语言本身作为其中间语言。Dart可以进行高效的AOT编译或JIT编译、解释或转译成其他语言。Dart编译和执行不仅非常灵活,而且速度特别快。AOT编译和“桥”前面讨论过一个有助于保持顺畅的特性,那就是Dart能AOT编译为本地机器码。预编译的AOT代码比JIT更具可预测性,因为在运行时不需要暂停执行JIT分析或编译。然而,AOT编译代码还有一个更大的优势,那就是避免了“JavaScript桥梁”。当动态语言(如JavaScript)需要与平台上的本地代码互操作时,它们必须通过桥进行通信,这会导致上下文切换,从而必须保存特别多的状态(可能会存储到辅助存储)。这些上下文切换具有双重打击,因为它们不仅会减慢速度,还会导致严重的卡顿。说明:即使编译后的代码也可能需要一个接口来与平台代码进行交互,并且这也可以称为桥,但它通常比动态语言所需的桥快几个数量级。另外,由于Dart允许将小部件等内容移至应用程序中,因此减少了桥接的需求。布局Dart的另一个好处是,Flutter不会从程序中拆分出额外的模板或布局语言,如JSX或XML,也不需要单独的可视布局工具。以下是一个简单的Flutter视图,用Dart编写:new Center(child: new Column(children: [ new Text(‘Hello, World!’), new Icon(Icons.star, color: Colors.green), ]))并且随着Dart 2的发布,上面的代码也变得越来越可读,因为new和const关键字变得可选,所以静态布局看起来像是用声明式布局语言编写的:Center(child: Column(children: [ Text(‘Hello, World!’), Icon(Icons.star, color: Colors.green), ]))至于,困扰原生开发人员的一个问题是:为什么缺乏专门的布局语言怎么会被称为优势呢?原生开发人员可以在下面的文章中找到答案:“为什么原生应用程序开发人员应认真看待Flutter”学习路线学习任何一门技术,最主要的渠道就是官方资料,由于是Google的产品,因此从一开始就受到很多开发者的喜爱,因此其社区建设也相对较快,读者可以现场Flutter中文社区了解一些Flutter开发的基础,然后再结合一些开源项目进行学习。Fluuter网上的学习资料也很多,可以参考下面的链接进行深入的学习:Flutter学习线路 ...

March 16, 2019 · 1 min · jiezi

Flutter系列:4.基于注解的代码生成应用

前言api数据序列化为model实例是移动开发中很常见也是很基础的技术点,得益于运行时等动态技术在ios开发中我们可以借助JSONModel或者SwiftyJSON很方便的实现序列化,对于刚刚接触flutter的开发者来说其序列化体验无疑是非常糟糕的。本身Dart语言是支持反射的,但是在Flutter中,Dart几乎放弃了脚本语言动态化的特性,如不支持反射、也不支持动态创建函数等;所以序列化只有依靠拦截注解来动态生成代码的方式实现。注解注解是一种可以为代码提供一些语义信息或元数据的标注,这在其他语言中也很常见,在dart中常见的注解有@deprecated、@override等,注解是以@开头的,他们可以作用于类,函数,属性等。dart中自定义注解很简单,其实现就是一个带有const构造函数的类library todo;class Todo { final String who; final String what; const Todo(this.who, this.what);}然后就可以这样使用Todo这个注解了import ’todo.dart’;@Todo(‘seth’, ‘make this do something’)void doSomething() { print(‘do something’);}source_gen通过注解的方式我们就可以为类或者属性添加一个额外的数据信息,source_gen可以拦截注解获取并解析上下文信息,通过解析注解实现source_gen的相关Generator就可以动态的生成代码了;source_gen是封装自build和 analyzer,并在此基础上提供友好的api封装。build是一个提供构建控制的库,analyzer是提供dart语法静态分析功能的库,source_gen将其整合便可以实现一套基于注解的代码生成工具。代码生成使用Annotation+source_gen的方式可以便捷的生成代码,source_gen通过拦截Annotation,解析其上下文element然后通过builder即可动态生成代码,下面简易的代码生成Demo。创建package终端运行:flutter create –template=package code_gen_demovscode打开刚刚创建的package, pubspec.yaml添加source_gen和build_runner依赖dependencies: flutter: sdk: flutter source_gen: ‘>=0.8.0’lib目录下创建注解mark.dartclass Mark { final String name; const Mark({this.name});}创建代码生成器generator.dart 负责拦截我们的注解Mark, 解析注解的类名称,路径及其参数name并返回import ‘package:analyzer/dart/element/element.dart’;import ‘package:source_gen/source_gen.dart’;import ‘package:build/build.dart’;import ‘mark.dart’;class MarkGenerator extends GeneratorForAnnotation<Mark> { @override generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { String className = element.displayName; String path = buildStep.inputId.path; String name =annotation.peek(’name’).stringValue; return “//$className\n//$path\n//$name”; }}lib目录创建构建器builder.dart, 添加一个顶级方法markBuilder供build runner解析调用import ‘package:source_gen/source_gen.dart’;import ‘package:build/build.dart’;import ‘mark_generator.dart’;Builder markBuilder(BuilderOptions options) => LibraryBuilder(MarkGenerator(), generatedExtension: ‘.mark.dart’);在package根目录下添加build.yaml文件(buildRunner会解析其配置执行builder指定的方法),配置成刚刚创建的builder内容如下targets: $default: builders: code_gen_demo|mark_builder: enabled: truebuilders: mark_builder: import: ‘package:code_gen_demo/builder.dart’ builder_factories: [‘markBuilder’] build_extensions: { ‘.dart’: [’.mark.dart’] } auto_apply: root_package build_to: sourceimport指定了builder的位置,builder_factories指定了builder的具体调用,build_extensions指定了输入输入文件的格式匹配,此列会生成".mark.dart"结尾的文件。至此代码生成相关的Annotation、 builder和Generator都准备好了,接下来我们创建example工程来做示例创建example工程在package的根目录下创建example工程,example是一个完整的flutter工程,执行命令:flutter create example在example工程中引入我们的package, 在example的pubspec.yaml中添加依赖package,以及添加对builder_runner的依赖来执行编译命令dependencies: flutter: sdk: flutter code_gen_demo: path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2dev_dependencies: flutter_test: sdk: flutter build_runner: ‘>=0.9.1’创建一个示例类,mark_demo.dart, 并添加Mark注解import ‘package:code_gen_demo/mark.dart’;@Mark(name: “hello”)class MarkDemo { }好了,接下来在example目录下执行builder runner命令来为Mark注解的mark_demo.dart生成一个相关代码mark_demo.mark.dartflutter packages pub run build_runner build –delete-conflicting-outputs重新执行run builder_runner前最好先clean一下flutter packages pub run build_runner clean命令执行完成后就可以看到在mark_demo.dart文件下生成了一个mark_demo.mark.dart的文件,其内容是mark_generator.dart中为Mark这个注解创建的Generator返回的内容:// GENERATED CODE - DO NOT MODIFY BY HAND// **************************************************************************// MarkGenerator// **************************************************************************//MarkDemo//lib/mark_demo.dart//hello本demo源码位置GitHubeasy_router目前在Flutter中常见的代码生成主要应用在json序列化库json_serializable中,在国内闲鱼技术团队使用这一技术实现了一套router的路由映射解决方案annotation_route,感兴趣的可以看看。作为学习我参考了闲鱼的annotation_route实现了一个简单的Flutter页面路由匹配方案easy_router,不同于闲鱼annotation_route的复杂和全面,简单实现路由url的匹配、参数解析赋值并返回page实例。easy_router源码戳我使用方式使用@EasyRoute来注解需要加入Router的page, url作为page的唯一标识,例如@EasyRoute(url: “easy://flutter/pagea”)class PageA extends StatefulWidget { final EasyRouteOption routeOption; PageA(this.routeOption); @override _PageAState createState() => _PageAState();}easy_router会调用page的构造函数并传入EasyRouteOption参数,所以每个page都应该有一个这样的构造函数,如果url有参数,参数会放到EasyRouteOption对象的params属性中,以便page获取。使用@easyRouter来注解你的router, 这样就会生成router相关的内部逻辑, 例如import ‘package:example/route.router.internal.dart’;import ‘package:easy_router/route.dart’;@easyRouterclass Router { EasyRouterInternal internalImpl = EasyRouterInternalImpl(); dynamic getPage(String url) { EasyRouteResult result = internalImpl.router(url); if(result.state == EasyRouterResultState.NOT_FOUND) { print(“Router error: page not found”); return null; } return result.widget; } }EasyRouterInternalImpl就是最终生成的router实现, 执行命令生成EasyRouterInternalImpl实现flutter packages pub run build_runner build –delete-conflicting-outputs调用router打开url对应的pageMaterialButton( child: Text(‘ToPageA’), onPressed: (){ Navigator.of(context).push( MaterialPageRoute( builder: (context) { return Router().getPage(’easy://flutter/pagea?parama=a’); } ) ); },),感兴趣自己改改,详细使用参看源码example实现方式routeParseBuilder:负责解析@EasyRoute注解的page页面,完成page和url的映射关系routerBuilder:读取routeParseBuilder生成的映射,完成对EasyRouterInternalImpl写入,依赖mustache4dart库完成替换写入 ...

March 14, 2019 · 2 min · jiezi

Flutter Http库Dio 2.1正式发布

Flutter Http库Dio 2.1正式发布Dio 是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等。目前Dio在pub上综合得分100分,排名已上榜pub首页(All Tab下) !同时Dio也是Github上最受欢迎的Flutter第三方库,项目地址:Dio-Github。在1.0发布至今,Dio受到了大量国内外开发者的关注,并收到了很多肯定和建议。为了让Dio功能更强大、让开发者使用起来更容易,我们综合了1.0中的各种反馈,对Dio进行了一次大的更新,为了让使用者在1.0和2.x之间有个过渡,我们将2.0.x-2.1.0作为预发布版在全网进行了接近两个月的公测。现在,很高兴的告诉大家,2.x的功能已经收敛、质量已经稳定,因此,今天我们正式发布Dio 2.x的第一个稳定版Dio v2.1.0。相比1.x,2.x在Restful API、拦截器、FormData等很多地方都进行了扩展和调整,除了这些,Dio在2.x中还引入Adapter层,为Mock接口数据和自定义底层网络库提供了支持。整体功能相比1.x有了很大的提升,因此我们强烈建议所有1.x用户都能升级到2.1。Dio V2.1.x 变更列表Restful API2.1中对所有Restful API的变化有:支持Uri,在1.x中,Url只能是字符串,2.1中所有API都提供了对应支持Uri的版本,如get方法有dio.get(…)和dio.gerUri(…)。所有方法都支持queryParameters,2.1标准化了参数语义,并允许所有请求都可以传query,而data只针对可以提交请求体的方法如post作为请求体提交。另外相对于Uri.queryParameters,我们对Restful API中的queryParameters的功能做了加强,主要有两个差异:参数值类型不同;前者只能接受Map<String, String|Iterable<String>>类型的参数,而后者可以接受Map<String, dynamic>类型,比如:dio.getUri(Uri(url, queryParameters: {“age”:15})) //会抛出异常,Uri.queryParameter的value不能是int类型dio.get(url, queryParameters: {“age”:15}); //这是OK的!编码方式有所差异; Uri.queryParameters编码方式遵循Dart SDK中的规则,而Restful API中的queryParameters编码方式和jQuery一致,如: dio.options.baseUrl=“http://domain.com/"; //下面请求的最终uri为:http://domain.com/api?selectedId=1&selectedId=2 Response response = await dio.getUri( Uri(path: “api”,queryParameters: {“selectedId”: [“1”, “2”],}); ); //下面请求的最终uri为:https://flutterchina.club?selectedId%5B%5D=1&selectedId%5B%5D=2 dio.get(“api”,queryParameters: {“selectedId”: [“1”, “2”], });支持以Stream方式提交数据了;2.1中可以通过Stream的方式来提交二进制数据了,详细的示例可以参考这里。支持以二进制数组形式接收数据了;1.x中如果要以二进制形式接收响应数据则需要设置options.responseType为ResponseType.stream 来接收响应流,然后再通过读取响应流来获取完整的二进制内容,而2.x中只需要设置为ResponseType.bytes,则可直接获得响应流的而精致数组。API统一添加了onSendProgress 和 onReceiveProgress 两个回调,用于监听发送数据和接收数据的具体精度,在1.x中只有在下载文件和上传formdata时才能监听进度,而2.x中所有接口都可以了。拦截器支持设置多个拦截器;这样我们就可以将一些功能单独抽离,比如打印请求/响应日志和cookie管理都可以单独封装在一个拦截器中,这样在解耦的同时可以提高代码的可复用度。2.1中拦截器是一个队列,拦截器将会按照FIFO顺序执行,如果队列中的某个拦截器返回了Response或Error,则请求结束,队列后面的拦截器将不会再被执行。预置了打印请求/响应日志的LogInterceptor和管理cookie的CookieManager拦截器,开发者可以按需使用,如: dio.interceptors ..add(LogInterceptor(responseBody: false)) ..add(CookieManager(CookieJar()));FormData1.x中,在提交FormData时会先将FormData转成一个二进制数组,然后再提交,这在FormData中的数据量比较大时(如包含多个大文件)在上传的过程中会比较占用内存。2.1中我们队FormData进行了增强,给FormData添加一个stream属性,它可以将FormData转为一个stream,在提交时无需一次性加载到内存。同时FormData也添加了asBytes() 、asBytesAsync()、length等方法、属性。ResponseResponse中添加了一些关于重定向信息的字段,有isRedirect、redirects、realUri。TransFormer2.x中对于DefaultTransformer添加了一个jsonDecodeCallback,通过它可以定制json解码器,这在flutter中非常有用,我们可以通过compute方法来在后台进行json解码,从而避免在UI线程对复杂json解码时引起的界面卡顿,详情请见这里 。HttpClientAdapterHttpClientAdapter是 Dio 和 HttpClient之间的桥梁。2.0抽象出了adapter层,可以带来两个主要收益:实现Dio于HttpClient的解耦,这样可以方便的切换、定制底层网络库。可以Mock数据;Dio实现了一套标准的、强大API,而HttpClient则是真正发起Http请求的对象,两者并不是固定的一对一关系,我们完全可以在使用Dio时通过其他网络库(而不仅仅是dart HttpClient )来发起网络请求。我们通过HttpClientAdapter将Dio和HttpClient解耦,这样一来便可以自由定制Http请求的底层实现,比如,在Flutter中我们可以通过自定义HttpClientAdapter将Http请求转发到Native中,然后再由Native统一发起请求。再比如,假如有一天OKHttp提供了dart版,你想使用OKHttp发起http请求,那么你便可以通过适配器来无缝切换到OKHttp,而不用改之前的代码。Dio 使用DefaultHttpClientAdapter作为其默认HttpClientAdapter,DefaultHttpClientAdapter使用dart:io:HttpClient 来发起网络请求。这里 有一个简单的自定义Adapter的示例,读者可以参考。另外本项目的自动化测试用例全都是通过一个自定义的MockAdapter来模拟服务器返回数据的。OptionsOptions对象包含了对网络请求的配置,在1.x中无论是实例配置还是单次请求的配置都使用的是Options 对象,这样会带来一些二义性,甚至有时会让开发者感到疑惑,比如Options.baseUrl属性代表请求基地址,理论上它只应该在实例配置中设置,而不应该出现在每次请求的配置中;再比如Options.path属性,它代表请求的相对路径,不应该在实例请求配置中。2.1中将请求配置分拆成三个类:类名作用BaseOptionsDio实例基配置,默认对该dio实例的所有请求生效Options单次请求配置,可以覆盖BaseOptions中的同名属性RequestOptions请求的最终配置,是对Option和BaseOptions合并后的另外,添加了一些新的配置项:cookies:可以添加一些公共cookiereceiveDataWhenStatusError:当响应状态码不是成功状态(如404)时,是否接收响应内容,如果是false,则response.data将会为nullmaxRedirects: 重定向最大次数。

March 12, 2019 · 1 min · jiezi

Flutter Widgets入门(一):MaterialApp 和 Scaffold

1 MaterialApp1.1 什么是MaterialApp?MaterialApp是我们使用 Flutter开发中最常用的符合Material Design设计理念的入口Widget。你可以将它类比成为网页中的<html></html>,且它自带路由、主题色,<title>等功能。1.2 MaterialApp的几个属性1.2.1 titleStrig类型,该属性会在Android应用管理器的App上方显示,对于iOS设备是没有效果的。如下面代码所示:import ‘package:flutter/material.dart’;void main() { runApp(MaterialApp( title: ‘一个Flutter应用’, home: Text(‘hello flutter’, style: TextStyle( color: Colors.white, decoration: TextDecoration.none))));}1.2.2 homeWidget类型,这是在应用程序正常启动时首先显示的Widget,除非指定了initialRoute。如果initialRoute显示失败,也该显示该Widget。需要注意的是, 如果你指定了home属性,则在routes的路由表中不允许出现/的命名路由。import ‘package:flutter/material.dart’;void main() { runApp(MaterialApp( title: ‘一个Flutter应用’, home: Center( child: Text(‘hello flutter’, style: TextStyle( color: Colors.white, decoration: TextDecoration.none)), )));}1.2.3 routesMap<String, WidgetBuilder>类型,是应用的顶级路由表。当我们再使用Navigator.pushNamed进行命名路由的跳转时,会在此路表中进行查找并跳转。如果你的应用程序只有一个页面,则无需使用routes,直接指定home对应的Widget即可。下面的例子中,定义了两个路由:/home和/detail,并使用GestureDetector定义了点击事件已实现路由跳转:import ‘package:flutter/material.dart’;void main() { runApp(MaterialApp(title: ‘一个Flutter应用’, home: HomePage(), routes: { ‘/home’: (BuildContext context) => HomePage(), ‘/detail’: (BuildContext context) => DetailPage() }));}class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: GestureDetector( onTap: () { Navigator.pushNamed(context, ‘/detail’); }, child: Text(‘首页,点击跳转详情页’, style: TextStyle( fontSize: 20.0, color: Colors.white, decoration: TextDecoration.none)))); }}class DetailPage extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: GestureDetector( onTap: () { Navigator.pushNamed(context, ‘/home’); }, child: Text(‘详情页,点击跳转首页’, style: TextStyle( fontSize: 20.0, color: Colors.white, decoration: TextDecoration.none)))); }}2 Scaffold2.1 什么是Scaffold?Scaffold通常被用作MaterialApp的子Widget,它会填充可用空间,占据整个窗口或设备屏幕。Scaffold提供了大多数应用程序都应该具备的功能,例如顶部的appBar,底部的bottomNavigationBar,隐藏的侧边栏drawer等。2.2 Scaffold的几个属性2.2.1 appBarPreferredSizeWidget类型,显示在Scaffold的顶部区域。import ‘package:flutter/material.dart’;void main() { runApp(MaterialApp( title: ‘一个Flutter应用’, home: Scaffold( appBar: AppBar( title: Text(‘首页’)) ) ) );}2.2.2 drawerWidget drawer类型,通常用来形成一个汉堡包按钮显示其侧边栏。import ‘package:flutter/material.dart’;void main() { runApp(MaterialApp( title: ‘一个Flutter应用’, home: Scaffold( appBar: AppBar(title: Text(‘首页’)), drawer: Drawer( child: Column( children: <Widget>[ DrawerItem(1, ‘列表1’), DrawerItem(2, ‘列表2’), DrawerItem(3, ‘列表3’), DrawerItem(4, ‘列表4’), DrawerItem(5, ‘列表5’) ], )))));}class DrawerItem extends StatelessWidget { final int id; final String name; DrawerItem(this.id, this.name); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: Colors.white, border: Border(bottom: BorderSide(width: 0.5, color: Color(0xFFd9d9d9))), ), height: 52.0, child: FlatButton( onPressed: () {}, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[Text(id.toString()), Text(’ - ‘), Text(name)], )), ); }}2.2.3 bottomNavigationBarWidget bottomNavigationBar类型,用户显示底部的tab栏,items必须大于2个。import ‘package:flutter/material.dart’;void main() { runApp(MaterialApp( title: ‘一个Flutter应用’, home: Scaffold( appBar: AppBar( title: Text(‘首页’), ), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: 1, items: [ new BottomNavigationBarItem( icon: Icon(Icons.account_balance), title: Text(‘银行’)), new BottomNavigationBarItem( icon: Icon(Icons.contacts), title: Text(‘联系人’)), new BottomNavigationBarItem( icon: Icon(Icons.library_music), title: Text(‘音乐’)) ], ))));}2.2.4 bodyWidget类型,Scaffold的主题内容。import ‘package:flutter/material.dart’;void main() { runApp(MaterialApp( title: ‘一个Flutter应用’, home: Scaffold( appBar: AppBar( title: Text(‘首页’), ), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: 1, items: [ new BottomNavigationBarItem( icon: Icon(Icons.account_balance), title: Text(‘银行’)), new BottomNavigationBarItem( icon: Icon(Icons.contacts), title: Text(‘联系人’)), new BottomNavigationBarItem( icon: Icon(Icons.library_music), title: Text(‘音乐’)) ], ), body: Center( child: Text(‘这是联系人页面’), ), )));} ...

March 9, 2019 · 2 min · jiezi

首个稳定更新版 —— Flutter 1.2 发布

2019 世界移动通信大会 (MWC 大会) 于 2 月 27 日在巴塞罗那顺利拉开帷幕。值此移动盛会,Flutter 团队宣布正式推出 Flutter 1.2。其实,这个大会对 Flutter 有着特别的纪念意义,因为 Flutter 的首个 beta 测试版正是在去年的 MWC 大会上与大家见面的,自此以后,Flutter 的发展速度远超我们的想象。如今我们再次聚首 MWC 大会,发布 Flutter 稳定版本的首个更新,以此庆祝 Flutter 诞生一周年。Flutter 1.2作为 Flutter 1.0 之后的首次更新, Flutter 1.2 围绕以下点进行了重点优化与改进:提升核心框架的稳定性、性能和质量改进现有 widget 视觉效果和功能为 Flutter 开发者提供全新的基于 Web 的调试工具自 Flutter 1.0 发布已经过去几个月了,我们在这段时间内集中精力改进了测试和代码基础框架,解决了此前积压的 pull requests,并全面提升了框架的质量与性能。有兴趣的开发者们可以前往 Flutter wiki 页面,查看完整的 pull requests 列表。此外,我们还在这次更新中加强了对 Swahili 等新 UI 设计语言的支持。我们将继续改进 Material 和 Cupertino 系列的 widgets,为开发者提供更加灵活的 Material 设计体验,并持续在 iOS 设备上继续交付完美的像素保真度。为此,我们添加了对浮动光标文本编辑的支持,并且对许多细节进行了进一步优化 (例如,我们更新了文本编辑光标在 iOS 设备上的绘制方式,以便真实呈现动画和绘图顺序)。受 Robert Penner 作品的启发,我们扩展了动画缓动函数的支持范围。此外,Flutter 1.2 还引入了全新的键盘事件和鼠标悬停支持,以作好准备为桌面级操作系统提供深层支持。与此同时,Flutter 插件团队也在积极展开针对 Flutter 1.2 发布的相关优化工作,主要负责实现 应用内购买 支持,以及修复视频播放器 (video player)、webview 和 地图 (maps) 中的一些错误。另外,我们还合并了一个来自 Intuit 工程师提交的 pull request,在 Flutter 中添加了 Android App Bundles 支持。Android App Bundles 是一种新的封装格式,它能有效减小应用的体积并启动应用动态交付等新特性。最后,Flutter 1.2 还包含了 Dart 2.2 SDK,此项更新为代码编译带来了显著的性能提升,并且为初始化集合提供了新语言支持。更多信息,请阅读《Dart 2.2 发布说明》。特别说明: 有些读者或许会好奇为什么这个版本的编号是 1.2,请允许我在这里稍作解释。我们的目标是大概每个月向 “测试版” 渠道发布 1.x 版本的 Flutter,然后每季度向 “稳定版” 渠道发布可在生产环境下使用的更新版本。上个月发布的 1.1 是测试版本,因此 1.2 是我们的首个稳定更新版本。新的调试工具每位开发者都有着不同的技术背景,偏爱的编程工具和编辑器也不尽相同。为此,Flutter 添加了多种工具支持,其中包括 Android Studio 和 Visual Studio Code 的 一级支持,以及支持命令行构建工具,这也就意味着开发者需要更加灵活的调试和运行时检查工具。所以我们在发布 Flutter 1.2 的同时,还带来了全新的基于 Web 的调试工具套件,目的是帮助您更好地分析与调试应用性能。这些工具支持与 Visual Studio Code 和 Android Studio 的扩展程序及加载项一同安装,并且提供多种功能:Widget 检查器: 对 Flutter 用于渲染的树状分级结构实现可视化和直观的探索;时间线视图: 可帮助您逐帧诊断自己的应用,并识别可能造成应用动画 “卡顿” 的渲染和计算问题;源代码级调试器: 支持单步执行代码,设置断点并检查调用堆栈;日志记录视图: 显示应用所记录的活动以及网络、框架和垃圾回收等事件。为了给 Flutter 和 Dart 开发者创造更好的开发体验,我们将进一步加大对基于 web 的调试工具的投入。此外,随着 web 集成技术的不断发展,我们还计划将这些服务直接添加到 Visual Studio Code 等工具中。下一步工作发布 Flutter 1.0 之后,除了日常开发工作之外,我们还规划了 Flutter 2019 产品路线图,从中您会发现我们未来仍很多工作要做。2019 年的一个工作重点是将 Flutter 的应用范围扩展到移动平台之外。我们在 Flutter Live 上启动了 Hummingbird 计划,加快推进 Flutter 在 Web 端的发展。我们会接下来的几个月里公布该项目的初步技术成果,请大家拭目以待!另外,我们还计划将 Flutter 引入到桌面开发中。因此,除了上述框架层面的开发工作之外,我们还会通过 Flutter 跨平台桌面应用计划 (Flutter Desktop Embedding Project) 帮助各位开发者在 Windows 和 Mac 等操作系统上封装和部署应用。Flutter Create: 您能使用 5K 的 Dart 代码做些什么?Flutter Create 挑战赛将从本周起开始接收报名,你敢来参加吗?参赛者需要利用 Flutter 构建充满创意和趣味的精美应用,并把这一切全部浓缩到 5K 的 Dart 代码里。5K 并不多,按照普通 MP3 格式的标准来算,差不多相当于三分之一秒的音乐。但我们敢说,有了 Flutter 的帮助,即使是使用如此少量的代码,您也能制作出令人大开眼界的应用。挑战赛将于 4 月 7 日结束,因此您将有几周的时间来构建出色应用。我们准备了一些很棒的奖品,其中包括一台搭载 14 核处理器和 128GB 内存的顶配版 iMac Pro 工作站,价值超过 10,000 美元!我们将在 Google I/O 大会上宣布获胜者名单,并且还会在此期间开展多个 Flutter 演讲、Codelab 课程和活动,敬请期待!结语Flutter 现已进入 Github Top 20 软件库,与此同时,Flutter 全球社区也在以惊人的速度蓬勃发展,为世界各地的开发者正带去独特的编程乐趣——印度清奈的开发者聚会,尼日利亚哈科特港的报道,丹麦哥本哈根的应用,以及美国纽约的孵化工作室 —— 从中我们可以清楚地看到 Flutter 正在成为一种全球现象,而这一切都离不开您的贡献!Flutter 作为移动开发领域一股不容小觑的新生力量,不仅为开发者赢得了亿万用户,还帮助创业者把理念推向市场。我们非常高兴看到您拥有如此多的创意,也希望能够帮助您使用 Flutter 来呈现这些创意。在印度 SRM 大学参加 Flutter 高级研讨会的与会者我们最近还在 YouTube 网站上专门为 Flutter 开设了一个新频道。欢迎前来 flutter.dev/youtube 进行订阅观看!这个频道包含了大家非常喜爱的一些视频合集如 Boring Flutter Development Show、Widget of the Week 和 Flutter in Focus,同时也欢迎前来学习 Dream11 是如何使用 Flutter 的 ,以及 其他的开发者故事等。 ...

March 7, 2019 · 2 min · jiezi

刚刚,阿里宣布开源Flutter应用框架Fish Redux!

3月5日,闲鱼宣布在GitHub上开源Fish Redux,Fish Redux是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用,它最显著的特征是 函数式的编程模型、可预测的状态管理、可插拔的组件体系、最佳的性能表现。下文中,我们将详细介绍Fish Redux的特点和使用过程,以下内容来自InfoQ独家对闲鱼Flutter团队的采访和Fish Redux的开源文档。开源背景在闲鱼接入Flutter之初,由于我们的落地的方案希望是从最复杂的几个主链路进行尝试来验证flutter完备性的,而我们的详情整体来讲业务比较复杂,主要体现在两个方面:页面需要集中状态管理,也就是说页面的不同组件共享一个数据来源,数据来源变化需要通知页面所有组件。页面的UI展现形式比较多(如普通详情、闲鱼币详情、社区详情、拍卖详情等),工作量大,所以UI组件需要尽可能复用,也就是说需要比较好的进行组件化切分。在我们尝试使用市面上已有的框架(google提供的redux以及bloc)的时候发现,没有任何一个框架可以既解决集中状态管理,又能解决UI的组件化的,因为本身这两个问题有一定的矛盾性(集中vs分治)。因此我们希望有一套框架能解决我们的问题,fish redux应运而生。fish redux本身是经过比较多次的迭代的,目前大家看到的版本经过了3次比较大的迭代,实际上也是经过了团队比较多的讨论和思考。第一个版本是基于社区内的flutter_redux进行的改造,核心是提供了UI代码的组件化,当然问题也非常明显,针对复杂的详情和发布业务,往往业务逻辑很多,无法做到逻辑代码的组件化。第二个版本针对第一个版本的问题,做出了比较重大的修改,解决了UI代码和逻辑代码的分治问题,但同时,按照redux的标准,打破了redux的原则,对于精益求精的闲鱼团队来讲,不能接受;因此,在第三个版本进行重构时,我们确立了整体的架构原则与分层要求,一方面按照reduxjs的代码进行了flutter侧的redux实现,将redux的原则完整保留下来。另一方面针对组件化的问题,提供了redux之上的component的封装,并创新的通过这一层的架构设计提供了业务代码分治的能力。至此,我们完成了fish redux的基本设计,但在后续的应用中,发现了业务组装以后的代码性能问题,针对该问题,我们再次提供了对应的adapter能力,保障了在长列表场景下的big cell问题。目前,fish redux已经在线上稳定运行超过3个月以上,未来,期待fish redux给社区带来更多的输入。Fish Redux技术解析分层架构图架构图:主体自底而上,分两层,每一层用来解决不通层面的问题和矛盾,下面依次来展开。ReduxRedux 是来自前端社区的一个数据管理框架,对 Native开发同学来说可能会有一点陌生,我们做一个简单的介绍。Redux 是做什么的?Redux 是一个用来做可预测易调试的数据管理的框架。所有对数据的增删改查等操作都由 Redux 来集中负责。Redux 是怎么设计和实现的?Redux 是一个函数式的数据管理的框架。传统 OOP 做数据管理,往往是定义一些 Bean,每一个 Bean 对外暴露一些 Public-API 用来操作内部数据(充血模型)。函数式的做法是更上一个抽象的纬度,对数据的定义是一些 Struct(贫血模型),而操作数据的方法都统一到具有相同函数签名 (T, Action) => T 的 Reducer 中。FP:Struct(贫血模型) + Reducer = OOP:Bean(充血模型)同时 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 机制,给框架带了极高的灵活性和扩展性。贫血模型、充血模型请参考:https://en.wikipedia.org/wiki/Plain_old_Java_objectRedux 的缺点Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点。在我们实际使用 Redux 中面临两个具体问题:Redux 的集中和 Component 的分治之间的矛盾;Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。Fish Redux 的改良Fish Redux 通过 Redux 做集中化的可观察的数据管理。然不仅于此,对于传统 Redux 在使用层面上的缺点,在面向端侧 flutter 页面纬度开发的场景中,我们通过更好更高的抽象,做了改良。一个组件需要定义一个数据(Struct)和一个 Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,我们解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。我们得到了理想的集中的效果和分治的代码。对社区标准的 followState、Action、Reducer、Store、Middleware 以上概念和社区的 ReduxJS 是完全一致的。我们将原汁原味地保留所有的 Redux 的优势。如果想对 Redux 有更近一步的理解,请参考:https://github.com/reduxjs/reduxComponent组件是对局部的展示和功能的封装。 基于 Redux 的原则,我们对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用 Effect)。于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的行为。这是一种面向当下,也面向未来的拆分。在面向当下的 Redux 看来,是数据管理和其他。在面向未来的 UI-Automation 看来是 UI 表达和其他。UI 的表达对程序员而言即将进入黑盒时代,研发工程师们会把更多的精力放在非修改数据的行为、修改数据的行为上。组件是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。关于 ViewView 仅仅是一个函数签名: (T,Dispatch,ViewService) => Widget它主要包含三方面的信息视图是完全由数据驱动。视图产生的事件/回调,通过 Dispatch 发出“意图”,不做具体的实现。需要用到的组件依赖等,通过 ViewService 标准化调用。比如一个典型的符合 View 签名的函数。关于 EffectEffect 是对非修改数据行为的标准定义,它是一个函数签名: (Context, Action) => Object它主要包含四方面的信息接收来自 View 的“意图”,也包括对应的生命周期的回调,然后做出具体的执行。它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们不崇尚持有数据,而通过上下文来获取最新数据。它不修改数据, 如果修要,应该发一个 Action 到 Reducer 里去处理。它的返回值仅限于 bool or Future, 对应支持同步函数和协程的处理流程。比如良好的协程的支持:关于 ReducerReducer 是一个完全符合 Redux 规范的函数签名:(T,Action) => T一些符合签名的 Reducer:同时我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。所以有这样的公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。一个典型的组装:通过 Component 的抽象,我们得到了完整的分治,多纬度的复用,更好的解耦。AdapterAdapter 也是对局部的展示和功能的封装。它为 ListView 高性能场景而生,它是 Component 实现上的一种变化。它的目标是解决 Component 模型在 flutter-ListView 的场景下的 3 个问题:1)将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化;2)Component 无法区分 appear|disappear 和 init|dispose ;3)Effect 的生命周期和 View 的耦合,在 ListView 的场景下不符合直观的预期。概括的讲,我们想要一个逻辑上的 ScrollView,性能上的 ListView ,这样的一种局部展示和功能封装的抽象。做出这样独立一层的抽象是我们看实际的效果,我们对页面不使用框架Component,使用框架 Component+Adapter 的性能基线对比。Reducer is long-lived, Effect is medium-lived, View is short-lived.我们通过不断的测试做对比,以某 Android机为例:使用框架前 我们的详情页面的 FPS,基线在 52FPS;使用框架, 仅使用 Component 抽象下,FPS 下降到 40, 遭遇“Big-Cell”的陷阱;使用框架,同时使用 Adapter 抽象后,FPS 提升到 53,回到基线以上,有小幅度的提升。Directory推荐的目录结构会是这样sample_page– action.dart– page.dart– view.dart– effect.dart– reducer.dart– state.dartcomponentssample_component– action.dart– component.dart– view.dart– effect.dart– reducer.dart– state.dart上层负责组装,下层负责实现, 同时会有一个插件提供, 便于我们快速填写。以闲鱼的详情场景为例的组装:组件和组件之间,组件和容器之间都完全的独立。Communication Mechanism组件|适配器内通信组件|适配器间内通信简单的描述:采用的是带有一段优先处理的广播, self-first-broadcast。发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的所有的通信诉求。Refresh Mechanism数据刷新局部数据修改,自动层层触发上层数据的浅拷贝,对上层业务代码是透明的。层层的数据的拷贝:一方面是对 Redux 数据修改的严格的 follow。另一方面也是对数据驱动展示的严格的 follow。视图刷新扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新。Fish Redux的优点数据的集中管理通过 Redux 做集中化的可观察的数据管理。我们将原汁原味地保留所有的 Redux 的优势,同时在 Reducer 的合并上,变成由框架代理自动完成,大大简化了使用 Redux 的繁琐度。组件的分治管理组件既是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。View、Reducer、Effect 隔离将组件拆分成三个无状态的互不依赖的函数。因为是无状态的函数,它更易于编写、调试、测试、维护。同时它带来了更多的组合、复用和创新的可能。声明式配置组装组件、适配器通过自由的声明式配置组装来完成。包括它的 View、Reducer、Effect 以及它所依赖的子项。良好的扩展性核心框架保持自己的核心的三层关注点,不做核心关注点以外的事情,同时对上层保持了灵活的扩展性。框架甚至没有任何的一行的打印的代码,但我们可通过标准的 Middleware 来观察到数据的流动,组件的变化。在框架的核心三层外,也可以通过 dart 的语言特性 为 Component 或者 Adapter 添加 mixin,来灵活的组合式地增强他们的上层使用上的定制和能力。框架和其他中间件的打通,诸如自动曝光、高可用等,各中间件和框架之间都是透明的,由上层自由组装。精小、简单、完备它非常小,仅仅包含 1000 多行代码;它使用简单,完成几个小的函数,完成组装,即可运行;它是完备的。关于未来开源之后,闲鱼打算通过以下方式来维护Fish Redux:通过后续的一系列的对外宣传,吸引更多的开发者加入或者使用。目前Flutter生态里,应用框架还是空白,有机会成为事实标准;配合后续的一系列的闲鱼Flutter移动中间件矩阵做开源;进一步提供,一系列的配套的开发辅助调试工具,提升上层Flutter开发效率和体验。Fish Redux 目前已在阿里巴巴闲鱼技术团队内多场景,深入应用。最后 Talk is cheap, Show me the code,我们今天正式在GitHub上开源,更多内容,请到GitHub了解。GitHub地址:https://github.com/alibaba/fish-redux本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 7, 2019 · 2 min · jiezi

跟着官网学第一个flutter应用

第一个flutter应用import ‘package:flutter/material.dart’;import ‘package:english_words/english_words.dart’;void main() => runApp(new MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: ‘Welcome to Flutter’, home: new RandomWords(), theme: new ThemeData( primaryColor: Colors.red, ), ); }}class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState();}class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = new Set<WordPair>(); final _biggerFont = const TextStyle(fontSize: 18.0); Widget _buildSuggestions() { return new ListView.builder( padding: const EdgeInsets.all(16.0), // 对于每个建议的单词对都会调用一次itemBuilder,然后将单词对添加到ListTile行中 // 在偶数行,该函数会为单词对添加一个ListTile row. // 在奇数行,该函数会添加一个分割线widget,来分隔相邻的词对。 // 注意,在小屏幕上,分割线看起来可能比较吃力。 itemBuilder: (context, i) { // 在每一列之前,添加一个1像素高的分隔线widget if (i.isOdd) return new Divider(); // 语法 “i ~/ 2” 表示i除以2,但返回值是整形(向下取整),比如i为:1, 2, 3, 4, 5 // 时,结果为0, 1, 1, 2, 2, 这可以计算出ListView中减去分隔线后的实际单词对数量 final index = i ~/ 2; // 如果是建议列表中最后一个单词对 if (index >= _suggestions.length) { // …接着再生成10个单词对,然后添加到建议列表 _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); }); } Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), trailing: new Icon( alreadySaved ? Icons.favorite : Icons.favorite_border, color: alreadySaved ? Colors.red : null, ), onTap: () { setState(() { if (alreadySaved) { _saved.remove(pair); } else { _saved.add(pair); } }); }, ); } void _pushSaved() { Navigator.of(context).push( new MaterialPageRoute( builder: (context) { final tiles = _saved.map( (pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); }, ); final divided = ListTile.divideTiles( context: context, tiles: tiles, ).toList(); return new Scaffold( appBar: new AppBar( title: new Text(‘Saved Suggestions’), ), body: new ListView(children: divided), ); }, ), ); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text(‘Startup Name Generator’), actions: <Widget>[ new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved), ], ), body: _buildSuggestions(), ); }} ...

March 5, 2019 · 2 min · jiezi

Flutter 1.2 发布,添加应用内支付和 App Bundles

近日在巴塞罗那召开的 MWC 发布会上,Google 正式发布了 Flutter 跨平台 UI 框架的 1.2 版本。新版本最大的改变就是引入了对 Android App Bundles 的支持,可有效打包 Android APP 并创建即时应用的最新技术。此外该框架还帮助开发者接受应用内支付奠定了基础,并添加了很多基于Web的工具。Flutter 1.2 更新包括了大量常规稳定性和性能更新,包括最新的 Dart 2.2 SDK(默认情况下,Flutter 应用程序是用 Google 的 Dart 语言编写的),此外团队还表示正积极改善对 iOS 的支持,支持浮动光标文本编辑等等。虽然Flutter一直专注于移动,但该团队最近也开始讨论使用该框架构建桌面应用程序。为此,在1.2版本中引入了全新的键盘事件和鼠标悬停支持。Project Hummingbird(将Flutter推广网页版)的技术预览版也将会未来几个月上线。对于新的工具,值得注意的是,Google 已经在 Android Studio 中构建了 Flutter 支持,并为微软日益流行的 Visual Studio Code 添加了工具。现在,它还在构建新的基于 Web 的编程工具 Dart DevTools。它们在本地运行,包括小部件检查器,时间轴视图,源级调试器和日志记录视图。更新详情可以查阅发布日志来自:cnBeta

February 28, 2019 · 1 min · jiezi

flutter的key在widget list的作用以及必要性

在做一个flutter的项目过程中,体会到了key在widget渲染中发挥的作用以及开发者需要避免的坑,在次提出共勉与react的diff算法类似(vue的也是),flutter在渲染同级类似的item的时候也是采用key值判断来重新渲染的。因此如果你的业务中如果包含了一个同类型的widget list,记得要为每个widget加上一个key,否则flutter也是默认使用item在list的index作为key,这样就会遇到下面这个常见的坑了:假设原本有一个list,list = [widget0: {key: 0, …}, widget1: {key: 1, …}, widget2: {key: 2, …}]对应的视图为:现在我们删除中间的widget1,list更新成:list = [widget0: {key: 0, …}, widget2: {key: 1, …}]对应的视图为:可以看到,widget1没有被删除,而是widget2被删除了,这显示是错误的。原因便在于:虽然widget1在list中确实删除了,但现在widget2的key变成了1当fultter执行diff算法的时候,由于它是根据前后widget的key是否变化来判断的而widget2的key是1,fultter会认为原来key为1的那个widget(就是widget1)没有变化,所以不更新,而删除掉原来key为2的widget(就是widget2)。知道了原因后,为每个widget加上一个uuid,问题就解决了:注:如果widget是stateless的,不加key也能够正确删除。可能的原因大概是stageless的widget每次都需要重新绘制,因此不管key变不变化都是重绘的,而stateful则是根据state有没有变化来重绘,这样由于key没有变所以state也没有改变。但是作为开发者的我们,都应该养成添加key的习惯。如有错误,还望指出!

February 28, 2019 · 1 min · jiezi

UI2Code智能生成Flutter代码--整体设计篇

摘要: UI2CODE项目是闲鱼技术团队研发的一款通过机器视觉理解+AI人工智能将UI视觉图片转化为端侧代码的工具。背景:随着移动互联网时代的到来,人类的科学技术突飞猛进。然而软件工程师们依旧需要花费大量精力在重复的还原UI视觉稿的工作。UI视觉研发拥有明显的特征:组件,位置和布局,符合机器学习处理范畴。能否通过机器视觉和深度学习等手段自动生成UI界面代码,来解放重复劳动力,成为我们关注的方向。UI2CODE简单介绍:UI2CODE项目是闲鱼技术团队研发的一款通过机器视觉理解+AI人工智能将UI视觉图片转化为端侧代码的工具。2018年3月UI2CODE开始启动技术可行性预研工作,到目前为止,经历了3次整体方案的重构(或者重写)。我们参考了大量的利用机器学习生成代码的方案,但都无法达到商用指标,UI2CODE的主要思想是将UI研发特征分而治之,避免鸡蛋放在一个篮子里。我们着重关注以下3个问题的解法:视觉稿还原精度:我们的设计师甚至无法容忍1像素的位置偏差;准确率:机器学习还处于概率学范畴,但我们需要100%的准确率;易维护性:工程师们看的懂,改的动是起点,合理的布局结构才能保障界面流畅运行。UI2CODE运行效果:UI2CODE插件化运行效果,如下视频:进过几轮重构,最终我们定义UI2CODE主要解决feeds流的卡片自动生成,当然它也可以对页面级自动生成。视频地址:https://yunqivedio.alicdn.com…架构设计:简化分析下UI2CODE的流程:大体分为4个步骤:1.通过机器视觉技术,从视觉稿提取GUI元素2.通过深度学习技术,识别GUI元素类型3.通过递归神经网络技术,生成DSL4.通过语法树模板匹配,生成flutter代码版面分析版面分析只做一件事:切图。图片切割效果直接决定UI2CODE输出结果的准确率。我们拿白色背景的简单UI来举例:上图是一个白色背景的UI,我们将图片读入内存,进行二值化处理:def image_to_matrix(filename): im = Image.open(filename) width, height = im.size im = im.convert(“L”) matrix = np.asarray(im) return matrix, width, height得到一个二维矩阵:将白色背景的值转化为0.像切西瓜一样,我们只需要5刀,就可以将GUI元素分离,切隔方法多种多样:(下面是横切的代码片段,实际切割逻辑稍微复杂些,基本是递归过程)def cut_by_col(cut_num, _im_mask): zero_start = None zero_end = None end_range = len(_im_mask) for x in range(0, end_range): im = _im_mask[x] if len(np.where(im==0)[0]) == len(im): if zero_start == None: zero_start = x elif zero_start != None and zero_end == None: zero_end = x if zero_start != None and zero_end != None: start = zero_start if start > 0: cut_num.append(start) zero_start = None zero_end = None if x == end_range-1 and zero_start != None and zero_end == None and zero_start > 0: zero_end = x start = zero_start if start > 0: cut_num.append(start) zero_start = None zero_end = None客户端的UI基本都是纵向流式布局,我们可以先横切在纵切。将切割点的x,y轴坐标记录下来,它将是处理组件位置关系的核心。切割完成后,我们获取到2组数据:6个GUI元素图片和对应的坐标系记录。后续步骤通过分类神经网络进行组件识别。在实际生产过程中,版面分析会复杂些,主要是在处理复杂背景方面。关注我们的技术公众号,我们后续会详细分解。组件识别:进行组件识别前我们需要收集一些组件样本进行训练,使用Tensorflow提供的CNN模型和SSD模型等进行增量训练。UI2CODE对GUI进行了几十种类型分类:IMAGE, TEXT,SHAP/BUTTON,ICON,PRICE等等,分别归类为UI组件,CI组件和BI组件。UI组件,主要针对flutter原生的组件进行分类。CI组件,主要针对闲鱼自定义UIKIT进行分类。BI组件,主要针对具备一定业务意义的feed卡片进行分类。组件的识别需要反复的通过全局特征反馈来纠正,通常会采用SSD+CNN协同工作,比如下图的红色“全新“shape,这该图例中是richtext的部分,同样的shape样式可能属于button或者icon。属性提取:这块的技术点比较杂,归纳起来需要处理3部分内容:shape轮廓, 字体属性和组件的宽高。完成属性提取,基本上我们完成所有GUI信息的提取。生成的GUI DSL如下图:通过这些数据我们就可以进行布局分析了。其中文字属性的提取最为复杂,后续我们会专门介绍。布局分析:前期我们采用4层LSTM网络进行训练学习,由于样本量比较小,我们改为规则实现。规则实现也比较简单,我们在第一步切图时5刀切割的顺序就是row和col。缺点是布局比较死板,需要结合RNN进行前置反馈。视频地址:https://yunqivedio.alicdn.com…视频中展示的是通过4层LSTM预测布局结构的效果,UI的布局结构就像房屋的框架,建造完成后通过GUI的属性进行精装修就完成了一个UI图层的代码还原工作。代码生成及插件化:机器学习本质上来说还属于概率学范畴,自动生成的代码需要非常高的还原度和100%的准确率,概率注定UI2CODE很难达到100%的准确率,所以我们需要提供一个可编辑工具,由开发者通过工具能够快速理解UI的布局结构和修改布局结构。我们将UI2CODE生成的DSL TREE进行代码模板化匹配,代码模板的内容由资深的flutter技术专家来定义,它代表目前我们发现的最优代码实现方案。代码模板中会引入一些标签,由Intellij plugin来检索flutter工程中是否存在对应的自定义UIKIT,并进行替换,提高代码的复用度。整个插件化工程需要提供自定义UIKIT的检索,替换和校验工作,以及DSL Tree的创建,修改,图示等工作,总体来说,更像ERP系统,花费一些时间能够做的更加完美。小结:本篇我们简单介绍了UI2CODE的设计思路,我们将整个工程结构分为5个部分,其中4块内容核心处理机器视觉的问题,通过机器学习将它们链接起来。代码的线上发布是非常严格的事情,而单纯的机器学习方式,很难达到我们要求,所以我们选择以机器视觉理解为主,机器学习为辅的方式,构建整个UI2CODE工程体系。我们将持续关注AI技术,来打造一个完美的UI2CODE工具。本文作者:闲鱼技术-上叶阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 27, 2019 · 1 min · jiezi

flutter安装开发环境-问题记录

按照文档快速开始https://flutterchina.club/set…问题:brew install –HEAD libimobiledevice配置Flutter中的ios环境时,执行brew install –HEAD libimobiledevice时,报异常^CError: Calling needs :cxx11 is disabled! There is no replacement.Please report this to the homebrew/core tap: /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/cmake.rb:23 卸载brew,重新安装;1、卸载brew,执行命令:/usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent…)“2、安装brew,执行命令:/usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent…)" 之后重新执行,brew install –HEAD libimobiledevice,就没有错误了;原因是因为升级mac系统导致brew部分功能不能使用的解决方案出自 https://blog.csdn.net/LXFX110…

February 27, 2019 · 1 min · jiezi

开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?

开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?原创: 嘉宾-张楠 开源中国 以往我们说某一功能跨多端,往往是指在诸如 PC、移动等不同类型的设备之间都能实现;或者更加具体一点,指的是“跨平台”,可能是大到跨操作系统,比如 Windows、macOS、Linux、iOS 与 Android 等,可能是小到跨某个具体技术的不同实现库。但是今天我们要介绍的是关于跨 MVVM 架构模式各种环境的场景。Chameleon 是一套开源跨端解决方案,它的目标是让 MVVM 跨端环境大一统,实现任意使用 MVVM 架构设计的终端,都能使用其进行开发并运行。 在这样一个 MVVM 环境中,涉及到了 Weex、React-Native、WebView/浏览器与 Flutter 等各种跨端技术,还有它们实现的具体业务产品,比如微信小程序、快应用、支付宝小程序、百度智能小程序、今日头条小程序与其它各类小程序。也许你发现了,这里提到了许多种“小程序”,虽然最早微信小程序的概念甚至早期版本出现的时候,有过不少不看好的声音,但是随着它不断发展,目前已经成为了大众生活不可或缺的应用形态。马化腾透露过,截至 2018 年 11 月有 150 万微信小程序开发者,小程序应用数量超过 100 万,覆盖 200 多个细分行业,日活用户达到 2 亿。这样的成功经验与几乎触及到生活方方面面的巨大流量入口,大家都想入场,于是可以看到后来其它公司纷纷给出了类似的小程序方案。另一方面,除了小程序百花齐放,2018 年小米、华为、OPPO 等 10 家安卓手机厂商还结成了快应用联盟,并且先后发布了一系列快应用。Chameleon 目标就是要跨这些端,而随着各家不同实现越来越多,跨端场景也不断变得更加复杂。我们采访了 Chameleon 创始人张楠,请他为读者具体分享了 Chameleon 在这个过程中的成长。项目地址:https://github.com/didi/chame…本文是 Chameleon 首次对外公开实现原理!干货超多,包括:终端开发未来的开发模式Chameleon 跨端实现原理当前各种跨端方案原理对比(各种小程序、快应用等)与 Taro 的对比演进过程中遇到的困难与思考当初为什么去研发 Chameleon?关于这个问题可以从行业背景讲起。中国互联网络信息中心(CNNIC)发布的《中国互联网络发展状况统计报告》显示,截至 2018 年 6 月,我国网民规模达 8.02 亿人,微信月活 10 亿 、支付宝月活 4 亿、百度月活 3.3 亿;另一方面,2018 Q3 中国 Android 手机占智能手机整体的比例超过 80%,月活约 6 亿。BAT 与 Android 成为了中国互联网真正的用户入口。但凡流量高的入口级别 APP 都希望做平台,成为一个生态平台和互联网流量入口,大量第三方应用的接入,从业务层让公司 APP 关联上更多企业的利益,并且拥有更强的生命力;从技术层面可以利用“本地能力接口层”收集大量用户数据,从消费互联网到产业互联网需要大量各行各业基础用户数据线索进行驱动和决策。在这么一种背景下,再结合计算机技术的发展历史,我们知道每一种新技术的出现都会经历“各自为政”的阶段,小程序技术也不例外,所以我们看到了其它各种小程序平台出现。微信小程序作为首创者,虽然其它小程序都有在技术实现原理、接口设计上刻意模仿,但是作为一线开发者在不同平台发布小程序,往往还是需要重复开发、测试,从前 1 单位的工作量变成了 N 单位的工作量。而这还没算上快应用等其它入口。这种情况下,滴滴的研发工程师是其中最显著的“受害者”之一,滴滴出行在微信钱包、支付宝、Android 快应用都有相关入口,而且用户流量占比不低。研发同学在端内既追求 H5 的灵活性,也要追求性能趋近于原生。面对入口扩张,主端、独立端、微信小程序、支付宝小程序、百度小程序、安卓厂商联盟快应用,单一功能在各平台都要重复实现,开发和维护成本成倍增加。迫切需要一个只维护一套代码就可以构建多入口的解决方案,于是我们着手去打造了 Chameleon(CML,卡梅龙)这么一个项目,真正专注于让一套代码运行多端。Chameleon 核心是运用了 MVVM 架构,为什么它可以实现跨多端?MVVM 也就是 Model View ViewModel,它本质上是 MVC( Model View Controller)的进化版本,将 View 的状态和行为抽象化,使得视图 UI 和业务逻辑分开。它是一种让数据驱动反射视图的模式,发展到现在可能会偏离它的初衷了,更像是一个视图数据间的“通信协议”,让终端开发变得更加单纯,这是一种趋势,面向未来框架都采用这种模式。Facebook 在 2013 年开源 React,React 这个项目本身是一个 Web UI 引擎,随着不断发展,它衍生出 React Native 项目,用来编写原生移动应用。正是它给跨端方向带来了 MVVM 模式。Vue.js 于 2014 年左右发布,逆流而上占据了大量用户群体,2016 阿里巴巴也基于它发布了 Weex 项目,使得可以用 Vue 编写 Native App。Google 在 2018 年末正式发布了面向未来的跨 Android、iOS 端的 Flutter 1.0.0。原理我们知道终端开发离不开三大要素——界面表现(结构、外观)层、逻辑处理层与系统接口层(网络、存储与媒体等)。开发者编写代码时在初始化阶段(生命周期)调用“界面表现层”界面模型的接口绘制界面,当用户触摸界面时,“界面表现层”将事件发送给用户“逻辑处理层”,后者经过条件判断再处理并反馈到用户界面,处理过程可能需要调用“系统接口层”,反馈过程需要调用“界面表现层”的接口。常规的终端开发架构模式下,无论是 Web 端、Android 端还是 iOS 端的项目开发,都强依赖各端的环境接口,特别是依赖界面相关模型设计。iOS 系统下绘制界面基于 Objective-C 语言环境下的 UIKit 框架;Android 系统下用户绘制界面基于 Java 语言环境,由 LayoutInflater 处理 XML 结构层次树;Web 端使用 DOM 模型和 CSS 来描述绘制界面。 MVVM 中的关键是它通过 ViewModel 这一层将界面和逻辑层彻底隔离开来,负责关联界面表现和逻辑处理层的响应事件(update/notify)关系,这一“隔离层”上下通信足够规范、足够纯净单一。 Model 进行逻辑处理是纯业务响应逻辑,任何一种语言都可以实现,你可以用 Android 的 Java,也可以用 iOS 的 Objective-C,你心情好用“世界第一语言 PHP”也能实现。之所以普遍选择 JavaScript,很大程度是因为在这个领域内它的优点显著,如学习成本低、天生具备跨端属性、虚拟机(V8、JavaScriptCore)和各方向组件建设较好、生态活跃。而系统接口层则更简单了,只需穷举统一基础接口+可扩展接口能力即可。各种 MVVM 方案具体来看看各种 MVVM 方案都是怎么样的。React Native、Weex 与快应用的 MVVM开发者编写的代码在虚拟机(V8、JavaScriptCore)里面运行,虚拟机容器里面包含扩展的系统基础接口。运行时,将描述界面的数据(主要是 CSS+DSL 所描述内容)通过通信层传递给 Android、iOS 端的渲染引擎,用户触摸界面时,通过通信层传递给虚拟机里面的业务处理代码,业务处理代码可能调用网络、储存与媒体等接口,最后再次反馈到界面。Flutter 的 MVVMFlutter 和 RN 的最大区别在于将“JavascriptCore/V8+JS”替换成“C++ 实现的 engine+Dart 实现的 Framework+静态类型 Dart+编译成机器码”。Flutter 的方案如下图所示:Service 其实就是本地能力接口层,Widget 树是视图层模型。Flutter 和 RN 的使用面设计上类似,Flutter 文档中提到“In Flutter, almost everything is a widget.”,widget 的调用从 RN 的 JSX 变成 Flutter 的 widget 调用,UI 的外观描述从 RN 的 CSS(文本样式、布局模型、盒模型)到定制化 Flutter Widget(textStyle 、Layout Widget、Widget)。本质上 Flutter 也是 MVVM 架构,逻辑层通过 setState 通知视图层更新,一定程度上这也是为什么 Flutter 敢说能转成 Web 框架的原因,核心还是基于这类数据驱动视图架构模式,业务代码不会深度依赖任何一端特有的“视图模型”。各类小程序的 MVVM小程序本质上和 Weex、React Native 的设计思路基本一样,最大区别在于前者还是用浏览器 WebView 做渲染引擎,而后者是单独实现了渲染引擎(所以大量的 CSS 布局模型不支持)。具体到 Chameleon 上是怎么实现的?首先任何一份应用层的高级语言代码块分成几层:语言层(Language)、框架层(Framewrok)与库层(Library):Language —— 通俗来说,实现程序所需的基本逻辑命令:逻辑判断(if)、循环(for)与函数调用(foo())等。Framewrok —— 通俗来说,完成一个 App 应用交互任务所需规范,例如生命周期(onLoad、onShow)、模块化与数据管理等。Library —— 可以理解就是“方法封装集合”。比如 Web 前端中 Vue 更适合叫框架,而 jQuery 更适合叫库;Android 系统下 activity manager + window Manager View System 等的集合叫框架,而 SQLite 、libc 更适合叫库。对应到 Chameleon 就是这样:具体到实现原理全景架构图如下:你可以理解 Chameleon 为了实现“让 MVVM 跨端环境大统一”的目标做了以下工作:定义了标准的 Language(CML DSL)、Framework 与 Library(内置组件和 API)协议层。在线下编译时将 DSL 转译成各端 DSL,只编译 Language 层面足够基础且稳定的代码。在各个端运行时分别实现了 Framework 统一,在各个端尽量使用原有框架,方便利用其生态,这样很多组件可以直接用起来。在各个端运行时分别实现了 Library(内置组件和 API)。为用户提供多态协议,方便扩展以上几方面的内容,触达底层端特殊属性,同时提升可维护性。实现思路很简单,所有设计为了 MVVM 标准化,不做多余设计,所以宏观的角度就像 Node.js(libuv)同时运行在 Windows 和 macOS 系统,都提供了一个跨平台抽象层。从 MVVM 角度来看的话:View(展现层)第三方 Render Engine:各类框架已有框架,浏览器的 Vue、Webview 里的小程序引擎、Android、iOS 里面的 React Native/Weex 引擎、甚至 Flutter 里面的 Dart Framework。Chameleon 内置组件库:多态协议定义统一组件 view、input、text、block 与 cell 等,它是界面组层的原始基类,衍生出多复杂界面功能。ViewModel(关联层)Chameleon 语法转译组件调用循环条件判断事件回调关联父子关系……Model(逻辑响应层)JavaScript 代码CML Runtime 框架Chameleon API:多态协议定义统一接口,cml.request、cml.store 等Chameleon 的跨多端方案给开发者的开发带来了极大的便利,具体表现是怎么样的?一句话:基于 Chameleon 开发,效率会越来越高。各个端的涌现,让原本是 1 的工作量因为多端存在而变成 N 倍,使用 Chameleon,工作量会变回 1.2。这多出来的 0.2 工作量是要处理各端的差异化功能,比如以下场景:某业务线迁入 Chameleon 时,发现没有“passport登录组件”,在各类小程序里面能免密登录了,在 Web、Native 端是弹出登录框登录,不同业务用户交互形态不一样所以 Chameleon 没有提供组件;开发者需要基于多态协议扩展单独一个登录组件<passport/>,无论如何最后返回一个登录后的回调 token 即可,外部无需组件关心里面如何操作。用户需要分享功能,发现没有“share组件”,在微信 Web 端可以引导右上角分享,在小程序直接分享,不同业务用户交互形态不一样,用户需要基于多态协议扩展单独一个登录组件<share/>。这种各端差异较大的例子,随着业务的积累,可以变成了一个个业务组件单独维护,后面也不需要重复开发了,且反推产品体验一致化,组件三层结构“CML框架内置组件->CML扩展组件->业务开发者自己扩展的多态组件”达成 100% 统一。随着组件积累业务开发工作量越来少,工程师可以专注做更加有意义的事情,这就是 Chameleon 存在的目的。基于统一的跨端抽象,用户在 Chameleon 项目持续维护过程中,Chameleon 发布新增一个端之后,你的业务代码基本不用改动即可无缝发布成新端。比如这个 cml-yanxuan 项目开发时支持 3 个端,后面新增了百度、支付宝小程序端,原有代码直接能跑起来运行 5 个端,一端所见即多端所见。开发时只能跑 3 个端原有代码无缝支持 5 个端另外特别强调的是,对于大公司团队,如果有很强的技术能力,希望开发的代码掌控在自己手里,对输出结果有更好控制能力。其实 Chameleon 内置组件和内置 API 是可以替换的,那么所有组件都是业务方自己开发了,哪天不想用了直接导出原生组件即可离开 Chameleon,如下图:目前跨多端统一的方案中,Taro 是比较亮眼的,能否具体对比一下 Chameleon 与 Taro。我们觉得 Chameleon 与其它解决方案的最大区别在于其它框架都是小程序增强,即用 Vue 或者 React 写小程序,这些框架官方给的已接入例子也都是跑微信小程序。它们更加类似 Chameleon 的前身 MPV(Mini Program View),即考虑如何增强小程序开发。2017 年微信小程序发布时,滴滴作为白名单用户首先开始尝试接入,开始面对重复开发的难题。这时候我们专门成立了一个小项目组,完成一个名为 MPV 的项目,一期目标是“不影响用户发挥,不依赖框架方的原则性实现一套代码运行 Web 和微信小程序”。看着很美好,用这样的方案实现 Web 端和小程序端,也确实完成了超过 90% 代码重用,总体上开发效率和测试效率都有了一定提升,但是却不是真正意义上的跨多端统一。单独说到 Chameleon 与 Taro 的区别,总体上看,可以归为这样一个表:表中每一项都是在做跨端方案时需要考虑到的。我们说除了 Chameleon,其它方案都只是在对小程序进行增强,或者说是模仿微信小程序的 API 和组件的接口设计。Taro 是通过将 JSX 转成小程序模板,在其它端模拟微信小程序的接口和组件,让其它端更像微信小程序,业务开发时不一致的地方需要环境变量判断差异分别调用,会造成端差异逻辑和产品逻辑混合在一起。此外,它要跟随小程序更新,业务方会有双重依赖;其它端的和小程序不能保持一致,用户要各种差异化兼容,不利于维护。那 Chameleon 呢?Chameleon 把这些问题都考虑到了,所以在早期伪跨端 MiniProgram View 成型之后不断演进的过程中,把它发展成为一个真正的跨多端方案。前边的表格显示了,Chameleon 既考虑统一性,又考虑差异性,且差异性不会影响可维护性;当各端差异确实太大,那就不要用一套代码实现多个端同一页面,而是统一公用组件。这还只是拿 Chameleon 与 Taro 的重合点进行了对比,但是别忘了 Chameleon 不仅仅是前端框架,它:还有统一的 Chameleon Native SDK,Chameleon 不仅仅希望统一各类小程序,还要覆盖自家 APP,会持续通过 Native SDK 扩展 API 和组件,期望有与小程序一样的本地能力。理想情况下,一套代码就能在各类小程序、自家 APP 里面无缝平滑运行。还有待开源的后台管理系统。还有待开源的 XEdtior 非研发用编辑器,可以直接编辑跨端页面、直接发布。另外,未来还将带来以下能力:后端统一接口(消息推送、分享与支付等)基于统一的 MVVM 标准,更有基于 Flutter 的原生 APP当前的各类小程序和 Native 跨端框架,类似当年多个浏览器时,Safari、Chrome、Firefox、IE 6/7/8/9、Android 浏览器等盛行的时代。以这个来类比,那么 Chameleon 的接口组件设计上更像一个 jQuery。网络请求有的是 XHRHttprequest 有的是 ActiveXObject,jQuery 考虑的是用户需要什么,需要一个网路请求接口,支持 get、post 等,所以 jQuery 写一个既非 ActiveXObject 又非 XHRHttprequest 的名为 $.ajax 接口,提供一个封装网络接口,你不用关心内部在不同端怎么调用的,jQuery 内部会帮你兼容。Chameleon 也是一样的思路,所有的接口设计都是真正能兼容跨所有的端,没有差异性,而且只保留当前所在端的接口调用代码:IE 里面只保留 ActiveXObject,Chrome 只保留 XHRHttprequest。Chameleon 的接口设计上比 jQuery 更强的地方在于,使用标准的多态协议,保障可维护性,性能上只保留当前端代码,且将多态协议暴露出来,让用户也能扩展自己想要的 API(类比 $.xxx)。当然时代已经变了,监听视图不在是 $(’#xxx’).click(fn),而是 MVVM 数据驱动视图方式了,所以提供了 Chameleon 双向绑定这样的 VM 层。前边讲到了 Chameleon 的前身 MPV,那具体分享一下 Chameleon 的整个演进过程吧。出生期:选择转译还是模拟小程序环境?前面讲到,2017 年的时候,我们完成一个名为 MPV 的项目,一期目标是不影响用户发挥,不依赖框架方的原则性实现一套代码运行 Web 和微信小程序。当时缺乏小程序资料是遇到的最大问题(就更别提今天讲到的业内这么多解决方案了),当时唯一一个可以参考的开源项目是 WEPT,WEPT 是一个微信小程序实时开发环境,它的目标是为小程序开发提供高效、稳定、友好、无限制的运行环境。它的设计思路是在 Web 端模仿小程序环境执行。于是我们在开发 MPV 时考虑了两种实现策略:1、在 Web 端像 WEPT 一样 mock 小程序环境;就像微信开发者工具里面也模拟了小程序执行环境,WAServie、WAWebview 提供的两套环境源码做底层,在页面中开启三个独立运行环境运行并用 iframe 通讯模拟微信小程序的 3 个 Webview 之间的联通关系。2、逐个转译代码支持小程序,缺点是可能会有 edge case 需要处理以及潜在的 bug 会比较多。最终在看完 WEPT 源码和微信开发者工具的情况下,我们明确放弃了第 1 条实现策略,选择了逐个转译代码支持小程序的路线,主要原因是于 Web 端兼容微信所有的功能,尺寸过于庞大。经过三个月紧锣密鼓的开发终于实现了第一版本 MPV: 经过实现几个 demo 之后,开始执行迁移计划: MPV 在 Webapp 上实践最终实现效果如下:最终实现效果挺美好,也确实完成了超过 90% 的代码重用,总体上开发效率和测试效率都有了明显提升。但是在后续实践过程中,发现存在大量的问题,并且项目越大问题越凸显出来,总结如下:可维护性问题,没有隔离公用代码和各端差异代码。项目中不止有业务逻辑,还混杂着 Web 端和小程序端产品功能差异化逻辑。比如前边举过的例子,分享功能 Web 端无法实现(引导分享),小程序可以实现,意味着各种环境判断各种差异化逻辑,牵一发动全身,还要来回测试。方向选择错误,MPV 使用了小程序语法标准(小程序的生命周期、API 接口等),导致用户使用上无法清晰理解。不能直接使用各端已有生态组件,即缺乏标准规范接入某个端已有开源组件。比如 Web 端 pick.js 组件缺乏快速接入规范,用户要么重新开发,或者在模板和 js 代码中使用环境判断的方式针对引入。最终导致同一功能不同端的调用方式、输入与输出不一致。业务项目依赖 MPV 框架。框架依赖微信小程序接口(模板、生命周期与接口),扩展了统一接口。例如微信小程序更新了 wx.request 时,业务项目方无法立刻使用,需要等框架更新。文件夹结构混乱,混杂着多个端代码文件,且识别成本高。不支持 vuex、redux 等高效数据管理方式尺寸单位不统一,px 和 rpx 不一致周边小型差异点太多:协议不一致,例如 Web 端可以用 //:www.didiglobal.com/passenger/create ,小程序只能用 https://:www.didiglobal.com/passenger/create打开一个新页面时链接不统一,例如打开发单页时,Web 端是 //:www.didiglobal.com/passenger/create,小程序是 /page/create页面之间跳转时,传参不统一debug 成本高,修改完代码之后两端需要测试两端界面效果不一致,基础内置组件统一性建设不足工程化建设落后,例如不支持 liveroload、数据 mock、资源定位、proxy、多端统一预览接口设计不完整,生命周期、组件分层、本地 API 设计等模板 DSL 语法不规范成长期:从伪统一到大一统在 MPV 的实践积累下,有了一定的底气和把握,后续的规划更加明确。2018 年 4 月我们把跨端项目规模进一步扩大,想要做一个真正跨 N 端的解决方案,目标是提供标准的 MVVM 架构开发模式统一各类终端。这就是 Chameleon 的出现契机。Chameleon 真正想要一套代码运行多端,总结下来要解决几大问题:要全面完成端开发的所有细节的统一性,特别是界面统一性有大量细节要做要在完成上一条的前提下考虑差异化定制空间持续可维护目标理想业务形态是这样的:图中上半部分是传统开发方式,下半部分 Chameleon 的模式抽象出了 UI 渲染层和本地接口能力层,业务代码一部分简单页面由 XEditor(h5Editor 的前身)编辑工具产出,另一部分工程师使用 Chameleon 开发,不止解决跨端问题,还弥补改进了工程开发过程中的效率、质量、性能与稳定性问题,让工程师专注有意义的业务,成长更快。首个 Native 渲染引擎选择——小程序架构、RN/Weex 架构从 MPV 到 Chameleon,外界看来最明显的变化是从跨 2 端(Web、小程序)升级到跨多端(Web、小程序、Android、iOS),最开始纠结于首个端上版本的渲染引擎使用小程序架构还是 RN/Weex 架构。RN/Weex 网上有大量资料可查,但是小程序方面则不然。千辛万苦搜索之后,根据一位知道内情的朋友的描述分享,才有了一定的了解。 这里分享几个印象深刻的要点:小程序展现层使用 Webview,里面内置了一套 JS 框架用来和 Native 通信,真正业务代码执行在单独 JS 虚拟机容器实例中JS 虚拟机容器使用情况,iOS 系统是 JavaScriptCore,Android 系统使用 QQ 浏览器的 X5 内核小程序的各个 TAG 组件使用的数据驱动用的是 Web Components显而易见,部分性能要求较高的使用原生控件(视频、键盘等等)插入到 Webview 里面。原生控件的具体位置 Native 怎么获取?答案是由嵌入到 Webview 的一套小程序框架通知给原生层原生控件怎么保证在内部可滚动的元素(Scroll-view)里面正常滚动?答案是 CSS 设置 -webkit-over-scroll:touch 时,iOS 的实现是原生的 UIScrollView,Native 可以通过一些黑科技找到视图层级中的 UIScrollView,然后对原生控件进行插入和处理;而 Android 直接绘制没办法做到这点。现在(截至 4 月)仅仅是直接覆盖到 Webview 最外层的 scrollview 上,由内置到 Webview 的一套 JS 框架控制原生控件位置最终多方面分析如下:虽然小程序方案看起来很简单,但其实很多细节点需要大量打磨,从确认方案到真正可以跑起来可以线上发布,仅仅花费在终端上的研发人力为 20P*6 个月,微信小程序团队的目标和我们跨端目标不一样,他们投入这么多成本是值得的,我们为了跨端没必要投入这么高成本。所以我们选择放弃小程序渲染方案,而使用已开源的 RN/Weex 方案。第一个版本最终使用 Weex,包括团队同学去看了 Weex 源码实现。在整体设计上仅仅使用 Weex 渲染功能,外层包装接口,保障后续能有更高扩展性。Chameleon Native SDK针对 Native SDK 我们主要从原生能力扩展、性能与稳定等三个方面做了工作。 原生能力扩展:无论是 Webview 还是 React Native、Weex 甚至 Flutter 都只提供渲染能力(以及一些最基础本地接口),更多完成业务功能所需本地环境的能力(例如分享到微信)需要 Android 和 iOS 的 Native 往容器去扩展。本地能力包含 2 种,涉及 UI 界面的统一叫组件(UI 组件如登录、支付),涉及到纯能力调用的统一叫 API(网络、存储等)性能:界面展现和交互耗时关键取决于 2 块,资源加载耗时(非打包到安装包部分代码)、执行耗时稳定:主要关注灰度发布(风险可控)和线上止损,主要工作是按用户灰度发布、可以快速降级到 H5以下是性能方向中的首屏加载时间的优化数据,原有 H5 使用 SSR(Server Side Render)已经算是最快的 Web 首屏技术方案了(不考虑优化后端多模块耗时的 BIGPIPE),它保持在 1.5 秒以下,在优化后降到 0.5 秒左右。 性能优化中我们有一个关于执行速度的 TODO 计划。同样是跨端,Flutter 之所以比 Weex 和 RN 执行速度快,主要原因是前者是编译型,客户端机器运行前已经是 CPU 可识别的机器码;后者是解释型,到客户端运行前是字符串,边编译边执行,虽然做了 JIT 尽量优化,差距还是较大。其实在这中间还有一个抹平了不同 CPU 架构下机器码差异的中间码;当然前提是开发语言改成静态类型,这里不作展开。原本分 5 次开发的 Web 端、支付宝小程序、快应用、微信小程序、Native 端变成了 1.2 次左右开发了。最重要的是随着业务级别各端差异化的多态组件和跨端组件积累,后续 1.2 工作量也会变成 0.8,0.4 的优化主要来自两个方面:0.2 是普通跨端组件的积累,复用度变高0.2 是各类业务级别的差异化多态组件,例如登录功能,在 Web端、Native 端和小程序端实现和交互是不一致的,这时候业务形态不一样,设计的 <passport> 组件也不一样,只能各业务线去封装。介绍一下接下来的 roadmap。我们的最终目标是提供标准的 MVVM 架构开发模式统一各类终端。接下来的具体 roadmap 如下表所示:欢迎有共同愿景的同学加入我们一起共建,往仓库贡献自己的代码。项目地址:https://github.com/didi/chame…QQ 群:公众号:采访嘉宾介绍张楠,Chameleon 创始人,技术团队负责人,前百度资深工程师,终身学习者。 ...

February 26, 2019 · 4 min · jiezi

开发了个 Flipper 调试工具的 Flutter 版本 SDK,让 Flutter 应用调试起来更容易

最近一直在持续的学习 Flutter,但一直没有发现有好用的网络调试工具,也不想太想使用 Charles 这个工具,后来发现了Facebook Flipper 这个工具,所以花了几天时间做了个 Flutter 版的 Flipper SDK。期间碰到了一些问题但 Flipper 项目的人迅速的帮忙。这个库可以让你能够在 Flipper 上查看你的 Flutter 应用的网络请求及 Preferences 数据,相比之前我之前使用 print 来输出请求数据来说,实在是方便了好多,如果你也在用 Flutter 开发你的应用,不妨来试一下吧。特性Network inspectorShared preferences (and UserDefaults) inspector集成到你的项目必备条件开始之前确保你已安装:已安装 Flipper Desktop安装添加以下内容到包的 pubspec.yaml 文件中:dependencies: flutter_flipperkit: ^0.0.2根据示例更改项目的 ios/Podfile 文件:Flipper 目前需要的 platform 为 8.0+source ‘https://github.com/facebook/flipper.git'+source ‘https://github.com/CocoaPods/Specs'# Uncomment this line to define a global platform for your project-# platform :ios, ‘9.0’+platform :ios, ‘9.0’根据示例更改项目的 android/app/build.gradle 文件:Flipper 目前需要的 sdkVersion 为 28android {- compileSdkVersion 27+ compileSdkVersion 28 defaultConfig {- targetSdkVersion 27+ targetSdkVersion 28 }}您可以通过命令行安装软件包:$ flutter packages get快速集成添加下列代码到 lib/main.dart 文件:import ‘package:flutter_flipperkit/flutter_flipperkit.dart’;void main() { FlipperClient flipperClient = FlipperClient.getDefault(); // 添加网络插件 flipperClient.addPlugin(new FlipperNetworkPlugin()); // 添加 Preferences 插件 flipperClient.addPlugin(new FlipperSharedPreferencesPlugin()); flipperClient.start(); runApp(MyApp());}Dio 集成示例:import ‘dart:io’;import ‘package:dio/dio.dart’;import ‘package:flutter_flipperkit/flutter_flipperkit.dart’;import ‘package:uuid/uuid.dart’;class DioClient { Dio _http; FlipperNetworkPlugin _flipperNetworkPlugin; DioClient() { _flipperNetworkPlugin = FlipperClient .getDefault().getPlugin(FlipperNetworkPlugin.ID); Options options = new Options( connectTimeout: 5000, receiveTimeout: 3000, headers: { “Accept”: “application/json”, “Content-Type”: “application/json” }, responseType: ResponseType.JSON, ); this._http = new Dio(options); // 在拦截器中添加和 Flipper 通讯的代码 this._http.interceptor.request.onSend = (Options options) async { // 发送请求数据到 Flipper this._reportRequest(options); return options; }; this._http.interceptor.response.onSuccess = (Response response) { // 发送响应数据到 Flipper this._reportResponse(response); return response; }; } Dio get http { return _http; } void _reportRequest(Options options) { String requestId = new Uuid().v4(); options.extra.putIfAbsent(“requestId”, () => requestId); RequestInfo requestInfo = new RequestInfo( requestId: requestId, timeStamp: new DateTime.now().millisecondsSinceEpoch, uri: ‘${options.baseUrl}${options.path}’, headers: options.headers, method: options.method, body: options.data, ); _flipperNetworkPlugin.reportRequest(requestInfo); } void _reportResponse(Response response) { Map<String, dynamic> headers = new Map(); for (var key in [] ..addAll(HttpHeaders.entityHeaders) ..addAll(HttpHeaders.requestHeaders) ..addAll(HttpHeaders.responseHeaders) ) { var value = response.headers.value(key); if (value != null && value.isNotEmpty) { headers.putIfAbsent(key, () => value); } } String requestId = response.request.extra[‘requestId’]; ResponseInfo responseInfo = new ResponseInfo( requestId: requestId, timeStamp: new DateTime.now().millisecondsSinceEpoch, statusCode: response.statusCode, headers: headers, body: response.data, ); _flipperNetworkPlugin.reportResponse(responseInfo); }}Dio 使用示例new DioClient().http.get(‘https://www.v2ex.com/api/topics/hot.json');运行程序这时,集成已经完成,启用应用后可在 Flipper Desktop 上实时看到你的网络请求了$ flutter run已知问题Flipper Desktop 中文乱码,但已解决并提交 PR 给官方,暂时可以使用我修改的版本 https://github.com/lijy91/fli…暂不支持 iOS 真机探讨如果您对此项目有任何建议或疑问,可以通过 Telegram 或我的微信进行讨论。相关链接https://github.com/blankapp/f…https://github.com/facebook/f… ...

February 25, 2019 · 2 min · jiezi

构建你的第一个Flutter视频通话应用

Flutter 1.0 发布也已经有一段时间了,春节后声网发布了Flutter平台上的Agora Flutter SDK(一个基于 Flutter 开发的 Plugin),今天我们就来看一下如何使用Agora Flutter SDK快速构建一个简单的移动跨平台视频通话应用。环境准备在Flutter中文网上,关于搭建开放环境的教程已经相对比较完善了,有关IDE与环境配置的过程本文不再赘述,若Flutter安装有问题,可以执行flutter doctor做配置检查。本文使用MacOS下的VS Code作为主开发环境。目标我们希望可以使用Flutter+Agora Flutter SDK实现一个简单的视频通话应用,这个视频通话应用需要包含以下功能,加入通话房间视频通话前后摄像头切换本地静音/取消静音声网的视频通话是按通话房间区分的,同一个通话房间内的用户都可以互通。为了方便区分,这个演示会需要一个简单的表单页面让用户提交选择加入哪一个房间。同时一个房间内可以容纳最多4个用户,当用户数不同时我们需要展示不同的布局。想清楚了?动手撸代码了。项目创建首先在VS Code选择查看->命令面板(或直接使用cmd + shift + P)调出命令面板,输入flutter后选择Flutter: New Project创建一个新的Flutter项目,项目的名字为agora_flutter_quickstart,随后等待项目创建完成即可。现在执行启动->启动调试(或F5)即可看到一个最简单的计数App看起来我们有了一个很好的开始:) 接下去我们需要对我们新建的项目做一下简单的配置以使其可以引用和使用agora flutter sdk。打开项目根目录下的pubspec.yaml文件,在dependencies下添加agora_rtc_engine: ^0.9.0,dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 # add agora rtc sdk agora_rtc_engine: ^0.9.0dev_dependencies: flutter_test: sdk: flutter保存后VS Code会自动执行flutter packages get更新依赖。应用首页在项目配置完成后,我们就可以开始开发了。首先我们需要创建一个页面文件替换掉默认示例代码中的MyHomePage类。我们可以在lib/src下创建一个pages目录,并创建一个index.dart文件。如果你已经完成了官方教程Write your first Flutter app,那么以下代码对你来说就应该不难理解。class IndexPage extends StatefulWidget { @override State<StatefulWidget> createState() { return new IndexState(); }}class IndexState extends State<IndexPage> { @override Widget build(BuildContext context) { // UI } onJoin() { //TODO }}现在我们需要开始在build方法中构造首页的UI。按上图分解UI后,我们可以将我们的首页代码修改如下,@overrideWidget build(BuildContext context) {return Scaffold( appBar: AppBar( title: Text(‘Agora Flutter QuickStart’), ), body: Center( child: Container( padding: EdgeInsets.symmetric(horizontal: 20), height: 400, child: Column( children: <Widget>[ Row(children: <Widget>[]), Row(children: <Widget>[ Expanded( child: TextField( decoration: InputDecoration( border: UnderlineInputBorder( borderSide: BorderSide(width: 1)), hintText: ‘Channel name’), )) ]), Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( children: <Widget>[ Expanded( child: RaisedButton( onPressed: () => onJoin(), child: Text(“Join”), color: Colors.blueAccent, textColor: Colors.white, ), ) ], )) ], )), ));}执行F5启动查看,应该可以看到下图,看起来不错!但也只是看起来不错。我们的UI现在只能看,还不能交互。我们希望可以基于现在的UI实现以下功能,为Join按钮添加回调导航到通话页面对频道名做检查,若尝试加入频道时频道名为空,则在TextField上提示错误TextField输入校验TextField自身提供了一个decoration属性,我们可以提供一个InputDecoration的对象来标识TextField的装饰样式。InputDecoration里的errorText属性非常适合在我们这里被拿来使用,同时我们利用TextEditingController对象来记录TextField的值,以判断当前是否应该显示错误。因此经过简单的修改后,我们的TextField代码就变成了这样, final _channelController = TextEditingController(); /// if channel textfield is validated to have error bool _validateError = false; @override void dispose() { // dispose input controller _channelController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { … TextField( controller: _channelController, decoration: InputDecoration( errorText: _validateError ? “Channel name is mandatory” : null, border: UnderlineInputBorder( borderSide: BorderSide(width: 1)), hintText: ‘Channel name’), )) … } onJoin() { // update input validation setState(() { _channelController.text.isEmpty ? _validateError = true : _validateError = false; }); }在点击加入频道按钮的时候回触发onJoin回调,回调中会先通过setState更新TextField的状态以做组件重绘。注意: 不要忘了overridedispose方法在这个组件的生命周期结束时释放_controller。前往通话页面到这里我们的首页基本就算完成了,最后我们在onJoin中创建MaterialPageRoute将用户导航到通话页面,在这里我们将获取的频道名作为通话页面构造函数的参数传递到下一个页面CallPage。import ‘./call.dart’;class IndexState extends State<IndexPage> { … onJoin() { // update input validation setState(() { _channelController.text.isEmpty ? _validateError = true : _validateError = false; }); if (_channelController.text.isNotEmpty) { // push video page with given channel name Navigator.push( context, MaterialPageRoute( builder: (context) => new CallPage( channelName: _channelController.text, ))); }}通话页面同样在/lib/src/pages目录下,我们需要新建一个call.dart文件,在这个文件里我们会实现我们最重要的实时视频通话逻辑。首先还是需要创建我们的CallPage类。如果你还记得我们在IndexPage的实现,CallPage会需要在构造函数中带入一个参数作为频道名。class CallPage extends StatefulWidget { /// non-modifiable channel name of the page final String channelName; /// Creates a call page with given channel name. const CallPage({Key key, this.channelName}) : super(key: key); @override _CallPageState createState() { return new _CallPageState(); } } class _CallPageState extends State<CallPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.channelName), ), backgroundColor: Colors.black, body: Center( child: Stack( children: <Widget>[], ))); }}这里需要注意的是,我们并不需要把参数在创建state实例的时候传入,state可以直接访问widget.channelName获取到组件的属性。引入声网SDK因为我们在最开始已经在pubspec.yaml中添加了agora_rtc_engine的依赖,因此我们现在可以直接通过以下方式引入声网sdk。import ‘package:agora_rtc_engine/agora_rtc_engine.dart’;引入后即可以使用创建声网媒体引擎实例。在使用声网SDK进行视频通话之前,我们需要进行以下初始化工作。初始化工作应该在整个页面生命周期中只做一次,因此这里我们需要overrideinitState方法,在这个方法里做好初始化。class _CallPageState extends State<CallPage> { @override void initState() { super.initState(); initialize(); } void initialize() { _initAgoraRtcEngine(); _addAgoraEventHandlers(); } /// Create agora sdk instance and initialze void _initAgoraRtcEngine() { AgoraRtcEngine.create(APP_ID); AgoraRtcEngine.enableVideo(); } /// Add agora event handlers void _addAgoraEventHandlers() { AgoraRtcEngine.onError = (int code) { // sdk error }; AgoraRtcEngine.onJoinChannelSuccess = (String channel, int uid, int elapsed) { // join channel success }; AgoraRtcEngine.onUserJoined = (int uid, int elapsed) { // there’s a new user joining this channel }; AgoraRtcEngine.onUserOffline = (int uid, int reason) { // there’s an existing user leaving this channel }; }}注意: 有关如何获取声网APP_ID,请参阅声网官方文档。在以上的代码中我们主要创建了声网的媒体SDK实例并监听了关键事件,接下去我们会开始做视频流的处理。在一般的视频通话中,对于本地设备来说一共会有两种视频流,本地流与远端流 - 前者需要通过本地摄像头采集渲染并发送出去,后者需要接收远端流的数据后渲染。现在我们需要动态地将最多4人的视频流渲染到通话页面。我们会以大致这样的结构渲染通话页面。这里和首页不同的是,放置通话操作按钮的工具栏是覆盖在视频上的,因此这里我们会使用Stack组件来放置层叠组件。为了更好地区分UI构建,我们将视频构建与工具栏构建分为两个方法。本地流创建与渲染要渲染本地流,需要在初始化SDK完成后创建一个供视频流渲染的容器,然后通过SDK将本地流渲染到对应的容器上。声网SDK提供了createNativeView的方法以创建容器,在获取到容器并且成功渲染到容器视图上后,我们就可以利用SDK加入频道与其他客户端互通了。 void initialize() { _initAgoraRtcEngine(); _addAgoraEventHandlers(); // use _addRenderView everytime a native video view is needed _addRenderView(0, (viewId) { // local view setup & preview AgoraRtcEngine.setupLocalVideo(viewId, 1); AgoraRtcEngine.startPreview(); // state can access widget directly AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0); }); } /// Create a native view and add a new video session object /// The native viewId can be used to set up local/remote view void _addRenderView(int uid, Function(int viewId) finished) { Widget view = AgoraRtcEngine.createNativeView(uid, (viewId) { setState(() { _getVideoSession(uid).viewId = viewId; if (finished != null) { finished(viewId); } }); }); VideoSession session = VideoSession(uid, view); _sessions.add(session); }注意: 代码最后利用uid与容器信息创建了一个VideoSession对象并添加到_sessions中,这主要是为了视频布局需要,这块稍后会详细触及。远端流监听与渲染远端流的监听其实我们已经在前面的初始化代码中提及了,我们可以监听SDK提供的onUserJoined与onUserOffline回调来判断是否有其他用户进出当前频道,若有新用户加入频道,就为他创建一个渲染容器并做对应的渲染;若有用户离开频道,则去掉他的渲染容器。 AgoraRtcEngine.onUserJoined = (int uid, int elapsed) { setState(() { _addRenderView(uid, (viewId) { AgoraRtcEngine.setupRemoteVideo(viewId, 1, uid); }); }); }; AgoraRtcEngine.onUserOffline = (int uid, int reason) { setState(() { _removeRenderView(uid); }); }; /// Remove a native view and remove an existing video session object void _removeRenderView(int uid) { VideoSession session = _getVideoSession(uid); if (session != null) { _sessions.remove(session); } AgoraRtcEngine.removeNativeView(session.viewId); }注意: _sessions的作用是在本地保存一份当前频道内的视频流列表信息。因此在用户加入的时候,需要创建对应的VideoSession对象并添加到sessions,在用户离开的时候,则需要删除对应的VideoSession实例。视频流布局在有了_sessions数组,且每一个本地/远端流都有了一个对应的原生渲染容器后,我们就可以开始对视频流进行布局了。 /// Helper function to get list of native views List<Widget> _getRenderViews() { return _sessions.map((session) => session.view).toList(); } /// Video view wrapper Widget _videoView(view) { return Expanded(child: Container(child: view)); } /// Video view row wrapper Widget _expandedVideoRow(List<Widget> views) { List<Widget> wrappedViews = views.map((Widget view) => _videoView(view)).toList(); return Expanded( child: Row( children: wrappedViews, )); } /// Video layout wrapper Widget _viewRows() { List<Widget> views = _getRenderViews(); switch (views.length) { case 1: return Container( child: Column( children: <Widget>[_videoView(views[0])], )); case 2: return Container( child: Column( children: <Widget>[ _expandedVideoRow([views[0]]), _expandedVideoRow([views[1]]) ], )); case 3: return Container( child: Column( children: <Widget>[ _expandedVideoRow(views.sublist(0, 2)), _expandedVideoRow(views.sublist(2, 3)) ], )); case 4: return Container( child: Column( children: <Widget>[ _expandedVideoRow(views.sublist(0, 2)), _expandedVideoRow(views.sublist(2, 4)) ], )); default: } return Container(); }工具栏(挂断、静音、切换摄像头)在实现完视频流布局后,我们接下来实现视频通话的操作工具栏。工具栏里有三个按钮,分别对应静音、挂断、切换摄像头的顺序。用简单的flex Row布局即可。 /// Toolbar layout Widget _toolbar() { return Container( alignment: Alignment.bottomCenter, padding: EdgeInsets.symmetric(vertical: 48), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ RawMaterialButton( onPressed: () => _onToggleMute(), child: new Icon( muted ? Icons.mic : Icons.mic_off, color: muted ? Colors.white : Colors.blueAccent, size: 20.0, ), shape: new CircleBorder(), elevation: 2.0, fillColor: muted?Colors.blueAccent : Colors.white, padding: const EdgeInsets.all(12.0), ), RawMaterialButton( onPressed: () => _onCallEnd(context), child: new Icon( Icons.call_end, color: Colors.white, size: 35.0, ), shape: new CircleBorder(), elevation: 2.0, fillColor: Colors.redAccent, padding: const EdgeInsets.all(15.0), ), RawMaterialButton( onPressed: () => _onSwitchCamera(), child: new Icon( Icons.switch_camera, color: Colors.blueAccent, size: 20.0, ), shape: new CircleBorder(), elevation: 2.0, fillColor: Colors.white, padding: const EdgeInsets.all(12.0), ) ], ), ); } void _onCallEnd(BuildContext context) { Navigator.pop(context); } void _onToggleMute() { setState(() { muted = !muted; }); AgoraRtcEngine.muteLocalAudioStream(muted); } void _onSwitchCamera() { AgoraRtcEngine.switchCamera(); }最终整合现在两个部分的UI都完成了,我们接下去要将这两个组件通过Stack组装起来。 @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.channelName), ), backgroundColor: Colors.black, body: Center( child: Stack( children: <Widget>[_viewRows(), _toolbar()], )));清理若只在当前页面使用声网SDK,则需要在离开前调用destroy接口将SDK实例销毁。若需要跨页面使用,则推荐将SDK实例做成单例以供不同页面访问。同时也要注意对原生渲染容器的释放,可以至直接使用removeNativeView方法释放对应的原生容器, @override void dispose() { // clean up native views & destroy sdk _sessions.forEach((session) { AgoraRtcEngine.removeNativeView(session.viewId); }); _sessions.clear(); AgoraRtcEngine.destroy(); super.dispose(); }最终效果:总结Flutter作为新生事物,难免还是有他不成熟的地方,但我们已经从他现在的进步上看到了巨大的潜力。从目前的体验来看,只要有充足的社区资源,在Flutter上开发跨平台应用还是比较舒服的。声网提供的Flutter SDK基本已经覆盖了原生SDK提供的大部分方法,开发体验基本可以和原生SDK开发保持一致。这次也是基于学习的态度写下了这篇文章,希望对于想要使用Flutter开发RTC应用的同学有所帮助。文章中讲解的完整代码都可以在 Agora-Flutter-Quickstart 找到。 ...

February 22, 2019 · 5 min · jiezi

开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题

导语发布app后,开发者最头疼的问题就是如何解决交付后的用户侧问题的还原和定位,是业界缺乏一整套系统的解决方案的空白领域,闲鱼技术团队结合自己业务痛点在flutter上提出一套全新的技术思路解决这个问题。我们透过系统底层来捕获ui事件流和业务数据的流动,并利用捕获到的这些数据通过事件回放机制来复现线上的问题。本文先介绍flutter触摸手势事件原理,接着介绍里面怎样录制flutter ui手势事件,然后介绍怎样还原回放flutter ui手势事件,最后附上包括native录制回放的整体框架图。为了便于理解本文,读者可以先阅读我之前写的关于native录制和回放文章《千人千面线上问题回放技术》背景现在的app基本都会提供用户反馈问题的入口,然而提供给用户反馈问题一般有两种方式:直接用文字输入表达,或者截图直接录制视频反馈这两种反馈方式常常带来以下抱怨:用户:输入文字好费时费力开发1:看不懂用户反馈说的是什么意思?开发2:大概看懂用户说的是什么意思了,但是我线下没办法复现哈开发3:看了用户录制的视频,但是我线下没办法重现,也定位不到问题所以:为了解决以上问题,我们用一套全新的思路来设计线上问题回放体系Flutter 手势基础知识如果要录制和回放flutter ui事件,那么我们首先必须了解flutter ui手势基本原理。1. Flutter UI触摸原始数据Pointer我们可以把Flutter中的手势系统分两层概念来理解。第一层概念为原始触摸数据(pointer),它描述了屏幕上指针(例如,触摸,鼠标和触控笔)的时间,类型,位置和移动。 第二层概念为手势,描述由一个或多个原始移动数据组成的语义动作。一般情况下单独的原始触摸数据没有任何意义。原始触摸数据是由系统传给native,native再通过flutter view channel传给flutter。flutter接收native传来的原始数据接口如下: void _handlePointerDataPacket(ui.PointerDataPacket packet) { // We convert pointer data to logical pixels so that e.g. the touch slop can be // defined in a device-independent manner. _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, ui.window.devicePixelRatio)); if (!locked) _flushPointerEventQueue(); }2. Flutter UI碰撞测试当屏幕接收到触摸时,dart Framework会对您的应用程序执行碰撞测试,以确定触摸与屏幕相接的位置存在哪些视图(renderobject)。 触摸事件然后被分发到最内部的renderobject上。 从最内部renderobject开始,这些事件在renderobject树中向上冒泡传递,通过冒泡传递最后把所有的renderobject遍历出来,从这个传递机制可想而知,遍历出来renderobject列表里的最后一个是WidgetsFlutterBinding(严格来讲WidgetsFlutterBinding不是renderobject),后面会介绍到WidgetsFlutterBinding。 void _handlePointerEvent(PointerEvent event) { assert(!locked); HitTestResult result; if (event is PointerDownEvent) { assert(!_hitTests.containsKey(event.pointer)); result = HitTestResult(); hitTest(result, event.position); _hitTests[event.pointer] = result; assert(() { if (debugPrintHitTestResults) debugPrint(’$event: $result’); return true; }()); } else if (event is PointerUpEvent || event is PointerCancelEvent) { result = _hitTests.remove(event.pointer); } else if (event.down) { result = _hitTests[event.pointer]; } else { return; // We currently ignore add, remove, and hover move events. } if (result != null) dispatchEvent(event, result); }上面代码以 histTest()检测当前触摸 pointer event 涉及到哪些视图。最后通过dispatchEvent(event, result)来处理该事件。void dispatchEvent(PointerEvent event, HitTestResult result) { assert(!locked); assert(result != null); for (HitTestEntry entry in result.path) { try { entry.target.handleEvent(event, entry); } catch (exception, stack) { } } }上面的代码就是用来分别调用每个视图(RenderObject)的手势识别器独自处理当前触摸事件(决定是否接收此事件)。entry.target是每个widget对应的RenderObject,所有的RenderObject都需要实现(implements)HitTestTarget类的接口,HitTestTarget里面有就有handleEvent这个接口,所以每个RenderObject都需要实现handleEvent这个接口, 这个接口就是用来处理手势识别。abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget除了最后一个WidgetsFlutterBinding外,其他视图RenderObject调用自己的handleEvent来识别手势,其作用就是判断当前手势是否要放弃,如果不放弃则丢到一个路由器里(这个路由器就是手势竞技场)最后由WidgetsFlutterBinding 调用handleEvent统一决议这些手势识别器最终谁胜出,所以这里WidgetsFlutterBinding.handleEvent其实就是统一处理接口,它的代码如下: void handleEvent(PointerEvent event, HitTestEntry entry) { pointerRouter.route(event); if (event is PointerDownEvent) { gestureArena.close(event.pointer); } else if (event is PointerUpEvent) { gestureArena.sweep(event.pointer); } }3. Flutter UI手势决议从上面的介绍可以得出一次触摸事件可能触发多个手势识别器。框架通过让每个识别器加入一个“手势竞争场”来决议用户想要的手势。“手势竞争场”使用以下规则来决议哪个手势胜出,非常简单在任何时候,任何识别器都可以自己宣布失败并主动离开“手势竞争场”。如果在当前“竞争场”中只剩下一个识别器,那么剩下来的就是赢家,赢家意味着独自接收此触摸事件并做出响应动作在任何时候,任何识别器都可以自己宣布胜利,并且最终就是它胜利,所有剩下的其他识别器都会失败4. Flutter UI手势例子下面示例表示屏幕window由ABCDEFKG视图组成,其中A视图是根视图,即是最底下的视图。红圈表示触摸点位置,触摸落在G视图的中间位置。根据碰撞测试,遍历出响应此触摸事件的视图路径:WidgetsFlutterBinding <— A <— C <— K <— G (其中GKCA是renderObject)遍历路径列表后,开始调用各自的视图(GKCA)entry.target.handleEvent来把自己识别器放到竞技场里参加决议,当然有些视图由于根据自己的逻辑判断主动放弃识别该触摸事件。这个处理过程如下图按G->K->C->A->WidgetsFlutterBinding顺序分别调用handleEvent()方法,最后通过WidgetsFlutterBinding调用自己的handleEvent()接口来统一决议最终哪个手势识别器胜出。胜出的那个手势识别器通过回调方法回调到上层业务代码,流程如下Flutter UI录制从上面的flutter手势处理可知,我们只需要在手势识别器回调上包装回调方法,即可拦截到手势回调方法,这样我们就可以在拦截过程读到WidgetsFlutterBinding <— A <— C <— K <— G链路的这棵视图树。我们只需要把这个棵树,树上的节点相关属性和手势类型记录下来,那回放时,通过这些信息去匹配到当前界面上的对应视图即可回放。下面是tap事件的录制代码,其他类型手势的录制代码原理一样,这里略过。 static GestureTapCallback onTapWithRecord(GestureTapCallback orgOnTap, BuildContext context) { if (null != orgOnTap && null != context) { final GestureTapCallback onTapWithRecord = () { if(bStartRecord) { saveTapInfo(context, TouchEventUIType.OnTap,null); } if (null != orgOnTap) { orgOnTap(); } }; return onTapWithRecord; } return orgOnTap; }static void saveTapInfo(BuildContext context, TouchEventUIType type, Offset point) { if(null == point && null != pointerPacketList && pointerPacketList.isNotEmpty) { final ui.PointerDataPacket last = pointerPacketList.last; if(null != last && null != last.data && last.data.isNotEmpty) { final ui.Rect rect = QueReplayTool.getWindowRect(context); point = new Offset(last.data.last.physicalX / ui.window.devicePixelRatio - rect.left, last.data.last.physicalY /ui.window.devicePixelRatio - rect.top); } } final RecordInfo record = createTapRecordInfo(context, type, point); if(null != record) { FlutterQuestionReplayPlugin.saveRecordDataToNative(record); } clearPointerPacketList(); }录制流程图如下:Flutter UI回放ui回放分两部分,第一部分通过录制的相关信息match到当前界面相应视图,第二部分是在此视图上进行模拟相关手势动作,这部分是个难点,也是重点,其中涉及到怎样生成原始的触摸数据信息,里面有时间,类型,坐标,方向,如果这些信息设置不合理或者错误会导致crash,还有滚动距离不符需要补偿,怎么补偿等等。下面是滚动事件回放流程图,其他类型手势的回放原理一样。上面的预处理,识别消耗指的是在滚动开始时,手势识别器要判断是否符合滚动手势所需要滚动的距离。所以我们为了让其控件滚动首先要生成一些触摸点数据,让手势识别器识别为滚动事件。这样才能进行后续的滚动动作。下面是滚动处理逻辑代码,如下: void verticalScroll(double dstPoint, double moveDis) { preReplayPacket = null; if (0.0 != moveDis) { //此处计算滚动方向,和滚动单元像素偏移,由于代码太长略过 int count = ((ui.window.devicePixelRatio * moveDis) / (unit.abs())).round() * 2; if (count < minCount) { count = minCount; //保证最少偏移50/2=25 小于这个数 可能没反应,因为被其他控件检测滚动消耗掉了 //还有就是如果count太小,count被scroll view消耗完前并没有滚动,这是就触摸结束了(ui.PointerChange.up),那可能引起cell //点击事件跳转事件 } final double physicalX = rect.center.dx * ui.window.devicePixelRatio; //376.0; double physicalY; final double needOffset = (count * unit).abs(); final double targetHeight = rect.size.height * ui.window.devicePixelRatio; final int scrollPadding = rect.height ~/ 4; if (needOffset <= targetHeight / 2) { physicalY = rect.center.dy * ui.window.devicePixelRatio; } else if (needOffset > targetHeight / 2 && needOffset < targetHeight) { physicalY = (orgMoveDis > 0) ? (rect.bottom - scrollPadding) * ui.window.devicePixelRatio : (rect.top + scrollPadding) * ui.window.devicePixelRatio; } else { physicalY = (orgMoveDis > 0) ? (rect.bottom - scrollPadding) * ui.window.devicePixelRatio : (rect.top + scrollPadding) * ui.window.devicePixelRatio; count = ((rect.height - 2 * scrollPadding) * ui.window.devicePixelRatio / unit.abs()) .round(); } final List<ui.PointerDataPacket> packetList =createTouchDataList(count, unit, physicalY, physicalX); exeScroolTouch(packetList,dstPoint); } else { new Timer(const Duration(microseconds: fpsInterval), () { replayScrollEvent(); }); } }上面代码大概处理逻辑:1.计算滚动方向,每个生成的触摸数据偏移单元 2.计算滚动的开始位置 3.生成滚动原始触摸数据列表 4.循环发射原始触摸数据,并计算是否滚动到指定的位置,如果还达不到指定的位置,则继续补给生成滚动原始触摸数据列表代码如下:第一数据是down触摸数据,其他都是move触摸数据。up数据在这里不需要生成,当滚动距离到目标位置后才另外生成up触摸数据。为什么这样设计?此处留给大家思考!List<ui.PointerDataPacket> createTouchDataList(int count,double unit,double physicalY,double physicalX) { final List<ui.PointerDataPacket> packetList = <ui.PointerDataPacket>[]; int uptime = 0; for (int i = 0; i < count; i++) { ui.PointerChange change; if (0 == i) { change = ui.PointerChange.down; } else { change = ui.PointerChange.move; physicalY += unit; if (i < 15) //前面几个点让在短时间内偏移的距离长点 这样避开单击和长按事件 { physicalY += unit; physicalY += unit; } } uptime += replayOnePointDuration; final ui.PointerData pointer = new ui.PointerData( timeStamp: new Duration(microseconds: uptime), change: change, kind: ui.PointerDeviceKind.touch, device: 1, physicalX: physicalX, physicalY: physicalY, buttons: 0, pressure: 0.0, pressureMin: 0.0, pressureMax: touchPressureMax, distance: 0.0, distanceMax: 0.0, radiusMajor: downRadiusMajor, radiusMinor: 0.0, radiusMin: downRadiusMin, radiusMax: downRadiusMax, orientation: orientation, tilt: 0.0); final List<ui.PointerData> pointerList = <ui.PointerData>[]; pointerList.add(pointer); final ui.PointerDataPacket packet = new ui.PointerDataPacket(data: pointerList); packetList.add(packet); } return packetList; }循环发射原始触摸数据,并判断是否继续补给代码如下:我们以定时器不断的往系统发送触摸数据,每次发送数据前都需要判断是否已经达到目标位置。void exeScroolTouch(List<ui.PointerDataPacket> packetList,double dstPoint){ Timer.periodic(const Duration(microseconds: fpsInterval), (Timer timer) { final ScrollableState state = element.state; final double curPoint = state.position.pixels;//ui.window.physicalSize.height*state.position.pixels/RecordInfo.recordedWindowH; final double offset = (dstPoint - curPoint).abs(); final bool existOffset = offset > 1 ? true : false; if (packetList.isNotEmpty && existOffset) { sendTouchData(packetList, offset); } else if (packetList.isNotEmpty) { record.succ = true; timer.cancel(); packetList.clear(); if (null != preReplayPacket) { final ui.PointerDataPacket packet = createUpTouchPointPacket(); if (null != packet) { ui.window.onPointerDataPacket(packet); } } new Timer(const Duration(microseconds: fpsInterval), () { replayScrollEvent(); }); } else if (existOffset) { record.succ = true; timer.cancel(); packetList.clear(); final ui.PointerDataPacket packet = createUpTouchPointPacket(); if (null != packet) { ui.window.onPointerDataPacket(packet); } verticalScroll(dstPoint, dstPoint - curPoint); } else { finishReplay(); } }); }问题回放整体框架图下图包括native和flutter,包括ui和数据。总结本文大概介绍了flutter ui手势问题回放,核心部分由四部分组成,一是flutter手势原理,二是flutter ui录制,三是flutter ui回放,四是整个框架图,由于篇幅有限,这四分部都介绍比较笼统,不够详细,请谅解!flutter录制回放代码其实很多,我这里只是附上比较重要,而且易于理解的代码。其他不重要或不易读懂的代码都省掉了。如果对里面的技术点感兴趣,你可以关注我们的公众号。我们后续会单独对里面的技术点详细深入的分析发文。如果觉得上面有错误的地方,请指出。谢谢后续的深入到目前为止,我们现在的flutter ui录制回放已经开发完成,但我们后续还需要继续优化和深入。我们后续从两个点来深入优化:1.如何在回放时模拟的触摸事件更逼真,比如滚动加速度,一次的滚动其实是一个曲线变化的过程 2.解决手势录制和回放不一致性。举个例子,在键盘里输入123,我们录制时截获到了手势123,但是由于业务上层的bug导致了当时输入3没有响应,输入框里只显示12,我们回放时模拟手势123,最终回放完后输入框显示123,所以这样导致录制和回放不一致性,这个问题怎么解决?这是个麻烦的问题,我们后续会解决。而且已经有这解决方案。本文作者:闲鱼技术–镜空阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 20, 2019 · 4 min · jiezi

Flutter入门二——项目结构及配置文件简介

前言环境搭建完成之后,我们来看看Flutter:New Project后生成的项目结构。具体环境搭建可以参考:w7上使用VSCode配置Flutter开发环境项目结构pubspec.yaml配置文件说明作用 每个发布包都需要一些元数据,以便能够指定它的依赖项。与他人共享的发布包还需要提供一些其他信息,以便用户能够发现它们。所有这些元数据都放在一个名为pubspec.yaml的yamll(YAML 是专门用来写配置文件的语言,非常简洁和强大,远比 JSON 格式方便)文件中。支持的字段 name,version,description,author/authors,homepage,repository,issue_tracker,documentation,dependencies,dev_dependcencies,dependency_overrides,environment,excutables,publish_to与Node.js的package.json文件的类比 (因为我在web开发里是使用Node.js来做包管理的,所以类比nonde.js的package.json对于由web入门学习flutter的同学会比较容易理解。)简单而言,pubspec.yaml文件的作用就相当于Node.js的package.json文件,是用来进行包管理的; 两者都分离了两个环境,dependencies和dev_dependencies; 前者使用yaml语法来定义,后者使用json语法; pubspec.yaml对版本的约束规则与package.json规则类似; Node.js Flutter 安装依赖 npm install flutter package get 升级依赖包版本 npm update flutter packages upgrade 协同开发保证包版本一致 package-lock.json pubspec.lock 关于此后续可以再丰富~~默认的配置如下,有备注解释:#包名name: todo_app#描述信息description: A new Flutter project.#版本号version: 1.0.0+1#指定环境environment: sdk: “>=2.0.0-dev.68.0 <3.0.0”#指定包依赖dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 english_words: ^3.1.0#指定开发环境下的包依赖dev_dependencies: flutter_test: sdk: flutter# For information on the generic Dart part of this file, see the# following page: https://www.dartlang.org/tools/pub/pubspec# The following section is specific to Flutter.flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true ...

February 18, 2019 · 1 min · jiezi

Flutter使用Cipher2插件实现AES加解密

Flutter是当下最流行的新兴APP跨平台开发架构。学习需趁早。因为我的项目需要使用AES加解密,而flutter package中并没有支持Dart 2的AES加密库,所以写了Cipher2插件并拿出来开源给大家用。本文介绍如何使用Cipher2插件在Flutter app中实现AES加密解密。本文不讲述如何安装配置Flutter开发环境。如有需要我会写关于安装配置flutter开环环境的文章。Cipher2插件地址:https://pub.dartlang.org/pack…https://github.com/shyandsy/c…各位如果有其他加密算法需求,请在github发issue,我会尽快跟进。PR is welcome!创建项目打开cmd命令行,cd命令定位到你想要创建项目的目录。然后创建flutter app项目flutter create cipher2_test用vscode打开项目目录安装Cipher2插件打开上图中pubspec.yaml文件的dependencies中,添加如下内容。然后ctrl + s保存,vscode会自动为你安装好插件。dependencies: flutter: sdk: flutter cipher2: anyCipher2的API解释Cipher2插件目前支持AES加密的cbc(128位 padding7)模式和gcm模式(128位)。插件提供了5个方法来实现加密解密。本插件所有字符串均使用UTF8编码。在处理第三方密文的时候,请注意字符串编码,以免不必要的麻烦。AES cbc 128位padding7加密/Cipher2.encryptAesCbc128Padding7参数: plainText: 被加密字符串 key:128 bit字符串 iv: 128 bit字符串返回: 经过base64编码的密文字符串/String encryptedString = await Cipher2.encryptAesCbc128Padding7(plainText, key, iv);AES cbc 128位padding7解密/Cipher2.decryptAesCbc128Padding7参数: encryptedString: base64编码的密文字符串 key:128 bit字符串 iv: 128 bit字符串返回: 明文字符串/String decryptedString = await Cipher2.decryptAesCbc128Padding7(encryptedString, key, iv);生成GCM模式的nonceString nonce = Cipher2.generateNonce()AES gcm 128位加密/Cipher2.encryptAesGcm128参数: plainText: 被加密字符串 key:128 bit字符串 nonce: based4编码的92bit nonce,可以用Cipher2.generateNonce()生成返回: 经过base64编码的密文字符串/String encryptedString = await Cipher2.encryptAesGcm128(plaintext, key, nonce);AES gcm 128位解密/Cipher2.decryptAesGcm128参数: encryptedString: base64编码的密文字符串 key:128 bit字符串 nonce: based4编码的92bit nonce,可以用Cipher2.generateNonce()生成返回: 明文字符串/result = await Cipher2.decryptAesGcm128(encryptedString, key, nonce);使用Cipher2插件官方提供了非常简单明了的测试用例,方便加密解密和异常捕获https://github.com/shyandsy/c…// Platform messages are asynchronous, so we initialize in an async method. Future<void> initPlatformState() async { String encryptedString; String plainText = ‘我是shyandsy,never give up man’; String key = ‘xxxxxxxxxxxxxxxx’; String iv = ‘yyyyyyyyyyyyyyyy’; String decryptedString; // 测试AES 128bit cbc padding 7加密 await testEncryptAesCbc128Padding7(); // 测试AES 128bit cbc padding 7解密 await testDecryptAesCbc128Padding7(); // 测试AES 128bit gcm加解密 await testEncryptAesGcm128(); // GenerateNonce(); // 加密,然后解密 try { // 加密 encryptedString = await Cipher2.encryptAesCbc128Padding7(plainText, key, iv); // 解密 decryptedString = await Cipher2.decryptAesCbc128Padding7(encryptedString, key, iv); } on PlatformException catch(e) { encryptedString = “”; decryptedString = “”; print(“exception code: " + e.code); print(“exception message: " + e.message); } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; setState(() { _plainText = plainText; _encryptedString = encryptedString; _decryptedString = decryptedString; }); }读一遍test case就会用了。这里不再重复 ...

February 1, 2019 · 2 min · jiezi

Flutter尝鲜3——动画处理<并行和串行>

本例的代码参考这里。并行动画当多个动画定义同时指向某个组件,并使用动画控制器启动时,就产生了并行动画(Parallel Animation)。例如我们可以让一个组件:移动的同时改变大小旋转的同时边界颜色闪烁圆形图片模糊的同时形状越来越方总之,掌握了动画原理以后我们知道,只要能将一个动画抽象值与一个组件的某个外观属性值联系起来,那么就能在动画中展现出连续平滑的外观变化。这一点,任何平台(Web、Android)的原理都是一致的。例子接前一篇的例子,我们让一个移动的正方形在位移过程中逐渐变为圆形。在已有的animation基础上,再添加一个新的animation用以控制动画组件的边角半径。class ParallelDemoState extends State<ParallelDemo> with SingleTickerProviderStateMixin { … Tween<double> slideTween = Tween(begin: 0.0, end: 200.0); Tween<double> borderTween = Tween(begin: 0.0, end: 40.0); // 添加边角半径变动范围 Animation<double> slideAnimation; Animation<double> borderAnimation; // 添加边角半径动画定义 @override void initState() { … controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this); slideAnimation = slideTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); borderAnimation = borderTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); // 定义边角半径动画 } … @override Widget build(BuildContext context) { return Container( width: 200, alignment: Alignment.centerLeft, child: Container( margin: EdgeInsets.only(left: slideAnimation.value), decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderAnimation.value), // 边角半径的属性上添加动画 color: Colors.blue, ), width: 80, height: 80, ), ); }}串行动画串行动画(Sequential Animation)顾名思义,多个动画像肉串一样一个接一个的发生。但这只是从现象上观察出的结果,实际的运行方式和并行动画差别不大。串行动画的关键之处在于,它为每个动画的发生设定了一个计时器,只有到特定时间点时,特定的动画效果才会发生。例如设计一个3秒钟的动画:移动动画从0秒开始,持续1秒旋转动画从1秒开始,持续1.5秒缩放动画从2秒开始,持续0.7秒那么,最后的动画效果便是:01秒,动画元素在移动12秒,动画元素在旋转22.5秒,动画既在旋转又在缩放2.52.7秒,动画在缩放2.7~3秒,动画静止不动例子在串行动画例子的基础上,我们加上计时器Interval的处理。Interval有三个参数,前两个参数指示了动画的开始和结束时间。这两个参数都是以动画控制器的Duration时长的比例来计算的。例如:Slide动画分别为0.0和0.5,表示动画从0秒(2000ms 0.0)这个时间点开始,至1秒(2000ms 0.5)这个时间点结束Border动画分别为0.5和1.0,表示动画从1秒(2000ms 0.5)这个时间点开始,至2秒(2000ms 1.0)这个时间点结束class SequentialDemoState extends State<ParallelDemo> with SingleTickerProviderStateMixin { … @override void initState() { … controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this); // slideAnimation = slideTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); // borderAnimation = borderTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); // 定义边角半径动画 // 换一种写法,加入Interval slideAnimation = slideTween.animate(CurveTween(curve: Interval(0.0, 0.5, curve: Curves.linear)).animate(controller)); borderAnimation = borderTween.animate(CurveTween(curve: Interval(0.5, 1.0, curve: Curves.linear)).animate(controller)); } … @override Widget build(BuildContext context) { return Container( width: 200, alignment: Alignment.centerLeft, child: Container( margin: EdgeInsets.only(left: slideAnimation.value), decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderAnimation.value), // 边角半径的属性上添加动画 color: Colors.blue, ), width: 80, height: 80, ), ); }} ...

January 31, 2019 · 1 min · jiezi

如何在Flutter上优雅地序列化一个对象

序列化一个对象才是正经事对象的序列化和反序列化是我们日常编码中一个非常基础的需求,尤其是对一个对象的json encode/decode操作。每一个平台都会有相关的库来帮助开发者方便得进行这两个操作,比如Java平台上赫赫有名的GSON,阿里巴巴开源的fastJson等等。而在flutter上,借助官方提供的JsonCodec,只能对primitive/Map/List这三种类型进行json的encode/decode操作,对于复杂类型,JsonCodec提供了receiver/toEncodable两个函数让使用者手动“打包”和“解包”。显然,JsonCodec提供的功能看起来相当的原始,在闲鱼app中存在着大量复杂对象序列化需求,如果使用这个类,就会出现集体“带薪序列化”的盛况,而且还无法保证正确性。来自官方推荐聪明如Google官方,当然不会坐视不理。json_serializable的出现就是官方给出的推荐,它借助Dart Build System中的build_runner和json_annotation库,来自动生成fromJson/toJson函数内容。(关于使用build_runner生成代码的原理,之前兴往同学的文章已经有所提及)关于如何使用json_serializable网上已经有很多文章了,这里只简单提一些步骤:Step 1 创建一个实体类Step 2 生成代码:来让build runner生成序列化代码。运行完成后文件夹下会出现一个xxx.g.dart文件,这个文件就是生成后的文件。Step 3 代理实现:把fromJson和toJson操作代理给上面生成出来的类我们为什么不用这个实现json_serializable完美实现了需求,但它也有不满足需求的一面:使用起来有些繁琐,多引入了一个类很重要的一点是,大量的使用"as"会给性能和最终产物大小产生不小的影响。实际上闲鱼内部的《flutter编码规范》中,是不建议使用"as"的。(对包大小的影响可以参见三笠同学的文章,同时dart linter也对as的性能影响有所描述)一种正经的方式基于上面的分析,很明显的,需要一种新的方式来解决我们面临的问题,我们暂且叫它,fish-serializable需要实现的功能我们首先来梳理一下,一个序列化库需要用到:获取可序列化对象的所有field以及它们的类型信息能够构造出一个可序列化对象,并对它里面的fields赋值,且类型正确支持自定义类型最好能够解决泛型的问题,这会让使用更加方便最好能够轻松得在不同的序列化/反序列化方式中切换,例如json和protobuf。困难在哪里flutter禁用了dart:mirrors,反射API无法使用,也就无法通过反射的方式new一个instance、扫描class的fields。泛型的问题由于dart不进行类型擦出,可以获取,但泛型嵌套后依然无法解开。Let’s rock无法使用dart:mirrors是个“硬”问题,没有反射的支持,类的内容就是一个黑盒。于是我们在迈出第一步的时候就卡壳了- -!这个时候笔者脑子里闪过了很多画面,白驹过隙,乌飞兔走,啊,不是…是c++,c++作为一种无法使用反射的语言,它是如何实现对象的 序列化/反序列化 操作的呢?一顿搜索猛如虎之后,发现大神们使用创建类对象的回调函数配合宏的方式来实现c++中类似反射这样的操作。这个时候,笔者又想到了曾经朝夕相处的Android(现在已经变成了flutter),Android中的Parcelable序列化协议就是一个很好的参照,它通过writeXXXAPIs将类的数据写入一个中间存储进行序列化,再通过readXXXAPIs进行反序列化,这就解决了我们上面提到的第一个问题,既如何将一个类的“黑盒子”打开。同时,Parcelable协议中还需要使用者提供一个叫做CREATOR的静态内部类,用来在反序列化的时候反射创建一个该类的对象或对象数组,对于没有反射可用的我们来说,用c++的那种回调函数的方式就可以完美解决反序列化中对象创建的问题。于是最终我们的基本设计就是:ValueHolder这是一个数据中转存储的基类,它内部的writeXXX APIs提供展开类内部的fields的能力,而readXXX则用来将ValueHolder中的内容读取赋值给类的fields。readList/readMap/readSerializable函数中的type argument,我们把它作为外部想要解释数据的方式,比如readSerializable<T>(key: ‘object’),表示外部想要把key为object的值解释为T类型。FishSerializableFishSerializable是一个interface,creator是个一个get函数,用来返回一个“创建类对象的回调”,writeTo函数则用来在反序列化的时候放置ValueHoder->fields的代码。JsonSerializer它继承于FishSerializer接口,实现了encode/decode函数,并额外提供encodeToMap和decodeFromMap功能。JsonSerializer类似JsonCodec,直接面向使用者用来json encode/decode以上,我们已经基本做好了一个flutter上支持对象序列化/反序列化操作的库的基本架构设计,对象的序列化过程可以简化为:由于ValueHolder中间存储的存在,我们可以很方便得切换 序列化/反序列器,比如现有的JsonSerializer用来实现json的encode/decode,如果有类似protobuf的需求,我们则可以使用ProtoBufSerializer来将ValueHolder中的内容转换成我们需要的格式。困难是不存在的有了基本的结构设计之后,实现的过程并非一帆风顺。如何匹配类型?为了能支持泛型容器的解析,我们需要类似下面这样的逻辑:List<SerializableObject> list = holder.readList<SerializableObject>(key: ’list’);List<E> readList<E>({String key}){ List<dynamic> list = _read(key);}E _flattenList<E>(List<dynamic> list){ list?.map<E>((dynamic item){ // 比较E是否属于某个类型,然后进行对应类型的转换 });}在Java中,可以使用Class#isAssignableFrom,而在flutter中,我们没有发现类似功能的API提供。而且,如果做下面这个测试,你还会发现一些很有意思的细节:void main() { print(‘int test’); test<int>(1); print(’\r\nint list test’); test<List<int>>(<int>[]); print(’\r\nobject test’); test<A<int>>(A<int>());}void test<T>(T t){ print(T); print(t.runtimeType); print(T == t.runtimeType); print(identical(T, t.runtimeType));}class A<T>{}输出的结果是:可以看到,对于List这样的容器类型,函数的type argument与instance的runtimeType无法比较,当然如果使用t is T,是可以返回正确的值的,但需要构造大量的对象。所以基本上,我们无法进行类型匹配然后做类型转换。如何解析泛型嵌套?接下去就是如何分解泛型容器嵌套的问题,考虑如下场景:Map<String, List<int>> listMap;listMap = holder.readMap<String, List<int>>(key: ’listMap’);readMap中得到的value type是一个List<int>,而我们没有API去切割这个type argument。所以我们采用了一种比较“笨”也相对实用的方式。我们使用字符串切割了type argument,比如:List<int> => <String>[List<int>, List, int]然后在内部展开List或Map的时候,使用字符串匹配的方式匹配类型,在目前的使用中,完美得支持了标准List和Map容器互相嵌套。但目前无法支持标准List和Map之外的其他容器类型。What’s moreIDE插件辅助写过Android的Parcelable的同学应该有种很深刻的体会,Parcelable协议中有大量的“机械”代码需要写,类似设计的fish-serializable也一样。为了不被老板和使用库的同学打死,同时开发了fish-serializable-intelij-plugin来自动生成这些“机械”代码。与json_serializable的对比fish-serializable在使用上配合IDE插件,减少了大量的"as"操作符的使用,同时在步骤上也更加简短方便。相比于json_annotation生成的代码,fish-serializable生成的代码也更具可读性,方便手动修改一些代码实现。fish-serializable可以通过手动接管 序列化/反序列化 过程的方式完美兼容json_annotation等其他方案。目前闲鱼app中已经开始大量使用。开源计划fish-serializable和fish-serializable-intelij-plugin都在开源计划中,相信不久就可以与大家见面,尽请期待~本文作者:闲鱼技术-海潴阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 30, 2019 · 1 min · jiezi

flutter常用组件API(第三期)

内容如果对你有帮助,帮忙点下赞,你的点赞是我更新最大的动力,谢谢啦!如果在开发的过程遇到问题可以一起讨论,可以加我的QQ群!167646174!具体代码见github ,欢迎各位Star,以及提issues!1.CheckBox(说实话,这个组件有点小丑)API作用可选参数value复选框值自定义activeColor选中的颜色Colortristate如果为 true,那么复选框的值可以是 true,false 或 nulltrue/falsematerialTapTargetSize点击区域padded:向四周扩展48区域 shrinkWrap:控件区域onChanged改变后触发事件Func2.CheckboxListTileAPI作用可选参数title标题-subtitle副标题-secondary前缀-selected文字是否高亮true/falsedense标题字变小true/falseisThreeLine是否三行显示(第三行用啥显示目前还没看到用法)true/falsecontrolAffinity将控件放在何处相对于文本,leading 按钮显示在文字前面,platform,trailing 按钮显示在文字后面-(未完待续)具体代码见github ,欢迎各位Star,以及提issues不定期更新,根据工作繁忙程度决定.——————————-以下是相关文章—————————-flutter常见组件(第一期)flutter常见组件之Button(第二期)

January 30, 2019 · 1 min · jiezi

Flutter环境搭建遇到问题

具体环境搭建搭建可以参考慕课网教程:https://www.imooc.com/video/1…这里就不一一叙述了,在搭建过程中不像教程上那么顺利,遇到以下问题和大家分享一下,在android sdutio启动flutter时候现please connect a device, or see flutter.io/setup for getting started instructions.经过一些资料查找主要原因未为:一、adb未启动1.原因为未加入环境变量2.adb默认端口号为5037通过命令查看被360手机助手占用,结束进程重启adb服务即可。二、右击android sdutio选择管理员身份运行,重启ide。在vscode中运行run命令时出现Could not download fastutil.jar (it.unimi.dsi:fastutil:7.2.0) > Could not get resource ‘https:解决方法:1.使用科学上网方式解决2.使用阿里云阿里云中央仓库在build.gradle中加入以下代码buildscript { repositories { maven{ url ‘http://maven.aliyun.com/nexus/content/groups/public/'} google()}dependencies { classpath ‘com.android.tools.build:gradle:3.2.1’}}allprojects {repositories {maven{ url ‘http://maven.aliyun.com/nexus/content/groups/public/'} google()}}保存代码重启运行出现久违的画面

January 27, 2019 · 1 min · jiezi

Flutter尝鲜2——动画处理<基础>

本例的代码参考这里。概述动画处理的基本原理是,对组件(widget)的某个或某组属性设置一组连续变化的值,这些值在一定时间间隔内不断被应用到该属性上,使得组件的外观看上去在进行平滑而连续的变动。例如2秒内每隔0.1s将一个组件的x轴坐标加1,那么该组件看上去就是从左至右移动了2秒共20个单位。处理组成部分具体到Flutter,动画处理主要分为三个部分:动画控制器(AnimationController),控制整个动画运行,包括开始结束和动画时长等。动画抽象(Animation),描述了动画运动的速率,例如组件是加速还是匀速,或者其它变化。变动范围(Tween),定义了动画组件属性值的变化范围,例如从坐标(0, 0)移动到(20, 0)处理流程上述三大组件,控制了整个动画的运行。用文字描述,其流程主要包括:初始化动画控制器,设定动画的时长,初始值等(如上例:2秒时长)初始化变动范围(如上例:Offset从[0, 0]到[20, 0])初始化动画抽象,定义它的运动速率(如上例:匀速变动)将动画描述的值,赋值到动画组件的对应属性上开始执行动画(调用动画控制器的开始方法)动画执行结束AnimationController定义AnimationController是一个特殊的Animation对象。创建一个AnimationController时,需要传递一个vsync参数。设置此参数的目的,是希望屏幕每一帧画面变化时能够被通知到。也就是说,屏幕刷新的每一帧,AnimationController都会生成一个新的值(同样也意味着,如果在屏幕外那么就不被触发)。这样动画组件就能够完成一个连续平滑的动画动作。Tickers can be used by any object that wants to be notified whenever a frame triggers。AnimationControler通常是在一个StatefulWidget中被声明,并且附带一个叫做SingleTickerProviderStateMixin的Mixin(原因就在上面说的,要设置vsync参数)。class AnimationDemo extends StatefulWidget { AnimationDemoState createState() => AnimationDemoState();}class AnimationDemoState extends State<AnimationDemo> with SingleTickerProviderStateMixin { AnimationController controller; @override void initState() { super.initState(); controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this); … } @override void dispose() { controller.dispose(); // 离开时需要销毁controller super.dispose(); } …}当Animation和Tween的设置完成后,简单调用controller.forward()即可开始动画。Tween定义Tween就是要改变的属性值的变动范围。它可以是任意的属性类如Offset或者Color,最常见的是double。… AnimationController controller; Tween<double> slideTween = Tween(begin: 0.0, end: 20.0);…Animation定义Animation对象本身可以看做是动画中所有变化值的一个集合。它包含了变化区间内的所有可取值,并返回给动画组件当前的变动值。Animation在使用中要设置的,是他的变动速率,如Curves.linear(线性变化)。… AnimationController controller; Tween<double> slideTween = Tween(begin: 0.0, end: 20.0); Animation<double> animation; @override void initState() { super.initState(); … animation = slideTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); }…动画组件定义为了说明简单,在build方法中嵌套两个Container组件,外部容器Container的paddingLeft跟随动画变动,达到移动内部Container的目的。class AnimationDemoState extends State<AnimationDemo> with SingleTickerProviderStateMixin { … @override Widget build(BuildContext context) { return Container( width: 200, alignment: Alignment.centerLeft, padding: EdgeInsets.only(left: animation.value), child: Container( color: Colors.blue, width: 80, height: 80, ), ); }}启动动画在启动动画之前有一个Flutter的基本概念要说明。做过React的同学很清楚,要想render方法重新执行,要么props有更新要么state有更新。在Flutter也同样如此,build方法同样依赖于state的更新才能重新执行。在AnimationController的说明中,我们知道因为设置了vsync所以屏幕刷新的每一帧都会更新它的值。所以可以在Controller上加上一个listener,每次有update都调用一下setState,以此达到重新渲染UI的目的。… @override void initState() { … animation.addListener(() => this.setState(() {})); controller.repeat(); // 动画重复执行 }调用controller.repeat()方法,动画会被反复执行。如果想只执行一次,那么可以使用controller.forward(); ...

January 25, 2019 · 1 min · jiezi

flutter常见组件之Button(第二期)

内容如果对你有帮助,帮忙点下赞,你的点赞是我更新最大的动力,谢谢啦!如果在开发的过程遇到问题可以一起讨论,可以加我的QQ群!167646174!也可以加我微信,在群里!具体代码见github ,欢迎各位Star,以及提issues!1.RaisedButtonAPI作用参数color背景色-padding与文字的内边距-textColor按钮内文字颜色-textTheme按钮主题-disabledColor按钮被禁用显示的颜色-disabledTextColor按钮被禁用时文字显示颜色-highlightColor击高亮的时候显示在控件上面,水波纹下面的颜色-splashColor水波纹颜色-colorBrightness按钮主题高亮-elevation按钮下面的阴影-highlightElevation高亮时候的阴影-disabledElevation按下时候的阴影clipBehavior抗锯齿能力-onHighlightChanged水波纹高亮时候回调-onPressed点击事件-shape拓展样式_icon小图标按钮只有IconButton才会使用到—扩展—1.1带斜角的按钮shape: BeveledRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20))),1.2圆按钮shape: CircleBorder( // 圆边颜色 side: BorderSide( color: Colors.black )),1.3圆角矩形按钮 shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10)) ),1.4两端半圆按钮shape: StadiumBorder(),2.OutlineButtonAPI同RaisedButton默认边线且背景透明的按钮3.FlatButtonAPI同RaisedButton4.ButtonBarAPI作用参数alignment对齐方式-mainAxisSize主轴大小,默认MainAxisSize.max-5.FloatingActionButtonAPI作用backgroundColor背景色elevation未点击的阴影值highlightElevation点击时的阴影值tooltip长按文字提示foregroundColor按钮里面文字小图标颜色具体代码见github ,欢迎各位Star,以及提issues!不定期更新,根据工作繁忙度决定!以下是往期相关文章:flutter常见组件API(第一期)

January 25, 2019 · 1 min · jiezi

Flutter v1.0 踩坑记

前面的安装这里就不叙述了。看这里:技术胖的Flutter(http://jspang.com/post/flutte…)按照步骤安装后,还是一直报错,到绝望。。。错误代码如下:Error running Gradle:ProcessException: Process “D:\project\Flutter\flutter_app\android\gradlew.bat” exited abnormally:> Configure project :appChecking the license for package Android SDK Platform 27 in C:\Users\Administrator.WINMICR-H4QQDHF\AppData\Local\Android\Sdk\licensesWarning: License for package Android SDK Platform 27 not accepted.FAILURE: Build failed with an exception.* Where:Build file ‘D:\project\Flutter\flutter_app\android\build.gradle’ line: 26* What went wrong:A problem occurred evaluating root project ‘android’.> A problem occurred configuring project ‘:app’. > Failed to install the following Android SDK packages as some licences have not been accepted. platforms;android-27 Android SDK Platform 27 To build this project, accept the SDK license agreements and install the missing components using the Android Studio SDK Manager. Alternatively, to transfer the license agreements from one workstation to another, see http://d.android.com/r/studio-ui/export-licenses.html Using Android SDK: C:\Users\Administrator.WINMICR-H4QQDHF\AppData\Local\Android\Sdk* Try:Run with –stacktrace option to get the stack trace. Run with –info or –debug option to get more log output. Run with –scan to get full insights.* Get more help at https://help.gradle.orgBUILD FAILED in 1s Command: D:\project\Flutter\flutter_app\android\gradlew.bat app:propertiesFinished with error: Please review your Gradle project setup in the android/ folder.解决办法:把项目的android/app目录下的build.gradle文件中的sdk版本号改掉,原本是27,改成28.路径:代码如下: ...

January 23, 2019 · 1 min · jiezi

Flutter尝鲜1——3步骤使用自定义Icon

官方IconFlutter本身自带了MaterialDesign的图标集,在pubspec.yaml中有如下配置…flutter: users-material-design: true…通过以上配置,就可以在代码中引用任何MD的官方图标(需翻墙)。这些图片都定义在了IconDatas中。Icon(Icons.favorite)第三方Icon第三方图标库和MD的图片库在使用上没有区别,但需要手动引入和配置路径。为了方便复用,我们可以把图标制作为一个第三方库来调用。例如:…import ‘package:my_icon/my_icon.dart’;…Icon icon = Icon(MyIcon.zhihu); # 知乎LOGO制作Icon库1.制作ttf文件一般我们会在iconfont.cn上去寻找合适的图标集或自行绘制,完成后打包下载,压缩包里有制作好的ttf文件。2.编写配置文件作为示例,在/lib目录下创建一个名为my_font的文件夹,文件夹中的pubspec.yaml内容如下:name: my_fontdescription: The font for my applicationauthor: Lynx <lynx86@126.com>homepage: http://www.a-lightyear.com/version: 1.0.0environment: sdk: “>=2.0.0-dev.28.0 <3.0.0"dependencies: flutter: sdk: flutterdev_dependencies: recase: “^2.0.0+1"flutter: fonts: - family: MyIcon fonts: - asset: lib/fonts/iconfont.ttf weight: 400从配置文件看出,iconfont下载的ttf文件放在/lib/my_font/lib/fonts/下面,该路径可以自行设置。3.编写库文件library font_social_flutter; import ‘package:flutter/widgets.dart’;class MyIcon { static const IconData zhihu = const _MyIconData(0xe6a2); static const IconData wechat = const _MyIconData(0xe697); static const IconData alipay = const _MyIconData(0xe698); static const IconData weibo = const _MyIconData(0xe6ab); static const IconData wechat_friends = const _MyIconData(0xe6ae); static const IconData qq = const _MyIconData(0xe6ac);}class _MyIconData extends IconData { const _MyIconData(int codePoint) : super( codePoint, fontFamily: ‘MyIcon’, fontPackage: ‘my_icon’, );}这里的0xe6a2即为每个Icon的unicode字符。在iconfont下载包里有一个html文件,打开后可以看到每个图片的unicode值。使用Icon引入Icon库在使用之前,需要把该库引入到当前flutter工程中。编辑flutter项目的pubspec.yaml,添加如下内容:…dependencies: flutter: sdk: flutter … my_icon: path: lib/my_icon/ # 在这里引入第三方icon库 ……使用Icon如开篇所述,在做好以上准备工作后,即可以如MD图标一般方便的引入自制的图标集。…import ‘package:my_icon/my_icon.dart’;…Icon icon = Icon(MyIcon.zhihu); # 知乎LOGO ...

January 22, 2019 · 1 min · jiezi

移动跨平台技术方案总结

“得移动端者得天下”,移动端取代PC端,成为了互联网行业最大的流量分发入口,因此不少公司制定了“移动优先”的发展策略。为了帮助读者更好地学习WEEX,本节将对React Native、Weex和Flutter等主流的跨平台方案进行简单的介绍和对比。React NativeReact Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的React框架在原生移动应用平台的衍生产物,目前主要支持iOS和安卓两大平台。RN使用Javascript语言来开发移动应用,但UI渲染、网络请求等均由原生端实现。具体来说,开发者编写的Javascript代码,通过中间层转化为原生控件后再执行,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域,并可以在不牺牲用户体验的前提下提高开发效率。作为一个跨平台技术框架,RN从上到下可以分为Javascript层、C++层和Native层。其中,C++层主要用于实现动态连结库(.so),作为中间适配层桥接,实现js端与原生端的双向通信交互,如下图所示是RN在Android平台上的通信原理图。在RN的三层架构中,最核心的就是中间的C++层,C++层最核心的功能就是封装JavaScriptCore,用于执行对js的解析。同时,原生端提供的各种Native Module(如网络请求,ViewGroup控件模块)和JS端提供的各种JS Module(如JS EventEmiter模块)都会在C++实现的so文件中保存起来,最终通过C++层中的保存的映射实现两端的交互。在RN开发过程中,大多数情况下开发人员并不需要需要了解RN框架的具体细节,只需要专注JS端的逻辑代码实现即可。但是需要注意的是,由于js代码是运行在独立的JS线程中,所以在js中不能处理耗时的操作,如fetch、图片加载和数据持久化等操作。最终,JS代码会被打包成一个bundle文件并自动添加到应用程序的资源目录下,而应用程序最终加载的也是打包后的bundle文件。RN的打包脚本位于“/node_modules/react-native/local-cli”目录下,打包后通过metro模块压缩成bundle文件,而bundle文件只包含打包js的代码,并不包含图片、多媒体等静态资源,而打包后的静态资源会是被拷贝到对应的平台资源文件夹中。总的来说,RN使用Javascript来编写应用程序,然后调用原生组件执行页面渲染操作,在提高了开发效率的同时又保留了Native的用户体验。并且,伴随着Facebook重构RN工作的完成,RN也将变得更快、更轻量、性能更好。Weex作为一套前端跨平台技术框架,Weex建立了一套源码转换以及Native与Js通信的机制。Weex表面上是一个客户端框架,但实际上它串联起了从本地开发、云端部署到分发的整个链路。具体来说,在开发阶段编写一个.we文件,然后使用Weex提供的weex-toolkit转换工具将.we文件转换为JS bundle,并将生成的JS bundle上传部署到云端,最后通过网络请求或预下发的方式加载至用户的移动应用客户端。当集成了Weex SDK的客户端接收到JS bundle文件后,调用本地的JavaScript引擎执行环境执行相应的JS bundle,并将执行过程中产生的各种命令发送到native端进行界面渲染、数据存储、网络通信以及用户交互响应。由上图可知,Weex框架中最核心的部分就是JavaScript Runtime。具体来说,当需要执行渲染操作时,在iOS环境下选择基于JavaScriptCore内核的iOS系统提供的JSContext,在Android环境下使用基于JavaScriptCore内核的JavaScript引擎。当JS bundle从服务器下载完成之后,Weex的Android、iOS和H5会运行一个JavaScript引擎来执行JS bundle,同时向各终端的渲染层发送渲染指令,并调度客户端的渲染引擎实现视图渲染、事件绑定和处理用户交互等操作。由于Android、iOS和H5等终端最终使用的是native渲染引擎,也就是说使用同一套代码在不同终端上展示的样式是相同的,并且Weex使用native引擎渲染的是native组件,所以在性能上比传统的WebView方案要好很多。当然,尽管Weex已经提供了开发者所需要的最常用的组件和模块,但面对丰富多样的移动应用研发需求,这些常用基础组件还是远远不能满足开发的需要,因此Weex提供了灵活自由的扩展能力,开发者可以根据自身的情况定制属于自己客户端的组件和模块,从而丰富Weex生态。FlutterFlutter是Google开源的移动跨平台框架,其历史最早可以追溯到2015年的Sky项目,该项目可以同时运行在Android、iOS和fuchsia等包含Dart虚拟机的平台上,并且性能无限接近原生。相较于RN和Weex使用Javascript作为编程语言与使用平台自身引擎渲染界面不同,Flutter直接选择2D绘图引擎库skia来渲染界面。如上图所示,Flutter框架主要由Framework和Engine层组成,而我们基于Framework开发App最终会运行在Engine上。其中,Engine是Flutter提供的独立虚拟机,正是由于它的存在Flutter程序才能运行在不同的平台上,实现跨平台运行的能力。与RN和Weex使用原生控件渲染界面不同,Flutter并不需要使用原生控件来渲染界面,而是使用Engine来绘制Widget(Flutter显示单元),并且Dart代码会通过AOT编译为平台的原生代码,实现与平台的直接通信,不需要JS引擎的桥接,也不需要原生平台的Dalvik虚拟机,如图1-5所示。同时,Flutter的Widget采用现代响应式框架来构建,而Widget是不可变的,仅支持一帧,并且每一帧上的内容不能直接更新,需要通过Widget的状态来间接更新。在Flutter中,无状态和有状态Widget的核心特性是相同的,视图的每一帧Flutter都会重新构建,通过State对象Flutter就可以跨帧存储状态数据并恢复它。总的来说,Flutter是目前跨平台开发中最好的方案,它以一套代码即可生成Android和iOS平台两种应用,很大程度上减少了App开发和维护的成本,同时Dart语言强大的性能表现和丰富的特性,也使得跨平台开发变得更加便利。而不足的是,Flutter还处于Alpha阶段,许多功能还不是特别完善,而全新的Dart语言也带来了学习上的成本,如果想要完全替代Android和iOS开发还有比较长的路要走。PWAPWA,全称Progressive Web App,是Google在2015年提出渐进式的网页技术。PWA结合了一系列的现代Web技术,并使用多种技术来增强Web App的功能,最终可以让网页应用呈现和原生应用相似的体验。相比于传统的网页技术,渐进式Web技术是可以横跨Web技术及Native APP开发的技术解决方案,具有可靠、快速且可参与等诸多特点。具体来说,当用户从手机主屏幕启动时,不用考虑网络的状态就可以立刻加载出PWA。并且,相比传统的网页加载速度,PWA的加载速度是非常快的,因为PWA使用了Service Worker 等先进技术。除此之外,PWA还可以被添加在用户的主屏幕上,不用从应用商店进行下载即可通过网络应用程序Manifest file提供类似于APP的使用体验。作为一种全新Web技术方案,PWA的正常工作需要一些重要的技术组件,它们协同工作并为传统的Web应用程序注入活力,如图1-8所示。其中,Service Worker表示离线缓存文件,其本质是Web应用程序与浏览器之间的代理服务器,可以在网络可用时作为浏览器和网络间的代理,也可以在离线或者网络极差的环境下使用离线的缓冲文件。Manifest则是W3C一个技术规范,它定义了基于JSON的清单,为开发人员提供一个放置与Web应用程序关联的元数据的集中地点。Manifest是PWA 开发中的重要一环,它为开发人员控制应用程序提供了可能。目前,渐进式Web应用还处于起步阶段,使用的厂商也是诸如Twitter、淘宝、微博等大平台。不过,PWA作为Google主推的一项技术标准,Edge、Safari和FireFox等主流浏览器也都开始支持渐进式Web应用。因此,可以预见的是,PWA必将成为继移动之后的又一革命性技术方案。对比在当前诸多的跨平台方案中,RN、Weex和Flutter无疑是最优秀的。而从不同的细节来看,三大跨平台框架又有各自的优点和缺点,可以通过表1-1来查看。对比类型React NativeWeexFlutter支持平台Android/IOSAndroid/IOS/WebAndroid/IOS实现技术JavaScriptJavaScript原生编码/渲染引擎JS V8JSCoreFlutter Engine编程语言ReactVueDartbundle包大小单一、较大较小、多页面不需要框架程度较重较轻重社区活跃、FB维护不活跃活跃如上表所示,RN、Weex采用的技术方案大体相同,它们都使用JavaScript作为编程语言,然后通过中间层转换为原生的组件后再利用Native渲染引擎执行渲染操作。而Flutter直接使用skia来渲染视图,而Flutter Widget则使用现代响应式框架来构建,和平台没有直接的关系。就目前跨平台技术来看,JavaScript在跨平台开发中可谓占据半壁江山,大有“一统天下”的趋势。从性能方面来说,Flutter的性能理论上是最好的,RN和Weex次之,并且都好于传统的WebView方案。但从目前的实际应用来看却并没有太大的差距,特别是和0.5.0版本以上的RN对比性能体验上差异并不明显。而从社群和社区的活跃来看,RN和Flutter无疑是最活跃的,RN经过4年多的发展已经成长为跨平台开发的实际领导者,并拥有各类丰富的第三方库和开发群体。Flutter作为最近才火起来的跨平台技术方案,不过目前还处在beta阶段,商用的实例也很少,不过应该看到google的号召力一直是很强,未来究竟如何发展让我们拭目以待。示例eros-yanxuan简介eros-yanxuan 是基于 eros 开发的Weex项目,部分页面参考了项目网易严选 weex 版本,欢迎star或fork。eros 文档eros github运行确保你本地已经集成了 eros 开发所需的环境。clone 项目到本地:$ git clone https://github.com/xiangzhihong/eros-yanxuan.git进入目录,下载前端所需的依赖:$ cd eros-yanxuan$ npm installiOS SDK打开platforms目录下的WeexEros项目,在WeexEros中使用pod添加依赖。$ cd platforms/ios/WeexEros$ pod update // 下载 iOS 依赖$ open WeexEros.xcworkspace // 自动打开项目选中模拟器,点击绿色箭头运行 app 即可。Android对于Android工程来说,使用Android Studio打开platforms目录下的WeexFrameworkWrapper的Android工程,然后使用install.sh安装Android工程的需要依赖包nexus和wxframework。具体可以参考自行导入项目,便可运行起来。运行项目根目录下运行 eros dev关闭调试,拦截器,打开热更新重新 build app效果Question运行过程中出现问题在以下地址解决方法,如果没有找到,请加群:515980159eros issueeros Q&A

January 22, 2019 · 1 min · jiezi

mac上搭建flutter开发环境并运行第一个程序

什么是flutter官方是这么解释的:Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。(闲鱼APP就是用的flutter)一、安装flutter#切换到准备安装flutter的目录cd project#有两种方法安装flutter SDK#1、使用git clonegit clone -b beta https://github.com/flutter/flutter.git#2、直接在github下载压缩包,下载地址https://github.com/flutter/flutter/releases#在目录下解压zip文件#配置环境export PATH=pwd/flutter/bin:$PATH #如果下载太慢或者失败,那么需要先配置中国镜像,然后再clone项目export PUB_HOSTED_URL=https://pub.flutter-io.cnexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn#切换到项目目录cd ./flutterflutter doctor不出意外的话,应该会报错,提示你安装android studio、Xcode、install dart和flutter插件等。按照提示逐个安装就行。需要注意的是,你可能会遇见pod setup这个步骤,但是却一直卡着进度条不动,快速的解决办法是,打开手机热点,mac连接手机的热点进行下载安装,5分钟内能够安装好(大小应该在500多M)二、安装android studio及插件android studio 下载地址打开android studio, 打开plugin输入flutter搜索,点击中间的 Search in repositories点击install,顺利的话安装完毕之后重启android studio三、运行第一个项目新建一个Flutter打开android studio后 会看到可选项多了一个 Start a new Flutter project创建成功后在终端中输入open -a Simulator则可以启动ios模拟器,然后在android studio 控制台中输入 flutter run 就能够看到安卓真机和ios模拟器了flutter run -d <设备id>就能够启动对应的平台了如我这里启动ios模拟器就输入flutter run -d B21和运行android项目一样的操作流程,连接安卓真机后在手机上能看到默认的项目

January 21, 2019 · 1 min · jiezi

用前端 最舒服的躺姿 搞定 Flutter (组件篇)

前言要说2018年最火的跨端技术,当属于 Flutter 莫属,应该没人质疑吧。一个新的技术的趋势,最明显的特征,就是它一定想把“前浪”拍死在沙滩上。这个前浪,就是"react Native",“weex”。目前随便在搜索引擎上 搜索"Flutter reactNative",就全是这两个技术的对比,评测。一股股浓浓 : 不服来 “掰” 啊 !!!的味道。是的,错过了react Native, weex 这些 “炸” 翻前端的技术,不能在错过 Flutter 了,这年头,你不会一门,跨端技术,怎么好意思说自己是【前端】。Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界… … 好了, 这些,大家早就知道了,来点实在的!!!话说隔壁师兄,“闲鱼” 是最早一批与谷歌展开合作,并在重要的商品详情页中使用flutter技术上线的BU。一路走来,积累了大量的开发经验。“闲鱼” flutter 相关文章的都很有深度,足见功力。深入理解Flutter引擎线程模式Android Flutter实践内存初探深入理解flutter的编译原理与优化… …都是一篇篇的深度好文,读完收益匪浅,但是对于刚接触 Flutter 的web前端同学来说还是好了,回到标题,笔者作为一名传统 web前端,想从前端最熟悉的视角 “躺” 着把 Flutter 了解一遍,不要敬仰,平视它!!!先从兴趣开始。正文Flutter 环境的搭建,其实有很多资源可以参考。这里就不累述了(知道有很多坑,在后续文章中,有机会把个人遇到的坑汇总一下 )。有兴趣可以参考 flutter安装环境的搭建 , 在这里建议各位,一定要自己亲自搭一下环境,跑一下官方demo, 小马过河,焉知深浅,自己定的位才是最准确的。有了环境,先配置编辑器。然后 创建一个Flutter项目。项目创建完成,可以先用 flutter run 跑一下。flutter run 好了,跑起来了吧,你会看到一个计数的官方示例,点击加号图片可以做加运算。这时候我们看项目的project 目录里 有一个入口文件叫 main.dart。然后打开 main.dart 就像下面这样:( 为什么你们看到代码比我的长,因为我折叠了!!! ) 这不是重点,重点是每个类继承的都是一个尾号为Widget 的字符。聪明的你一定会觉得 Widget 和 Flutter 有着某种神秘的联系。“Binggo!",是的,Flutter 有两个重型武器,一个叫 Dart ,另一个就是 Widget 了。Dart 一切皆来自 Object, Flutter 的组件皆来自 Widget。关于强大的Dart,今天暂且不表,后面有时间可以独立篇幅来聊聊Dart。先祭出一张 Flutter 的架构老图。在 Flutter 的世界里,包括views,view controllers,layouts等在内的概念都建立在Widget之上。Widget 是 Flutter 组件的抽象描述。所以掌握Flutter的基础就是学会使用 Widget开始。在Flutter界面渲染过程分为三个阶段:布局、绘制、合成,布局和绘制在Flutter框架中完成,合成则交由引擎负责:Flutter 通过组合、嵌套不同类型的控件,就可以构建出任意功能、任意复杂度的界面。它包含的最主要的几个类有:class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding,PaintingBinding, RendererBinding, WidgetsBinding { … }abstract class Widget extends DiagnosticableTree { … }abstract class StatelessWidget extends Widget { … }abstract class StatefulWidget extends Widget { … }abstract class RenderObjectWidget extends Widget { … }abstract class Element extends DiagnosticableTree implements BuildContext { … }abstract class RenderObjectElement extends Element { … }class StatelessElement extends ComponentElement { … }class StatefulElement extends ComponentElement { … }上面这些类的主要作用如下:基于Flutter控件系统开发的程序都需要使用WidgetsFlutterBinding,它是Flutter的控件框架和Flutter引擎的胶水层。Widget就是所有控件的基类,它本身所有的属性都是只读的。RenderObjectWidget所有的实现类则负责提供配置信息并创建具体的RenderObjectElement。Element是Flutter用来分离控件树和真正的渲染对象的中间层,控件用来描述对应的element属性,控件重建后可能会复用同一个element。RenderObjectElement持有真正负责布局、绘制和碰撞测试(hit test)的RenderObject对象。StatelessWidget和StatefulWidget并不会直接影响RenderObject创建,只负责创建对应的RenderObjectWidgetStatelessElement和StatefulElement也是类似的功能。很复杂是吧,先不用管,简单表述Widget是这样的:Widget = 样式(css) + 标记语义(标签) + 组件化(官方) + 数据绑定(props)“什么?这不就是 react 吗?“对, React 的概念和 Flutter 的 Widget 是有相通性的。“既然有react的概念,难道还有state,setState吗?“又对, Flutter 还真有 类似 state 状态机制的概念,而且也确实有 setState 的方法。“dome里有两个基类,StatelessWidget 和 StatefulWidget 是做什么用的?”StatelessWidget 和 StatefulWidget,这里两个类特别重要,几乎所有的组件都是基于他们创建的。StatelessWidget 是状态不可变的widget,称为 无状态widget。初始状态设置以后就不可再变化。如果需要变化需要重新创建。StatefulWidget 可以保存自己的状态,称为 有状态widget。Flutter 首先保存了初始化时创建的State,状态是通过改变State,来重新构建 Widget 树来进行UI变化。改变状态的方法,就是我们用的最多的神器"setState”,而单纯改变数据是不会引发UI改变的,这个概念和我们的 React 一样一样的。如果你是初次接触 Flutter 可以不用记忆这么多组件基类,只用记住以下式子就可以, 不夸张的说,熟悉这个式子就可以开发 Flutter 项目了:拆解围绕着widget的构成,我们来拆解分析一下,标记语义,样式,组件化。标记语义为什么不称为 “模版”,“标签”,“element" ,而叫"标记语义",是因为flutter的 widget 结构并不只是 “模版”,“标签”,“element"。widget 描述结构更像是 React 的虚拟dom阶段,浓缩了相关上下文,以对象化的结构展示。我们先来看看,React 创建出来的虚拟dom结构( 伪代码 ):var newTree = el(‘div’, {‘id’: ‘container’}, [ el(‘h1’, {style: ‘color: red’}, [‘simple virtal dom’]), el(‘p’, [‘Hello, virtual-dom’]), el(‘ul’, [el(’li’), el(’li’)])])再来看看,flutter 用Dart 创建的 widget 代码结构:Widget build(BuildContext context) { return new Column( children: <Widget>[ new Container( padding: new EdgeInsets.only(top:100.0), child: new Text(‘这是一个组件’) ), new Container( decoration: new BoxDecoration(border: new Border.all(width:1.0,color: Colors.blue)), padding: new EdgeInsets.all(20.0), child: new Text(‘来自输入框:’+active) ) ], ); }是不是这样看就熟悉很多了。注:很多前端er 会不习惯这中书写方式,目前有开发者在社区推动,在编译前,使用jsx标签,编译后再解析成标记树,比如这个DSX设计的提案。虽然jsx->标记树 还只是提案,但其实可以帮助我们更容易理解,此提案想表达的样式像这样:class MyScaffold extends StatelessWidget { build(context) { return <Material> <Column> <MyAppBar title={<Text text=‘Example title’ style={Theme.of(context).primaryTextTheme.title}, />} /> <Expanded> <Center> <Text text=‘Hello, world!’/> </Center> </Expanded> </Column> </Material>; }}上面这段 jsx 要是用 Dart 来写是什么样的?如下:class MyScaffold extends StatelessWidget { @override Widget build(BuildContext context) { return Material( child: Column( children: <Widget>[ MyAppBar( title: Text( ‘Example title’, style: Theme.of(context).primaryTextTheme.title, ), // Text ), // MyAppBar Expanded( child: Center( child: Text(‘Hello, world!’), ), // Center ), // Expanded ], // <Widget>[] ), // Column ); // Material }}虽然 Flutter 与 react 这么类比多少有些牵强,但可以个人总结一些方法,方便理解:开头大写的类名 相当于 jsx 的标签名child 相当于 jsx 的 子标签+布局children 相当于 jsx 的 群组子标签(和child还是有区别的)其他属性相当于 jsx 的 props大家有没有注意, 第二段dart 语法结尾都会带上 “ // ” 注释符号,这个是编辑器IDE在识别是 Flutter 项目后,自动追加上去的,像 jsx 语言的标签封闭,方便发现标注的起始节点。样式对于 Flutter 样式的理解,可以查看官方的这篇文档,也是同样用类比的方式,很直观的了解,HTML、css样式和 flutter 之间的联系.可以参考这里:https://flutter.io/docs/get-s…https://flutterchina.club/web…笔者摘选其中样例的重点部分,对比展示来说明( 由于篇幅问题, 父子关系css 结构,用tab方式来表示 ):文本样式:.demo1 { background-color: #e0e0e0; width: 320px; height: 240px; font: 900 24px Georgia; letter-spacing: 4px; text-transform: uppercase; }var demo1 = new Container( child: new Text( “Lorem ipsum”.toUpperCase(), // 对应 左边的文本转换大小写 style: new TextStyle( // 对应 左边的 font fontSize: 24.0 fontWeight: FontWeight.w900, fontFamily: “Georgia”, letterSpacing: 4.0, ), ), width: 320.0, // 对应 左边的 width height: 240.0, // 对应 左边的 height color: Colors.grey[300], // 对应 左边的 background-color);样式居中:.demo2 { display: flex; align-items: center; justify-content: center; }var demo2 = new Container( child: new Center( // 对应左边的整个 flex 属性 child: new Text(“Lorem ipsum”) ) ););设置最大(小)宽度: .container{ width:300px .demo3 { width: 100%; max-width: 240px; } }// 对于嵌套容器,如果父级的宽度小于子级宽度,则子级容器将自行调整大小以匹配父级。var container = new Container( child: new Center( child: new Text(“Lorem ipsum”), decoration: new BoxDecoration( … ), // constraints属性,创建一个新的BoxConstraints来设置minWidth或maxWidth width: 240.0, // 对应左边的 max-width ), width:300.0 ),旋转组件: .box { transform: rotate(15deg); }var container = new Container( // gray box child: new Transform( // 对应左边的 transform child: new Container( … ), alignment: Alignment.center, // 对应左边的 transform transform: new Matrix4.identity() // 对应左边的 transform ..rotateZ(15 * 3.1415927 / 180), ), ));缩放组件:.box { transform: scale(1.5); }var container = new Container( // gray box child: new Transform( // 对应左边的 transform child: new Container( … ), alignment: Alignment.center, // 对应左边的 transform的中心 transform: new Matrix4.identity() // 对应左边的 transform ..scale(1.5), ), ));设置绝对位置和相对位置:.greybox { position: relative; .redbox { position: absolute; top: 24px; left: 24px; }}var container = new Container( // grey box child: new Stack( // 相对跟容器位置 relative children: [ new Positioned( // 相对父容器位置 absolute child: new Container( … ), left: 24.0, top: 24.0, )], ));颜色渐变:.redbox { background: linear-gradient(180deg, #ef5350, rgba(0, 0, 0, 0) 80%); }var container = new Container( // grey box child: new Center( child: new Container( // red box child: new Text( … ), decoration: new BoxDecoration( gradient: new LinearGradient( // 对应左边的 background: linear-gradient begin: const Alignment(0.0, -1.0), end: const Alignment(0.0, 0.6), colors: <Color>[ const Color(0xffef5350), const Color(0x00ef5350) ], ), ) ), ));圆角:.box { border-radius: 8px; }var container = new Container( // grey box child: new Center( child: new Container( // red circle child: new Text( … ), decoration: new BoxDecoration( borderRadius: new BorderRadius.all( const Radius.circular(8.0), ), ) ), ));阴影:.box { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.8), 0 6px 20px rgba(0, 0, 0, 0.5);}var container = new Container( // grey box child: new Center( child: new Container( // red box child: new Text( … ), decoration: new BoxDecoration( boxShadow: <BoxShadow>[ // 对应左边的 box-shadow new BoxShadow ( color: const Color(0xcc000000), offset: new Offset(0.0, 2.0), blurRadius: 4.0, ), new BoxShadow ( color: const Color(0x80000000), offset: new Offset(0.0, 6.0), blurRadius: 20.0, ), ], ) ), ));画圆:.circle { text-align: center; width: 160px; height: 160px; border-radius: 50%; }var container = new Container( // grey box child: new Center( child: new Container( // red circle child: new Text( … ), decoration: new BoxDecoration( color: Colors.red[400], shape: BoxShape.circle, // 画圆和圆角不太一样,用的是BoxShape绘制图像能力 ), width: 160.0, height: 160.0, ), ));内联样式:// css 的内联结构.greybox { font: 900 24px Roboto; .redbox { em { font: 300 48px Roboto; font-style: italic; } } }var container = new Container( // grey box child: new Center( child: new Container( // red box child: new RichText( text: new TextSpan( style: bold24Roboto, children: <TextSpan>[ new TextSpan(text: “Lorem “), // 继承内联样式 new TextSpan( text: “ipsum”, style: new TextStyle( // 具有自定义样式的单独样式 fontWeight: FontWeight.w300, fontStyle: FontStyle.italic, fontSize: 48.0, ), ), ], ), ), ), ));组件化官方的widgets目录点击每一个card后,里面还有子card, 以一个展开的纬度来看,是这样的:这真是一个庞大的组件系统,这不是社区提供的,而是官方的。flutter 团队事无巨细的实现了目前市面上基本上能见到的组件方式和类型。个人认为这样做优缺点并存的。先说缺点:1.学习成本增加,曲线也还是比较陡峭的。2.组件直接的继承关系路径比较零乱,比如 继承自 Widget 是这些大类道还清晰:PreferredSizeWidget ProxyWidget RenderObjectWidget StatefulWidget StatelessWidget。但是,StatelessWidget和StatefulWidget的子类就过于平行化了,名称上晦涩,没有抽象架构化,分层或者塔型级别。StatelessWidget:StatefulWidget3.对自定义组件定义模糊,有这么庞大的组件库,到底以后是有个一个更系统的第三方组件库去替换它,还是说 Flutter 官方就不建议使用第三方组件,这个也未有定论。再说优点:1.可以阻止以后轮子泛滥,在团队僵持不下使用哪个轮子库时,最好的理由就是“官方”二字,因为使用官方,可以弱化和规避一些问题,比如: 版本迭代不同步,性能瓶颈,规范不统一等问题,也能快速支持官方的辅助工具。2.正是由于官方的标准划一,为自动化编译,自动化搭建,测试调优,可视化带来便利,甚至为Ai前端模型化业务场景,提供支撑,都说前端的组件像搭乐高,Flutter就是颗粒标准统一的乐高。3.天下之势,分久必合,合久必分。前端在经历了 flash 一统pc页面的富媒体时代,后被乔布斯和H5瓦解;之后又有H5 的繁盛和框架、语法、构建模式的乱战;再到有“别再更新,老子学不动"的呼声下,希望有个一统江湖的跨平台系统出现;让开发者更专注于,提高业务内容,创造 “新, 酷,炫” 的展现形势上下功夫。也许,真有一个前端江湖的王者的诞生。写在最后实际的 Flutter Widget 要复杂的更多的多,在眼花缭乱的 Widget 组件中,笔者想用自己的一些理解,去粗取精,来逐步理解 Flutter 这个新家伙 ,文中有理解不到位的地方,欢迎大家指正。笔者团队也正在开发一套《Flutter GO》的APP,帮助大家熟悉复杂的 Flutter Widget。原文链接更多学习 Flutter的小伙伴,欢迎入QQ群 Flutter Go :679476515《Flutter GO》项目地址 alibaba/flutter-go ...

January 19, 2019 · 5 min · jiezi

Flutter 2019 产品路线图正式公布

2019Flutter 1.0 的发布对我们来说是一个很重要的起点,长路漫漫,我们仍有很多工作要做。这里我们向大家公开我们的产品路线图(Roadmap)规划,一方面是保持开源项目的透明度,另一方面,开发者们也可以根据我们的工作优先级来制定更适合的工程方案。以下几点我们今年会着重关注:核心和基础易用性生态系统移动端之外的支持动态更新工具链我们的计划会根据大家的反馈以及新的市场变化来做调整,这份路线图里的内容不尽然是我们一定会完成的工作。如果你有任何反馈,我们鼓励你通过 Issuse,或者在我们的邮件群组等与我们保持联系。Flutter 是一个开源项目,我们鼓励你参与到我们当中来。版本发布使用 Flutter 的开发者们可以选择一个「频道」来「接收」我们的版本更新和变化,我们目前有四个频道:master、dev、beta 和 stable,质量和稳定性从前向后依次递增,发布速度当然也会是依次相对放缓。我们计划每个月发布一个 beta 频道的版本,这个发布通常会是在月初,全年会在 stable 频道发布四个较大的「正式」版本。在生产环境里,我们建议开发者们使用 stable 频发布的 Flutter 版本。如果你想了解更多关于我们的版本发布流程,可以查看 发布流程 这篇 Wiki。关注领域核心和基础我们的首要任务依然是为 Flutter 现有的核心和基础添砖加瓦:修复 Bug:Bug 修复的优先级主要是基于 Issue 下的互动数量,比如 GitHub 自带的一些针对 Issue 的表情互动,点赞等;性能调优:包括减少内存、引擎占用空间(包大小),提高帧率等。如果开发者们有特别的性能基准要求,可以通过 devicelab 测试数据给我们看一下;改进 Flutter 测试流程:以确保为开发者们提供稳定的版本构建不会出现版本回归;改进错误消息提醒:通过 Google 用户研究(User Research)团队的工作,使错误提醒更具备可操作性以及包含一些常见的解决方案;API 文档改进:特别是提供示例代码和图表等,让我们的 API 文档更易用。易用性为新晋使用 Flutter 的开发者清扫绊脚石,如:完善和满足希望使用混合工程(将 Flutter 集成到于现有的 Native 工程项目)的开发者们的需求,如提供新的插件模板和 Android 内嵌 API;更新 Flutter 官方文档以提供更详尽的文档和使用教程;在 Flutter 应用里管理 state 的最佳实践;更好的帮助 iOS 开发者:投入时间持续更新和维护我们的 Cupertino widgets;在非完整工具链和运行环境下更容易体验和使用 Flutter。生态系统在 Flutter 中生态系统意味着使用 Flutter 的开发者们可以便捷地完成任何他们想做的事情,甚至在 Flutter 框架不提供提供开箱即用支持的情况下也如此。我们花费了大量的精力在工具和基础设施建设的工作上,以支持围绕着核心 Flutter 技术而蓬勃发展的生态系统。Google 也会投入时间开发插件和工具来贡献这个生态。2019 年我们会特别关注的生态系统建设工作:更好的 C/C++ 库支持,包括从 Dart 到 C 或 C++ 之间的相互调用;推进官方开发 / 维护的 Packages(调用原生系统的插件和纯 Dart Package)达到与核心框架代码相同的质量和完整性;在 iOS 和 Android 上完成地图和 WebView 插件的开发;确保 Flutter 应用可以使用一些谷歌服务,比如应用内支付和 YouTube;提供本地推送通知和本地数据存储的支持。移动端之外的支持我们将继续把 Flutter 拓展到更多形态的终端,以实现我们的目标:构建一个便携 UI 工具包,在任何需要的地方画出每一帧像素。更好的支持键盘和鼠标的输入;完善可以让 Flutter 可以运行在 Web 平台的 Hummingbird 项目;继续尝试让 Flutter 运行在桌面级的平台之上(如 macOS 和 Windows)。动态更新Dart 语言平台为 Flutter 应用开发提供了热重载(Hot Reload)的特性,让开发者们无需重新部署就可以把代码推送到应用中去。Android 上的动态修复:让开发者直接将代码更新从服务器推送到 Android 应用里;动态载入:让应用里不常用的部分延迟加载。工具链继续投入精力支持 Visual Studio Code,Android Studio 和 IntelliJ,使它们能够作为开发 Flutter 的主力 IDE;增加对 Language Server Protocol 以及其他开放协议的支持;通过改进开发过程中的分析、调试体验,让开发者更简单地提高应用的整体质量和性能;持续提升模版的体验,让 Flutter 的上手开发既快又简单。里程碑及计划时间如果你对我们每个月将会发布什么感兴趣的话,你可以我们 GitHub 上的 milestones 页面查看。计划赶不上变化,我们的里程碑可能会因为某些 Issue 而被改变,所以我们不能保证每个里程碑的确定完成时间。欢迎对本文作出反馈。文/ Flutter 社区:(微信 ID:flutter-io)原始 Wiki 地址 https://github.com/flutter/fl… ...

January 18, 2019 · 1 min · jiezi

2亿用户背后的Flutter应用框架Fish Redux

背景在闲鱼深度使用 Flutter 开发过程中,我们遇到了业务代码耦合严重,代码可维护性糟糕,如入泥泞。对于闲鱼这样的负责业务场景,我们需要一个统一的应用框架来摆脱当下的开发困境,而这也是 Flutter 领域空缺的一块处女地。Fish Redux 是为解决上面问题上层应用框架,它是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用。它的最大特点是配置式组装, 一方面将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现,另一方面将 Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数。所以它会非常干净,易编写、易维护、易协作。Fish Redux 的灵感主要来自于 Redux、React、Elm、Dva 这样的优秀框架,而 Fish Redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。分层架构图架构图,主体自底而上,分三层,每一层用来解决不通层面的问题和矛盾,下面依次来展开。ReduxRedux 是来自前端社区的一个数据管理框架, 对 Native 开发同学来说可能会有一点陌生,我们做一个简单的介绍。Redux 做什么的?Redux 是一个用来做可预测易调试的数据管理的框架。所有对数据的增删改查等操作都由 Redux 来集中负责。Redux 是怎么设计和实现的?Redux 是一个函数式的数据管理的框架。传统 OOP 做数据管理,往往是定义一些 Bean,每一个 Bean 对外暴露一些 Public-API 用来操作内部数据(充血模型)。函数式的做法是更上一个抽象的纬度,对数据的定义是一些 Struct(贫血模型),而操作数据的方法都统一到具有相同函数签名 (T, Action) => T 的 Reducer 中。FP:Struct(贫血模型) + Reducer = OOP:Bean(充血模型)同时 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 机制,给框架带了极高的灵活性和扩展性。贫血模型、充血模型 参考:https://en.wikipedia.org/wiki/Plain_old_Java_objectRedux 的缺点Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点。在我们实际使用 Redux 中面临两个具体问题Redux 的集中和 Component 的分治之间的矛盾。Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。Fish Redux 的改良Fish Redux 通过 Redux 做集中化的可观察的数据管理。然不仅于此,对于传统 Redux 在使用层面上的缺点,在面向端侧 flutter 页面纬度开发的场景中,我们通过更好更高的抽象,做了改良。一个组件需要定义一个数据(Struct)和一个 Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,我们解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。我们得到了理想的集中的效果和分治的代码。对社区标准的 followState、Action、Reducer、Store、Middleware 以上概念和社区的 ReduxJS 是完全一致的。我们将原汁原味地保留所有的 Redux 的优势。如果想对 Redux 有更近一步的理解,请参考 https://github.com/reduxjs/reduxComponent组件是对局部的展示和功能的封装。 基于 Redux 的原则,我们对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用 Effect)。于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的行为。这是一种面向当下,也面向未来的拆分。在面向当下的 Redux 看来,是数据管理和其他。在面向未来的 UI-Automation 看来是 UI 表达和其他。UI 的表达对程序员而言即将进入黑盒时代,研发工程师们会把更多的精力放在非修改数据的行为、修改数据的行为上。组件是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。关于 ViewView 仅仅是一个函数签名: (T,Dispatch,ViewService) => Widget它主要包含三方面的信息视图是完全由数据驱动。视图产生的事件/回调,通过 Dispatch 发出“意图”,不做具体的实现。需要用到的组件依赖等,通过 ViewService 标准化调用。比如一个典型的符合 View 签名的函数关于 EffectEffect 是对非修改数据行为的标准定义,它是一个函数签名: (Context, Action) => Object它主要包含四方面的信息接收来自 View 的“意图”,也包括对应的生命周期的回调,然后做出具体的执行。它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们不崇尚持有数据,而通过上下文来获取最新数据。它不修改数据, 如果修要,应该发一个 Action 到 Reducer 里去处理。它的返回值仅限于 bool or Future, 对应支持同步函数和协程的处理流程。比如:良好的协程的支持关于 ReducerReducer 是一个完全符合 Redux 规范的函数签名:(T,Action) => T一些符合签名的 Reducer同时我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。所以有这样的公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。一个典型的组装通过 Component 的抽象,我们得到了完整的分治,多纬度的复用,更好的解耦。AdapterAdapter 也是对局部的展示和功能的封装。它为 ListView 高性能场景而生,它是 Component 实现上的一种变化。它的目标是解决 Component 模型在 flutter-ListView 的场景下的 3 个问题1)将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化。2)Component 无法区分 appear|disappear 和 init|dispose 。3)Effect 的生命周期和 View 的耦合,在 ListView 的场景下不符合直观的预期。概括的讲,我们想要一个逻辑上的 ScrollView,性能上的 ListView ,这样的一种局部展示和功能封装的抽象。做出这样独立一层的抽象是,我们看实际的效果, 我们对页面不使用框架,使用框架 Component,使用框架 Component+Adapter 的性能基线对比Reducer is long-lived, Effect is medium-lived, View is short-lived.我们通过不断的测试做对比,以某 android 机为例:使用框架前 我们的详情页面的 FPS,基线在 52FPS。使用框架, 仅使用 Component 抽象下,FPS 下降到 40, 遭遇“Big-Cell”的陷阱。使用框架,同时使用 Adapter 抽象后,FPS 提升到 53,回到基线以上,有小幅度的提升。Directory推荐的目录结构会是这样sample_page– action.dart– page.dart– view.dart– effect.dart– reducer.dart– state.dartcomponentssample_component– action.dart– component.dart– view.dart– effect.dart– reducer.dart– state.dart上层负责组装,下层负责实现, 同时会有一个插件提供, 便于我们快速填写。以闲鱼的详情场景为例的组装:组件和组件之间,组件和容器之间都完全的独立。Communication Mechanism组件|适配器内通信组件|适配器间内通信简单的描述:采用的是带有一段优先处理的广播, self-first-broadcast。发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的所有的通信诉求。Refresh Mechanism数据刷新局部数据修改,自动层层触发上层数据的浅拷贝,对上层业务代码是透明的。层层的数据的拷贝一方面是对 Redux 数据修改的严格的 follow。另一方面也是对数据驱动展示的严格的 follow。视图刷新扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新优点数据的集中管理通过 Redux 做集中化的可观察的数据管理。我们将原汁原味地保留所有的 Redux 的优势,同时在 Reducer 的合并上,变成由框架代理自动完成,大大简化了使用 Redux 的繁琐度。组件的分治管理组件既是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。View、Reducer、Effect 隔离将组件拆分成三个无状态的互不依赖的函数。因为是无状态的函数,它更易于编写、调试、测试、维护。同时它带来了更多的组合、复用和创新的可能。声明式配置组装组件、适配器通过自由的声明式配置组装来完成。包括它的 View、Reducer、Effect 以及它所依赖的子项。良好的扩展性核心框架保持自己的核心的三层关注点,不做核心关注点以外的事情,同时对上层保持了灵活的扩展性。框架甚至没有任何的一行的打印的代码,但我们可通过标准的 Middleware 来观察到数据的流动,组件的变化。在框架的核心三层外,也可以通过 dart 的语言特性 为 Component 或者 Adapter 添加 mixin,来灵活的组合式地增强他们的上层使用上的定制和能力。框架和其他中间件的打通,诸如自动曝光、高可用等,各中间件和框架之间都是透明的,由上层自由组装。精小、简单、完备它非常小,仅仅包含 1000 多行代码。它使用简单,完成几个小的函数,完成组装,即可运行。它是完备的。Fish Redux 目前已在阿里巴巴闲鱼技术团队内多场景,深入应用。本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 18, 2019 · 2 min · jiezi

【学习】Flutter: 在flutter_console.bat中运行sdkmanager --update报错的解决方案

配置Flutter开发环境时,运行sdkmanager –update报错在执行flutter doctor后,提示Android license status unknown。google后知,可以运行flutter doctor –android-licenses,会给出具体的解决方法。结果如下:A newer version of the Android SDK is required. To update, run: AndroidSDK/tools/bin/sdkmanager –update执行sdkmanager –update,报错:Exception in thread “main” java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema继续google后,发现是因为JDK版本过高,安装jdk1.8后问题解决。

January 17, 2019 · 1 min · jiezi

flutter常见组件API(持续更新)

ListTile效果如上: new ListTile( // 前缀 leading:Icon(Icons.navigation), // 标题 title: Text(“导航栏组件”,style:TextStyle(fontWeight:FontWeight.w500)), // 副标题 subtitle: Text(“常见的底部导航栏组件”), // 后缀 trailing: Icon(Icons.chevron_right), // 点击事件 onTap: (){ Navigator.push( context, MaterialPageRoute( builder:(context)=>new BottomNavigation() ) ); }, // 长按事件 onLongPress: (){ print(“object”); } )

January 17, 2019 · 1 min · jiezi

Flutter部件Widget和BuildContext上下文环境的关系

问题关于路由跳转页面遇到一个跳转失败的问题,log日志报“Navigator operation requested with a context that does not include a Navigator.”代码如下:解决方案把Scaffold段代码抽取出来:探究BuildContext上下文对象是整个APP Widget树结构中的Widget话柄,每个Wideget对应的都有属于自己的BuildContext。BuildContext还提供了一组方法,这些方法能够在StatelessWidget.build 函数中被当前的上下文环境调用。比如 Navigator.pushNamed(context, ‘/’);当部件Widget在StatelessWidget.build函数被返回时,这个部件会成为父部件。所以这意味着StatelessWidget.build方法中的context和函数内部部件Widegt的context不是同一个上下文。所以它们两个不同上下文能够调用的方法是有区别的。这就是这个问题关键的所在。回顾问题结尾如有错误的地方欢迎指出,交流进步。参考文献:https://docs.flutter.io/flutt…

January 17, 2019 · 1 min · jiezi

Flutter 状态管理之 Scoped Model & Redux

前言文章原文地址:Nealyang/PersonalBlog可能作为一个前端,在学习 Flutter 的过程中,总感觉非常非常相似 React Native,甚至于,其中还是有state的概念 setState,所以在 Flutter 中,也当然会存在非常多的解决方案,比如 redux 、RxDart 还有 Scoped Model等解决方案。今天,我们主要介绍下常用的两种 State 管理解决方案:redux、scoped model。Scoped Model介绍Scoped Model 是 package 上 Dart 的一个第三方库scoped_model。Scoped Model 主要是通过数据model的概念来实现数据传递,表现上类似于 react 中 context 的概念。它提供了让子代widget轻松获取父级数据model的功能。从官网中的介绍可以了解到,它直接来自于Google正在开发的新系统Fuchsia核心 Widgets 中对 Model 类的简单提取,作为独立使用的独立 Flutter 插件发布。在直接上手之前,我们先着重说一下 Scoped Model 中几个重要的概念Model 类,通过继承 Model 类来创建自己的数据 model,例如 SearchModel 或者 UserModel ,并且还可以监听 数据model的变化ScopedModelDescendant widget , 如果你需要传递数据 model 到很深层级里面的 widget ,那么你就需要用 ScopedModel 来包裹 Model,这样的话,后面所有的子widget 都可以使用该数据 model 了(是不是更有一种 context 的感觉)ScopedModelDescendant widget ,使用此 widget 可以在 widget tree 中找到相应的 Scope的Model ,当 数据 model 发生变化的时候,该 widget 会重新构建当然,在 Scoped Model 的文档中,也介绍了一些 实现原理Model类实现了Listenable接口AnimationController和TextEditingController也是Listenables使用InheritedWidget将数据 model 传递到Widget树。 重建 InheritedWidget 时,它将手动重建依赖于其数据的所有Widgets。 无需管理订阅!它使用 AnimatedBuilder Widget来监听Model并在模型更改时重建InheritedWidget实操Demodemo地址从gif上可以看到咱们的需求非常的简单,就是在当前页面更新了count后,在第二个页面也能够传递过去。当然,new ResultPage(count:count)就没意思啦~ 咱不讨论哈新建数据 modellib/model/counter_model.dart import ‘package:scoped_model/scoped_model.dart’; class CounterModel extends Model{ int _counter = 0; int get counter => _counter; void increment(){ _counter++; // 通知所有的 listener notifyListeners(); } }这一步非常的简单,新建一个类去继承 Model里面定义了一个 get方法,以便于后面取数据model定义了 increment 方法,去改变我们的数据 model ,调用 package 中的 通知方法 notifyListenerslib/main.dart import ‘package:flutter/material.dart’; import ‘./model/counter_model.dart’; import ‘package:scoped_model/scoped_model.dart’; import ‘./count_page.dart’; void main() { runApp(MyApp( model: CounterModel(), )); } class MyApp extends StatelessWidget { final CounterModel model; const MyApp({Key key,@required this.model}):super(key:key); @override Widget build(BuildContext context) { return ScopedModel( model: model, child: MaterialApp( title: ‘Scoped Model Demo’, home:CountPage(), ), ); } }这是 app 的入口文件,划重点MyApp 类 中,我们传入一个定义好的数据 model ,方便后面传递给子类将 MaterialApp 用 ScopedModel 包裹一下,作用上面已经介绍了,方便子类可以拿到 ,类似于 redux 中 Provider 包裹一下一定需要将数据 model 传递给 ScopedModel 的 model 属性中lib/count_page.dart class CountPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(‘Scoped Model’), actions: <Widget>[ IconButton( tooltip: ’to result’, icon: Icon(Icons.home), onPressed: (){ Navigator.push(context,MaterialPageRoute(builder: (context)=>ResultPage())); }, ) ], ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text(‘你都点击’), ScopedModelDescendant<CounterModel>( builder: (context, child, model) { return Text( ‘${model.counter.toString()} 次了’, style: TextStyle( color: Colors.red, fontSize: 33.0, ), ); }, ) ], ), ), floatingActionButton: ScopedModelDescendant<CounterModel>( builder: (context,child,model){ return FloatingActionButton( onPressed: model.increment, tooltip: ‘add’, child: Icon(Icons.add), ); }, ), ); } }常规布局和widget这里不再重复介绍,我们说下主角:Scoped Model简单一句,哪里需要用数据 model ,哪里就需要用 ScopedModelDescendantScopedModelDescendant中的build方法需要返回一个widget,在这个widget中我们可以使用数据 model中的方法、数据等最后在 lib/result_page.dart中就可以看到我们数据 model 中的 count 值了,注意这里跳转页面,我们并没有通过参数传递的形式传递 Navigator.push(context,MaterialPageRoute(builder: (context)=>ResultPage()));完整项目代码:flutter_scoped_modelflutter_redux相信作为一个前端对于 redux 一定不会陌生,而 Flutter 中也同样存在 state 的概念,其实说白了,UI 只是数据(state)的另一种展现形式。study-redux是笔者之前学习redux时候的一些笔记和心得。这里为了防止有新人不太清楚redux,我们再来介绍下redux的一些基本概念statestate 我们可以理解为前端UI的状态(数据)库,它存储着这个应用所有需要的数据。 action既然这些state已经有了,那么我们是如何实现管理这些state中的数据的呢,当然,这里就要说到action了。 什么是action?E:action:动作。 是的,就是这么简单。。。只有当某一个动作发生的时候才能够触发这个state去改变,那么,触发state变化的原因那么多,比如这里的我们的点击事件,还有网络请求,页面进入,鼠标移入。。。所以action的出现,就是为了把这些操作所产生或者改变的数据从应用传到store中的有效载荷。 需要说明的是,action是state的唯一信号来源。reducerreducer决定了state的最终格式。 reducer是一个纯函数,也就是说,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。reducer对传入的action进行判断,然后返回一个通过判断后的state,这就是reducer的全部职责。 从代码可以简单地看出: import {INCREMENT_COUNTER,DECREMENT_COUNTER} from ‘../actions’; export default function counter(state = 0,action) { switch (action.type){ case INCREMENT_COUNTER: return state+1; case DECREMENT_COUNTER: return state-1; default: return state; } }对于一个比较大一点的应用来说,我们是需要将reducer拆分的,最后通过redux提供的combineReducers方法组合到一起。 比如: const rootReducer = combineReducers({ counter }); export default rootReducer;这里你要明白:每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。 combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理, 然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。storestore是对之前说到一个联系和管理。具有如下职责维持应用的 state;提供 getState() 方法获取 state提供 dispatch(action) 方法更新 state;通过 subscribe(listener) 注册监听器;通过 subscribe(listener) 返回的函数注销监听器。再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。 store的创建通过redux的createStore方法创建,这个方法还需要传入reducer,很容易理解:毕竟我需要dispatch一个action来改变state嘛。 应用一般会有一个初始化的state,所以可选为第二个参数,这个参数通常是有服务端提供的,传说中的Universal渲染。后面会说。。。 第三个参数一般是需要使用的中间件,通过applyMiddleware传入。说了这么多,action,store,action creator,reducer关系就是这么如下的简单明了: 结合 flutter_redux一些工具集让你轻松地使用 redux 来轻松构建 Flutter widget,版本要求是 redux.dart 3.0.0+Redux WidgetsStoreProvider :基础组件,它将给定的 Redux Store 传递给所欲请求它的的子代组件StoreBuilder : 一个子代组件,它从 StoreProvider 获取 Store 并将其传递给 widget 的 builder 方法中StoreConnector :获取 Store 的一个子代组件StoreProvider ancestor,使用给定的 converter 函数将 Store 转换为 ViewModel ,并将ViewModel传递给 builder。 只要 Store 发出更改事件(action),Widget就会自动重建。 无需管理订阅!注意Dart 2需要更严格的类型!1、确认你正使用的是 redux 3.0.0+2、在你的组件树中,将 new StoreProvider(…) 改为 new StoreProvider<StateClass>(…)3、如果需要从StoreProvider<AppState> 中直接获取 Store<AppState> ,则需要将 new StoreProvider.of(context) 改为 StoreProvider.of<StateClass> .不需要直接访问 Store 中的字段,因为Dart2可以使用静态函数推断出正确的类型实操演练官方demo的代码先大概解释一下 import ‘package:flutter/material.dart’; import ‘package:flutter_redux/flutter_redux.dart’; import ‘package:redux/redux.dart’; //定义一个action: Increment enum Actions { Increment } // 定义一个 reducer,响应传进来的 action int counterReducer(int state, dynamic action) { if (action == Actions.Increment) { return state + 1; } return state; } void main() { // 在 基础 widget 中创建一个 store,用final关键字修饰 这比直接在build方法中创建要好很多 final store = new Store<int>(counterReducer, initialState: 0); runApp(new FlutterReduxApp( title: ‘Flutter Redux Demo’, store: store, )); } class FlutterReduxApp extends StatelessWidget { final Store<int> store; final String title; FlutterReduxApp({Key key, this.store, this.title}) : super(key: key); @override Widget build(BuildContext context) { // 用 StoreProvider 来包裹你的 MaterialApp 或者别的 widget ,这样能够确保下面所有的widget能够获取到store中的数据 return new StoreProvider<int>( // 将 store 传递给 StoreProvider // Widgets 将使用 store 变量来使用它 store: store, child: new MaterialApp( theme: new ThemeData.dark(), title: title, home: new Scaffold( appBar: new AppBar( title: new Text(title), ), body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: [ new Text( ‘You have pushed the button this many times:’, ), // 通过 StoreConnector 将 store 和 Text 连接起来,以便于 Text直接render // store 中的值。类似于 react-redux 中的connect // // 将 Text widget 包裹在 StoreConnector 中, // StoreConnector将会在最近的一个祖先元素中找到 StoreProvider // 拿到对应的值,然后传递给build函数 // // 每次点击按钮的时候,将会 dispatch 一个 action并且被reducer所接受。 // 等reducer处理得出最新结果后, widget将会自动重建 new StoreConnector<int, String>( converter: (store) => store.state.toString(), builder: (context, count) { return new Text( count, style: Theme.of(context).textTheme.display1, ); }, ) ], ), ), // 同样使用 StoreConnector 来连接Store 和FloatingActionButton // 在这个demo中,我们使用store 去构建一个包含dispatch、Increment // action的回调函数 // // 将这个回调函数丢给 onPressed floatingActionButton: new StoreConnector<int, VoidCallback>( converter: (store) { return () => store.dispatch(Actions.Increment); }, builder: (context, callback) { return new FloatingActionButton( onPressed: callback, tooltip: ‘Increment’, child: new Icon(Icons.add), ); }, ), ), ), ); } }上面的例子比较简单,鉴于小册Flutter入门实战:从0到1仿写web版掘金App下面有哥们在登陆那块评论了Flutter状态管理,这里我简单使用redux模拟了一个登陆的demolib/reducer/reducers.dart首先我们定义action需要的一些action type enum Actions{ Login, LoginSuccess, LogoutSuccess }然后定义相应的类来管理登陆状态 class AuthState{ bool isLogin; //是否登录 String account; //用户名 AuthState({this.isLogin:false,this.account}); @override String toString() { return “{account:$account,isLogin:$isLogin}”; } }然后我们需要定义一些action,定义个基类,然后定义登陆成功的action class Action{ final Actions type; Action({this.type}); } class LoginSuccessAction extends Action{ final String account; LoginSuccessAction({ this.account }):super( type:Actions.LoginSuccess ); }最后定义 AppState 以及我们自定义的一个中间件。 // 应用程序状态 class AppState { AuthState auth; //登录 MainPageState main; //主页 AppState({this.main, this.auth}); @override String toString() { return “{auth:$auth,main:$main}”; } } AppState mainReducer(AppState state, dynamic action) { if (Actions.LogoutSuccess == action) { state.auth.isLogin = false; state.auth.account = null; } if (action is LoginSuccessAction) { state.auth.isLogin = true; state.auth.account = action.account; } print(“state changed:$state”); return state; } loggingMiddleware(Store<AppState> store, action, NextDispatcher next) { print(’${new DateTime.now()}: $action’); next(action); }在稍微大一点的项目中,其实就是reducer 、 state 和 action 的组织会比较麻烦,当然,罗马也不是一日建成的, 庞大的state也是一点一点累计起来的。下面就是在入口文件中使用 redux 的代码了,跟基础demo没有差异。 import ‘package:flutter/material.dart’; import ‘package:flutter_redux/flutter_redux.dart’; import ‘package:redux/redux.dart’; import ‘dart:async’ as Async; import ‘./reducer/reducers.dart’; import ‘./login_page.dart’; void main() { Store<AppState> store = Store<AppState>(mainReducer, initialState: AppState( main: MainPageState(), auth: AuthState(), ), middleware: [loggingMiddleware]); runApp(new MyApp( store: store, )); } class MyApp extends StatelessWidget { final Store<AppState> store; MyApp({Key key, this.store}) : super(key: key); @override Widget build(BuildContext context) { return new StoreProvider(store: store, child: new MaterialApp( title: ‘Flutter Demo’, theme: new ThemeData( primarySwatch: Colors.blue, ), home: new StoreConnector<AppState,AppState>(builder: (BuildContext context,AppState state){ print(“isLogin:${state.auth.isLogin}”); return new MyHomePage(title: ‘Flutter Demo Home Page’, counter:state.main.counter, isLogin: state.auth.isLogin, account:state.auth.account); }, converter: (Store<AppState> store){ return store.state; }) , routes: { “login”:(BuildContext context)=>new StoreConnector(builder: ( BuildContext context,Store<AppState> store ){ return new LoginPage(callLogin: (String account,String pwd) async{ print(“正在登录,账号$account,密码:$pwd”); // 为了模拟实际登录,这里等待一秒 await new Async.Future.delayed(new Duration(milliseconds: 1000)); if(pwd != “123456”){ throw (“登录失败,密码必须是123456”); } print(“登录成功!”); store.dispatch(new LoginSuccessAction(account: account)); },); }, converter: (Store<AppState> store){ return store; }), }, )); } } class MyHomePage extends StatelessWidget { MyHomePage({Key key, this.title, this.counter, this.isLogin, this.account}) : super(key: key); final String title; final int counter; final bool isLogin; final String account; @override Widget build(BuildContext context) { print(“build:$isLogin”); Widget loginPane; if (isLogin) { loginPane = new StoreConnector( key: new ValueKey(“login”), builder: (BuildContext context, VoidCallback logout) { return new RaisedButton( onPressed: logout, child: new Text(“您好:$account,点击退出”),); }, converter: (Store<AppState> store) { return () => store.dispatch( Actions.LogoutSuccess ); }); } else { loginPane = new RaisedButton(onPressed: () { Navigator.of(context).pushNamed(“login”); }, child: new Text(“登录”),); } return new Scaffold( appBar: new AppBar( title: new Text(title), ), body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ /// 有登录,展示你好:xxx,没登录,展示登录按钮 loginPane ], ), ), ); } }完整项目代码:Nealyang/Flutter最后更多学习 Flutter的小伙伴,欢迎入QQ群 Flutter Go :679476515关于 Flutter 组件以及更多的学习,敬请关注我们正在开发的: alibaba/flutter-go参考flutter_architecture_samplesflutter_reduxflutter examplescoped_model ...

January 16, 2019 · 6 min · jiezi

Flutter系列:3.APP基础设施搭建

前言在上一篇文章Flutter系列:2.实现一个简单的登录界面通过一个简单的登录页面带入了Flutter中页面构建的方式以及一些简单控件的使用;在开发一个app前首要的任务往往是搭建app需要的基础结构,比如底部菜单,路由导航,网络请求以及一些常用的颜色、图标、按钮、toast组件等。本次的demo将实现一个简单的app所需的基础结构,实现一个简单的app,基于底部TabBar的方式模块切分,实现网络层调用豆瓣api展示电影列表,任意界面登录验证,app如下图。[GitHub源码传送]TabBar菜单目前app设计中大部分app都是由底部TabBar菜单+顶部导航信息的方式构建的,在iOS开发中UITabBarController 和 UINavigationController 几乎是APP的标配, 同样在Flutter中基于Scaffold的构建方式也直接提供了appBar+body+bottomNavigationBar的方式来切分导航栏、内容和底部菜单,所以我们只需要在首页的Scaffold构造中传入bottomNavigationBar即可。在Flutter中为我们提供了material design风格的BottomNavigationBar和iOS风格的CupertinoTabBar,我们只需要选择其一稍作封装即可,本demo选择CupertinoTabBar,并封装到BottomNavWidget中,相关细节请看源码。body的切换虽然Scaffold提供了appBar+body+bottomNavigationBar的组合,但是并没有实现bottomNavigationBar点击切换body页面显示功能,所以需要开发者自己去处理bottomNavigationBar的点击回调来动态切换body中的内容。不同的bottomNavigationBarItem对应着不同的显示页面,电影tabBar对应显示电影列表页面,发现tabBar对应显示发现页面… 他们都在body中,他们之间有着频繁的切换但同时只能显示一个页面;基于此使用Stack布局的方式来实现,每个页面组成一个数组成为Stack Widget的children并缓存避免重复创建,使用Offstage组件来包装每个tab页面,并将bottomNavigationBar当前选择的index对应的页面的offstage设置为false, 这样只有当前选择的tab对应的页面显示在body中,而其他的界面并不会显示也不会接收事件占用空间。路由导航路由导航也是app常见的基础功能,服务器通过下发路由信息可以实现动态的控制app的页面跳转,常用于动态页面,push和web跳转。Flutter中的导航有点类似iOS的方式,都是通过栈的方式来管理路由页面。Navigator就是Flutter中管理导航路线的Widget,注意Navigator管理的是页面导航的路线,称为Route的东西而不是像iOS中直接管理的controller,而每个Route(CupertinoPageRoute)则可以通过builder来指定显示的Widget,同时Navigator也提供了对Route 栈操作的方法,push和pop。Navigator管理的对象是Route,Flutter提供了MaterialPageRoute和iOS风格的CupertinoPageRoute,MaterialPageRoute是根据手机平台自动调整页面的出现动画,本Demo选用CupertinoPageRoute以从右到左的页面出现动画,然后指定其builder即可实现页面的跳转。MaterialApp内置了一个顶层的导航器Navigator,routes属性支持配置静态的路由表,如果在routes中找不到对应的路由配置时则调用onGenerateRoute来支持动态的路由跳转,它的定义如下:所以我们需要通过一个函数来实现MaterialApp的onGenerateRoute就可以根据RouteSettings中的路由信息动态的生成页面的Route,同时以Uri的方式来指定Route的名称就可以实现动态传参了,具体详见Demo源码中RouteManager类。登录注册登录注册页面可能在app的任何页面推出,同时可能不支持返回需要强制登录的情况,在iOS中常常以present的方式出现,所以在Flutter中需要指定CupertinoPageRoute的fullscreenDialog属性为true即可页面的跳转在iOS的开发中基于UITabBarController 和 UINavigationController的构建方式中页面跳转是在UINavigationController内跳转的,同时通过设置Controller的hidesBottomBarWhenPushed属性支持动态的显示和隐藏底部的TarBar, 每个TabBar对应的是一个独立控制的UINavigationController,他们各自有自己路由的导航栈,在Flutter中提供的CupertinoTabScaffold通过为每个TabBar指定显示为CupertinoTabView来实现了同样的机制。往往在开发中进入二级界面后底部的导航栏都是隐藏的,所以我们完全可以只使用MaterialApp内置的顶层Navigator来实现我们的导航控制,本Demo也是如此。网络请求移动端的网络环境是千变万化的,所以app的网络请求应该是一个异步的过程,不能阻塞主线程,本Demo是基于Dart的第三方Http网络请求库dio。dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等…网络层实现了通过dio请求到网络数据然后反序列为Model对象,dart中的json反序列化要比其他语言麻烦,借助的是json_annotation这个库。从请求api到回调再到反序列为Model对象这个过程都应该是一个异步的过程,所以他们返回的都是一个Futrure对象,使用Completer就可以很方便的生成一个Future, 然后在恰当的时候传入数据或者错误来结束这个Future。列表列表的展示是基于FutureBuilder的方式,因为其依赖api请求返回的future,当future的状态变更时FutureBuilder会接收到最新的快照信息AsyncSnapshot,通过其当前快照来控制ListView或者CircularProgressIndicator的显示。其他App开发中还有许多其他的基础模块,比如和原生通信组件(channel)、图片组件、日志组件、其他公共的弹窗、上下拉刷新组件等,本Demo还来不及一一实现,随着学习的深入以后再慢慢总结吧,有不妥的地方还望指正。Demo源码地址:[GitHub源码传送]

January 10, 2019 · 1 min · jiezi

Flutter 环境搭建以及创建第一个APP遇到的坑

win10 64位 安装Flutter一、Flutter官方为中国开发者搭建了临时镜像,大家可以将如下环境变量加入到用户环境变量中:export PUB_HOSTED_URL=https://pub.flutter-io.cnexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn二、获取Flutter SDK://git 拉取flutter sdkgit clone -b dev https://github.com/flutter/flutter.git//配置path 变量(clone项目下的bin文件地址)export PATH="$PWD/flutter/bin:$PATH"cd ./flutter//安装下载依赖flutter doctor//会有错吴如下[-] Android toolchain - develop for Android devices• Android SDK at D:\Android\sdk✗ Android SDK is missing command line tools; download from https://goo.gl/XxQghQ• Try re-installing or updating your Android SDK, visit https://flutter.io/setup/#android-setup for detailed instructions.三、上面报错原因是没有下载安装Android sdk,Android设置。1、下载android studio2、安装3、安装过程中会遇到downloading components慢的问题 [解决方法][1]4、配置好了镜像后 注释掉disable.android.first.run=true 再启动android studio downloading components就会下载好了5、[设置android studio 模拟器][2]6、配置环境变量 android sdk export ANDROID_HOME 例如SDK装在D:\androidSDK中7、解决第二步 报错 android studio 安装flutter和dart插件四、vscode编辑器配置1、安装flutter和dart插件2、调用 View>Command Palette…3、输入 ‘doctor’, 然后选择 ‘Flutter: Run Flutter Doctor’ action没问题就执行如下:4、调用 View>Command Palette…5、输入 ‘flutter’, 然后选择 ‘Flutter: New Project’ action6、新建项目后F5,这里会报错,问题是之前用镜像下载sdk的问题 要删除之前的代理 android sdk路径的gradle.properties 删除代理7、再按f5就ok了 ...

January 9, 2019 · 1 min · jiezi

Flutter Exception降到万分之几的秘密

flutter exception闲鱼技术团队于2018年上半年率先引入了Flutter技术实现客户端开发,到目前为止成功改造并上线了复杂的商品详情和发布业务。随着flutter比重越来越多,我们开始大力治理flutter的exception,起初很长一段时间内闲鱼内flutter的exception率一直在千分之几左右。经过我们的整理和解决,解决了90%以上的flutter exception。我们对exception进行了归类,大头主要分为两大类,这两大类堆栈数量很多,占到整体90%左右:1.第一大类的堆栈都指向了setstate#0 State.setState (package:flutter/src/widgets/framework.dart:1141)#1 _DetailCommentWidgetState.replyInput.<anonymous closure>.<anonymous closure> (package:fwn_idlefish/biz/item_detail/fx_detail_comment.dart:479)#2 FXMtopReq.sendReq.<anonymous closure> (package:fwn_idlefish/common_lib/network/src/mtop_req.dart:32)#3 NetService.requestWithModel.<anonymous closure> (package:fwn_idlefish/common_lib/network/src/net_service.dart:58)#4 _rootRunUnary (dart:async/zone.dart:1132)#5 _CustomZone.runUnary (dart:async/zone.dart:1029)#6 _FutureListener.handleValue (dart:async/future_impl.dart:129)2.第二大类堆栈都与buildContext直接或者间接相关#0 Navigator.of (package:flutter/src/widgets/navigator.dart:1270)#1 Navigator.pop (package:flutter/src/widgets/navigator.dart:1166)#2 UploadProgressDialog.hide (package:fwn_idlefish/biz/publish/upload_progress_dialog.dart:35)#3 PublishSubmitReducer.doPost.<anonymous closure> (package:fwn_idlefish/biz/publish/reducers/publish_submit_reducer.dart:418)<asynchronous suspension>#4 FXMtopReq.sendReq.<anonymous closure> (package:fwn_idlefish/common_lib/network/src/mtop_req.dart:32)#5 NetService.requestWithModel.<anonymous closure> (package:fwn_idlefish/common_lib/network/src/net_service.dart:58)#6 _rootRunUnary (dart:async/zone.dart:1132)#7 _CustomZone.runUnary (dart:async/zone.dart:1029)第一类明显与element和sate的生命周期有关。第二类与buildContext有关。buildContext是什么?下面是一段state中获取buildContext的实现Element get _currentElement => _registry[this];BuildContext get currentContext => _currentElement;很明显buildContext其实就是element实例。buildContext是一个接口,element是buildContext的具体实现。所以上面的exception都指向了flutter element和state的生命周期2.flutter 生命周期1.state生命周期2. element 与state生命周期element是由widget createElement所创建。state的生命周期状态由element调用触发。最核心的是在new elment的时候element的state的双向绑定正式建立。在umount的时候element和state的双向绑定断开。3. activity生命周期与state关系flutter提供WidgetsBindingObserver给开发者来监听AppLifecycleState。AppLifecycleState有4中状态1.resumed界面可见,比如应用从后台到前台2.inactive页面退到后台或者弹出dialog等情况下这种状态下接收不到很任何用户输入,但是还会有drawframe的回调3.paused应用挂起,比如退到后台。进入这种状态代表不在有任何drawframe的回调4.suspendingios中没用,puased之后进入的状态,进入这种状态代表不在有任何drawframe的回调看下android生命周期和appLifecycleState、state关系创建2.按home键退到后台3.从后台回到前台4.back键退出当前页面(route pop)5.back键退出应用3.常见的exception例子1.在工程开发中,我们最容易忽略了state的dispose状态。看一段例子:这个例子可能会在某些情况下excetion。在state dispose后,element会和state断开相互引用,如果在这个时候开发者去拿element的位置信息或者调用setstate 刷新布局时就会报异常。最常见的是在一些timer、animate、网络请求等异步逻辑后调用setstate导致的excetion。安全的做法是在调用setstate前判断一下state是否是mounted状态。如下:2.buildcontext使用错误看一段错误使用buildcontext例子上面的错误在于在跨堆栈使用了buildcontext。由于outcontext的生命周期与buttomcontext不一致,在弹出bottomsheet的时候outcontext可以已经处于umount或者deactivite。上面例子正确的做法是使用bottomcontext获取focusScopeNode。我们在跨堆栈传递参数(如bottomsheet、dialog、alert、processdialog等)场景时特别要注意buildcontext的使用。最后不过瘾?如果你还想了解更多关于flutter开发更多有趣的实战经验,就来关注微信公众号 “闲鱼技术”。参考https://github.com/flutter/flutterhttps://flutter.io/docs本文作者:闲鱼技术-虚白阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 27, 2018 · 1 min · jiezi

Flutter开发遇坑记录

问题1 Android Studio flutter 项目运行报错Launching lib/main.dart on Android SDK built for x86 in debug mode…Initializing gradle…Finished with error: ProcessException: Process “/Users//AndroidStudioProjects/flutter_app_one/android/gradlew” exited abnormally:Downloading https://services.gradle.org/distributions/gradle-4.10.2-all.zipException in thread “main” java.net.ConnectException: Operation timed out (Connection timed out) at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) at java.net.Socket.connect(Socket.java:589) at sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:673) at sun.security.ssl.BaseSSLSocketImpl.connect(BaseSSLSocketImpl.java:173) at sun.net.NetworkClient.doConnect(NetworkClient.java:180) at sun.net.www.http.HttpClient.openServer(HttpClient.java:432) at sun.net.www.http.HttpClient.openServer(HttpClient.java:527) at sun.net.www.protocol.https.HttpsClient.<init>(HttpsClient.java:264) at sun.net.www.protocol.https.HttpsClient.New(HttpsClient.java:367) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.getNewHttpClient(AbstractDelegateHttpsURLConnection.java:191) at sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1138) at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1032) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:177) at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1546) at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474) at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254) at org.gradle.wrapper.Download.downloadInternal(Download.java:58) at org.gradle.wrapper.Download.download(Download.java:44) at org.gradle.wrapper.Install$1.call(Install.java:61) at org.gradle.wrapper.Install$1.call(Install.java:48) at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65) at org.gradle.wrapper.Install.createDist(Install.java:48) at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128) at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61) Command: /Users//AndroidStudioProjects/flutter_app_one/android/gradlew -v主要看这句话Finished with error: ProcessException: Process “/Users/***/AndroidStudioProjects/flutter_app_one/android/gradlew” exited abnormally:Downloading https://services.gradle.org/distributions/gradle-4.10.2-all.zip由于网的问题(翻墙),Android构建工具 gradle 下载失败了。下载位置是//{AppName}/android/gradle/wrapper/gradle-wrapper.propertiesdistributionUrl=https://services.gradle.org/distributions/gradle-4.10.2-all.zip解决方法是,将这个文件 gradle-4.10.2-all.zip 下载到本地,再把上面的地址指向你本地的文件地址即可,记得重启。(持续更新……) ...

December 26, 2018 · 1 min · jiezi

Flutter-WeChat -- 学习Flutter期间仿做的一个微信App

Flutter-WeChat – 学习Flutter期间仿做的一个微信App全都是app静态布局与部分页面交互,没有调用后台接口(个人学习作品,侵权必删)还在学习中,后续会更新页面与交互…有问题还望各位指出…谢谢!!!github地址

December 25, 2018 · 1 min · jiezi

Flutter打包踩坑

今天在打包的时候遇到这么一个问题Flutter crash report; please file at https://github.com/flutter/flutter/issues.## commandflutter build apk## exceptionFormatException: FormatException: Bad UTF-8 encoding 0xa8 (at offset 84)_Utf8Decoder.convert (dart:convert/utf.dart:568:13)_Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:345:14) _Utf8ConversionSink.add (dart:convert/string_conversion.dart:341:5) _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:86:18) _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:120:24) _rootRunUnary (dart:async/zone.dart:1132:38) _CustomZone.runUnary (dart:async/zone.dart:1029:19) _CustomZone.runUnaryGuarded (dart:async/zone.dart:931:7) _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:336:11)_BufferingStreamSubscription._add (dart:async/stream_impl.dart:263:7)_SyncStreamController._sendData (dart:async/stream_controller.dart:763:19) _StreamController._add (dart:async/stream_controller.dart:639:7)_StreamController.add (dart:async/stream_controller.dart:585:5) _Socket._onData (dart:io/runtime/binsocket_patch.dart:1721:41)项目运行的时候一切正常打包的时候报错了, 在群里问了一句,说是win下面的普遍情况, 我就一直试着打包了几次了 还是不行然后google了一下, 有个问题很相似,按照他的步骤设置了一下步骤一.先确定你的代码没有问题,如果配置无问题,但是代码有问题,也是同样会出现这个错误.步骤二,android studio修改设置. 具体如下 file - other settings - default settings -找到project encoding,改为utf-8 . get,进行继续去开发吧.运行打包命令,还是同样的报错, 仔细梳理了一下, 打包之前,新增了一个key.properties文件 于是找到这个文件,单独给设置了编码格式。打包运行这次依然是报错了 ,但是提示的不是上一个错误,这说明我们遇到的上一个问题是生效了的。Execution failed for task ‘:app:validateSigningRelease’.Keystore file ‘F:Flutterroute_animationandroidappE:key.jks’ not found for signing config ‘release’.这次的报错,提示的是找不到签名文件。然后打开key.properties 发现签名文件的位置写错了storeFile=E:\key.jks 修改路径为 ‘E:/key.jks’win下的路径要用反斜杠啊。同志们, 一定不要粗心啊。再次运行打包命令打包成功安装,一切正常 ...

December 21, 2018 · 1 min · jiezi

Flutter 插件开发:以微信SDK为例

就像 React Native 一样,在 Flutter 应用中,如果需要调用第三方库的方法或者有一些功能需要使用原生的开发来提供,使用 Flutter Plugin 是一种不错的方式,它本质上就是一个 Dart Package,但与其它的 package 不同点在于,Flutter 插件中一般都存在两个特殊的文件夹:android 与 ios,如果需要编写Java、Kotlin或者 Object-C 以及 Swift 代码,我们就需要在这两个文件夹项目中进行,然后通过相应的方法将原生代码中开发的方法映射到 dart 中。本文以开发一个微信插件为例,为Flutter应用提供微信分享、登录、支付等功能,项目代码可以直接在下方找到,也已经提交至Pub库:原文地址:https://pantao.onmr.com/press/flutter-wechat-plugin.htmlPub库:https://pub.dartlang.org/packages/wechat项目地址:https://github.com/pantao/flutter-wechat创建插件目录要开发插件,可以使用下面的代码快速基于 plugin 模板开始:flutter create –template=plugin wechat上面的代码中,表示以 plugin 模板创建一个名为 wechat 的 package,创建完成之后,整个项目的目录结构就都提供好了,并且官方还提供了一些基本开发示例。目录结构- android // Android 相关原生代码目录- ios // ios 相关原生代码目录- lib // Dart 代码目录- example // 一个完整的调用了我们正在开发的插件的 Flutter App- pubspec.yaml // 项目配置文件从 example/lib/main.dart 开始在开发我们的应用之后,先来了解一下 flutter 为我们生成的文件们,打开 example/lib/main.dart,代码如下:import ‘package:flutter/material.dart’;import ‘dart:async’;import ‘package:flutter/services.dart’;import ‘package:wechat/wechat.dart’;void main() => runApp(MyApp());class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState();}class _MyAppState extends State<MyApp> { String _platformVersion = ‘Unknown’; @override void initState() { super.initState(); initPlatformState(); } // Platform messages are asynchronous, so we initialize in an async method. Future<void> initPlatformState() async { String platformVersion; // Platform messages may fail, so we use a try/catch PlatformException. try { platformVersion = await Wechat.platformVersion; } on PlatformException { platformVersion = ‘Failed to get platform version.’; } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; setState(() { _platformVersion = platformVersion; }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text(‘Plugin example app’), ), body: Center( child: Text(‘Running on: $_platformVersion\n’), ), ), ); }}这里需要特别注意的就是 initPlatformState() 方法中对 Wechat.platformVersion 的调用,这里面的 Wechat 就是我们的插件,platformVersion 就是插件提供的 get 方法,跟着这个文件,找到 lib/wechat.dart 文件,代码如下:import ‘dart:async’;import ‘package:flutter/services.dart’;class Wechat { static const MethodChannel _channel = const MethodChannel(‘wechat’); static Future<String> get platformVersion async { final String version = await _channel.invokeMethod(‘getPlatformVersion’); return version; }}在该文件中,可以看到 class Wechat 定义了一个 get 方法 platformVersion,它的函数体有点特别:final String version = await _channel.invokeMethod(‘getPlatformVersion’);return version;我们的 version 是通过 _channel.invokeMethod(‘getPlatformVersion’) 方法的调用得到的,这个 _channel 就是我们 Dart 代码与 原生代码进行通信的桥了,也是 Flutter 原生插件的核心(当然,如果你编写的插件并不需要原生代码相关的功能,那么,_channel 就是可有可无的了,比如我们可以写一个下面这样的方法,返回 两个数字 a 与 b 的和:class Wechat { … static int calculate (int a, int b) { return a + b; }}之后,修改 example/lib/main.dart 代码:class _MyAppState extends State<MyApp> { String _platformVersion = ‘Unknown’; // 定义一个 int 型变量,用于保存计算结果 int _calculateResult; @override void initState() { super.initState(); initPlatformState(); } Future<void> initPlatformState() async { String platformVersion; try { platformVersion = await Wechat.platformVersion; } on PlatformException { platformVersion = ‘Failed to get platform version.’; } if (!mounted) return; // init 的时候,计算一下 10 + 10 的结果 _calculateResult = Wechat.calculate(10, 10); setState(() { _platformVersion = platformVersion; }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text(‘Plugin example app’), ), body: Container( padding: EdgeInsets.all(16.0), child: SingleChildScrollView( child: Column( children: <Widget>[ Text(‘Running on: $_platformVersion\n’), // 输出该结果 Text(‘Calculate Result: $_calculateResult\n’), ], ), ), ), ), ); }}支持原生编码提供的方法很多时候,写插件,更多的是因为我们需要让应用能够调用原生代码提供的方法,怎么做呢?Android 系统打开 android/src/main/java/com/example/wechat/WechatPlugin.java 文件,看如下代码:package com.example.wechat;import io.flutter.plugin.common.MethodCall;import io.flutter.plugin.common.MethodChannel;import io.flutter.plugin.common.MethodChannel.MethodCallHandler;import io.flutter.plugin.common.MethodChannel.Result;import io.flutter.plugin.common.PluginRegistry.Registrar;/** WechatPlugin /public class WechatPlugin implements MethodCallHandler { /* Plugin registration. / public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), “wechat”); channel.setMethodCallHandler(new WechatPlugin()); } @Override public void onMethodCall(MethodCall call, Result result) { if (call.method.equals(“getPlatformVersion”)) { result.success(“Android " + android.os.Build.VERSION.RELEASE); } else { result.notImplemented(); } }}还记得上面提到的 getPlatformVersion 吗?还记得 _channel 那么,是不是在这里面也看到的对应的存在?没错, dart 中的 getPlatformVersion 通过 _channel.invokeMethod 发起一次请求,然后,Java 代码中的 onMethodCall 方法回被调用,该方法有两个参数:MethodCall call:请求本身Result result:结果处理方法然后通过 call.method 可以知到 _channel.invokeMethod 中的方法名,然后通过 result.success 回调返回成功结果响应。registerWith在上面还有一小段代码 registerWith,可以看到里面有一个调用:final MethodChannel channel = new MethodChannel(registrar.messenger(), “wechat”);channel.setMethodCallHandler(new WechatPlugin());这里就是在注册我们的插件,将 wechat 注册成为我们的 channel 名,这样,才不会调用 alipay 插件的调用最后到了 wechat 插件这里。iOS 系统同样的,这次我们打开 ios/Classes/WechatPlugin.m 文件:#import “WechatPlugin.h”@implementation WechatPlugin+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>)registrar { FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@“wechat” binaryMessenger:[registrar messenger]]; WechatPlugin* instance = [[WechatPlugin alloc] init]; [registrar addMethodCallDelegate:instance channel:channel];}- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@“getPlatformVersion” isEqualToString:call.method]) { result([@“iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); } else { result(FlutterMethodNotImplemented); }}@end虽然语法有所不同,但是,可以看到,跟 android 的 Java 代码结构上几乎是一模一样的,首先 register 一个名为 wechat 的 channel,然后去 handleMethodCall,同样的通过 call.method 拿到方法名,通过 result 做出响应。小试牛刀接下来,我们将前面的 caculate 方法,移到原生代码中来提供(虽然这很没必要,但毕竟,只是为了演示嘛)。Android在前面打开的 android/src/main/java/com/example/wechat/WechatPlugin.java 文件中,修改 onMethodCall 方法: @Override public void onMethodCall(MethodCall call, Result result) { if (call.method.equals(“getPlatformVersion”)) { result.success(“Android " + android.os.Build.VERSION.RELEASE); } else if (call.method.equals(“calculate”)) { int a = call.argument(“a”); int b = call.argument(“b”); int r = a + b; result.success(”” + r); } else { result.notImplemented(); } }添加了 call.method.equals(“calculate”) 判断,这里面具体的过程是:调用 call.argument() 方法,可以取得由 wechat.dart 传递过来的参数计算结果调用 result.success() 响应结果然后,我们需要在 lib/wechat.dart 中修改 calculate 方法的实现,代码如下: static Future<int> calculate (int a, int b) async { final String result = await _channel.invokeMethod(‘calculate’, { ‘a’: a, ‘b’: b }); return int.parse(result); }由于 _channel.invokeMethod 是一个异步操作,所以,我们需要将 calculate 的返回类型修改为 Future,同时加上 async,此时我们就可以直接使用 await 关键字了,跟 JavaScript 中的 await 一样,让我们用同步的方式编写异步代码,在新版的 calculate 代码中,我们并没有直接计算 a+b 的结果,而是调用 _channel.invokeMethod 方法,将 a 与 b 传递给了 Java 端的 onMethodCall 方法,然后返回该方法返回的结果。_channel.invokeMethod该方法接受两个参数,第一个定义一个方法名,它是一个标识,简单来说,它告诉原生端的代码,我们这次是要干什么,第二个参数是一个 Map<String, dynamic> 型数据,是参数列表,我们可以在原生代码中获取到。接着,我们需要更新一下对该方法的调用了,回到 example/lib/main.dart 中,修改成如下调用:_calculateResult = await Wechat.calculate(10, 10);因为我们现在的 calculate 方法已经是一个异步方法了。iOS如果我们的插件需要支持 Android 与 IOS 两端,那么需要同步的在 ios 中实现上面的方法,打开 ios/Classes/WechatPlugin.m 文件,作如下修改:- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSDictionary arguments = [call arguments]; if ([@“getPlatformVersion” isEqualToString:call.method]) { result([@“iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); } else if ([@“calculate” isEqualToString:call.method]) { NSInteger a = [arguments[@“a”] intValue]; NSInteger b = [arguments[@“b”] intValue]; result([NSString stringWithFormat:@"%d”, a + b]); } else { result(FlutterMethodNotImplemented); }}实现过程与 java 端保持一致即可。添加第三方 SDK我们的插件是可以提供微信的分享相关功能的,所以,肯定需要用到第三方SDK,还是从 Android 开始。Android 端 WechatSDK按 官方接入指南 所述,我们需要添加依赖:dependencies { compile ‘com.tencent.mm.opensdk:wechat-sdk-android-with-mta:+’}或dependencies { compile ‘com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+’}前者带有统计功能,这很简单,打开 android/build.gradle 文件 ,在最下方粘贴以上片段即可:…android { compileSdkVersion 27 defaultConfig { minSdkVersion 16 testInstrumentationRunner “android.support.test.runner.AndroidJUnitRunner” } lintOptions { disable ‘InvalidPackage’ }}dependencies { compile ‘com.tencent.mm.opensdk:wechat-sdk-android-with-mta:+’}然后,回到 WechatPlugin.java 文件,先添加一个 register 方法,它将我们的Appid 注册给微信,还是接着前面的 onMethodCall 中的 if 判断:…import com.tencent.mm.opensdk.openapi.WXAPIFactory;… else if (call.method.equals(“register”)) { appid = call.argument(“appid”); api = WXAPIFactory.createWXAPI(context, appid, true); result.success(api.registerApp(appid)); }…然后回到 lib/wechat.dart 添加相应调用:… /// Register app to Wechat with [appid] static Future<dynamic> register(String appid) async { var result = await _channel.invokeMethod( ‘register’, { ‘appid’: appid } ); return result; }…此时,在我们的 example 应该中,就可以调用 Wechat.register 方法,来注册应用了ios按照官方 ios 接入指南所述,我们可以通过 pod 添加依赖:pod ‘WechatOpenSDK’打开 ios/wechat.podspec ,可以看到如下内容:## To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html#Pod::Spec.new do |s| s.name = ‘wechat’ s.version = ‘0.0.1’ s.summary = ‘A new flutter plugin project.’ s.description = <<-DESCA new flutter plugin project. DESC s.homepage = ‘http://example.com’ s.license = { :file => ‘../LICENSE’ } s.author = { ‘Your Company’ => ’email@example.com’ } s.source = { :path => ‘.’ } s.source_files = ‘Classes/**/’ s.public_header_files = ‘Classes//*.h’ s.dependency ‘Flutter’ s.ios.deployment_target = ‘8.0’end留意到数第三行的 s.dependency,这就是在指定我们依赖 Flutter,如果有其它依赖在这里添加一行即可:… s.public_header_files = ‘Classes//*.h’ s.dependency ‘Flutter’ s.dependency ‘WechatOpenSDK’ s.ios.deployment_target = ‘8.0’end然后打开 ios/Classes/WechatPlugin.h 文件,修改如下:#import <Flutter/Flutter.h>#include “WXApi.h”@interface WechatPlugin : NSObject<FlutterPlugin, WXApiDelegate>@end再回到 ios/Classes/WechatPlugin.m,接着前面的 if 条件继续添加判断:… // Register app to Wechat with appid else if ([@“register” isEqualToString:call.method]) { [WXApi registerApp:arguments[@“appid”]]; result(nil); }…此时,我们的插件已经支持微信 SDK 的 注册至微信 功能了,更多实现,本文就不再讨论,有兴趣,可以直接下载完整项目,后面都是大同小异的实现,唯一需要的是,你需要有一定的 Java 编码与 Objective-C 编码能力。 ...

December 20, 2018 · 5 min · jiezi

Flutter路由管理代码这么长长长长长,阿里工程师怎么高效解决?(实用)

背景:在flutter的业务开发过程中,flutter侧会逐渐丰富自己的路由管理。一个轻量的路由管理本质上是页面标识(或页面路径)与页面实例的映射。本文基于dart注解提供了一个轻量路由管理方案。 不论是在native与flutter的混合工程,还是纯flutter开发的工程,当我们实现一个轻量路由的时候一般会有以下几种方法:较差的实现,if-else的逻辑堆叠: 做映射时较差的实现是通过if-else的逻辑判断把url映射到对应的widget实例上,class Router { Widget route(String url, Map params) { if(url == ‘myapp://apage’) { return PageA(url); } else if(url == ‘myapp://bpage’) { return PageB(url, params); } }}这样做的弊端比较明显: 1)每个映射的维护影响全局映射配置的稳定性,每次维护映射管理时需要脑补所有的逻辑分支. 2)无法做到页面的统一抽象,页面的构造器和构造逻辑被开发者自定义. 3)映射配置无法与页面联动,把页面级的配置进行中心化的维护,导致维护责任人缺失.一般的实现,手动维护的映射表: 稍微好一点的是将映射关系通过一个配置信息和一个工厂方法来表现class Router { Map<String, dynamic> mypages = <String, dynamic> { ‘myapp://apage’: ‘pagea’, ‘myapp://bpage’: ‘pageb’ } Widget route(String url, Map params) { String pageId = mypages[url]; return getPageFromPageId(pageId); } Widget getPageFromPageId(String pageId) { switch(pageId) { case ‘pagea’: return PageA(); case ‘pageb’: return PageB(); } return null; }在flutter侧这种做法仍然比较麻烦,首先是问题3仍然存在,其次是由于flutter目前不支持反射,必须有一个类似工厂方法的方式来创建页面实例。 为了解决以上的问题,我们需要一套能在页面级使用、自动维护映射的方案,注解就是一个值得尝试的方向。我们的路由注解方案annotation_route(github地址:https://github.com/alibaba-flutter/annotation_route)) 应运而生,整个注解方案的运行系统如图所示: 让我们从dart注解开始,了解这套系统的运作。dart注解注解,实际上是代码级的一段配置,它可以作用于编译时或是运行时,由于目前flutter不支持运行时的反射功能,我们需要在编译期就能获取到注解的相关信息,通过这些信息来生成一个自动维护的映射表。那我们要做的,就是在编译时通过分析dart文件的语法结构,找到文件内的注解块和注解的相关内容,对注解内容进行收集,最后生成我们想要的映射表,这套方案的构想如图示: 在调研中发现,dart的部分内置库加速了这套方案的落地。source_gendart提供了build、analyser、source_gen这三个库,其中source_gen利用build库和analyser库,给到了一层比较好的注解拦截的封装。从注解功能的角度来看,这三个库分别给到了如下的功能:build库:整套资源文件的处理analyser库:对dart文件生成完备的语法结构source_gen库:提供注解元素的拦截 这里简要介绍下source_gen和它的上下游,先看看我们捋出来的它注解相关的类图:source_gen的源头是build库提供的Builder基类,该类的作用是让使用者自定义正在处理的资源文件,它负责提供资源文件信息,同时提供生成新资源文件的方法。source_gen从build库提供的Builder类中派生出了一个自己的builder,同时自定义了一套生成器Generator的抽象,派生出来的builder接受Generator类的集合,然后收集Generator的产出,最后生成一份文件,不同的派生builder对generator的处理各异。这样source_gen就把一个文件的构造过程交给了自己定义的多个Generator,同时提供了相对build库而言比较友好的封装。 在抽象的生成器Generator基础上,source_gen提供了注解相关的生成器GeneratorForAnnotation,一个注解生成器实例会接受一个指定的注解类型,由于analyser提供了语法节点的抽象元素Element和其metadata字段,即注解的语法抽象元素ElementAnnotation,注解生成器即可通过检查每个元素的metadata类型是否匹配声明的注解类型,从而筛选出被注解的元素及元素所在上下文的信息,然后将这些信息包装给使用者,我们就可以利用这些信息来完成路由注解。annotation_route在了解了source_gen之后,我们开始着手自己的注解解析方案annotation_route 刚开始介入时,我们遇到了几个问题:只需要生成一个文件:由于一个输入文件对应了一个生成文件后缀,我们需要避免多余的文件生成需要知道在什么时候生成文件:我们需要在所有的备选文件扫描收集完成后再能进行映射表的生成source_gen对一个类只支持了一个注解,但存在多个url映射到一个页面 在一番思索后我们有了如下产出首先将注解分成两类,一类用于注解页面@ARoute,另一类用于注解使用者自己的router@ARouteRoot。routeBuilder拥有RouteGenerator实例,RouteGenerator实例,负责@ARoute注解;routeWriteBuilder拥有RouteWriterGenerator实例,负责@ARouteRoot注解。通过build库支持的配置文件build.yaml,控制两类builder的构造顺序,在routeBuilder执行完成后去执行routeWriteBuilder,这样我们就能准确的在所有页面注解扫描完成后开始生成自己的配置文件。 在注解解析工程中,对于@ARoute注解的页面,通过RouteGenerator将其配置信息交给拥有静态存储空间的Collector处理,同时将其输出内容设为null,即不会生成对应的文件。在@ARoute注解的所有页面扫描完成后,RouteWriteGenerator则会调用Writer,它从Collector中提取信息,并生成最后的配置文件。对于使用者,我们提供了一层友好的封装,在使用annotation_route配置到工程后,我们的路由代码发生了这样的变化: 使用前: class Router { Widget pageFromUrlAndQuery(String urlString, Map<String, dynamic> query) { if(urlString == ‘myapp://testa’) { return TestA(urlString, query); } else if(urlString == ‘myapp://testb’) { String absoluteUrl = Util.join(urlString, query); return TestB(url: absoluteUrl); } else if(urlString == ‘myapp://testc’) { String absoluteUrl = Util.join(urlString, query); return TestC(config: absoluteUrl); } else if(urlString == ‘myapp://testd’) { return TestD(PageDOption(urlString, query)); } else if(urlString == ‘myapp://teste’) { return TestE(PageDOption(urlString, query)); } else if(urlString == ‘myapp://testf’) { return TestF(PageDOption(urlString, query)); } else if(urlString == ‘myapp://testg’) { return TestG(PageDOption(urlString, query)); } else if(urlString == ‘myapp://testh’) { return TestH(PageDOption(urlString, query)); } else if(urlString == ‘myapp://testi’) { return TestI(PageDOption(urlString, query)); } return DefaultWidget; } }使用后:import ‘package:annotation_route/route.dart’; class MyPageOption { String url; Map<String, dynamic> query; MyPageOption(this.url, this.query); } class Router { ARouteInternal internal = ARouteInternalImpl(); Widget pageFromUrlAndQuery(String urlString, Map<String, dynamic> query) { ARouteResult routeResult = internal.findPage(ARouteOption(url: urlString, params: query), MyPageOption(urlString, query)); if(routeResult.state == ARouteResultState.FOUND) { return routeResult.widget; } return DefaultWidget; } }目前该方案已在闲鱼app内稳定运行,我们提供了基础的路由参数,随着flutter业务场景越来越复杂,我们也会在注解的自由度上进行更深的探索。关于annotation_route更加详细的安装和使用说明参见github地址:https://github.com/alibaba-flutter/annotation_route ,在使用中遇到任何问题,欢迎向我们反馈。本文作者:闲鱼技术-兴往阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 18, 2018 · 2 min · jiezi

【解决问题】FlutterBlue在安卓手机上无法连接设备,扫描缓慢

现在的FlutterBlue在安卓手机上很难搜索连接,在iOS上是没问题的,进行下列更改可以快速修复这个问题,但是会损失掉一些功能,不能通过指定Service的UUID搜索到设备(因为uuids数组被改成了[])。懒人可以直接用我fork之后修改的版本,修改YML文件的flutter_blue地址如下: flutter_blue: git: url: git://github.com/mjl0602/flutter_blue.gitIf you can’t use flutterblue connect device with Android Phone. You can try low version Api of Android. These apis are deprecated but worked very well in some Android Phone. ThesePhones are bad support with new API: you can call the function successful, but scan and connect will be very slow and easy connect fail. To solve this problem, change file: android/src/main/java/com/pauldemarco/flutterblue/see new file on: https://github.com/mjl0602/fl...This change didn’t solve this problem completely. To solve this problem, must add new args to control the api version. but not use Build.VERSION.SDK_INT. It’s works bad.简单的说,就是很多手机系统到了新版本,但是对新版本的硬件API支持的很差,强制换成老版本的用法就好了。希望作者加一个字段来控制具体用什么版本的API来搜索。Change connect way.// old code,hardly connect device on Red Mi Note 4 // BluetoothGatt gattServer = device.connectGatt(registrar.activity(), options.getAndroidAutoConnect(), mGattCallback);// improve MI phone connect speed. If didn’t call connect,Mi phone can’t connect successBluetoothGatt gattServer = device.connectGatt(registrar.activity(), false, mGattCallback);gattServer.connect();Use old version api// use old version apiprivate void startScan(MethodCall call, Result result) { byte[] data = call.arguments(); Protos.ScanSettings settings; try { settings = Protos.ScanSettings.newBuilder().mergeFrom(data).build(); // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // startScan21(settings); // } else { startScan18(settings); // } result.success(null); } catch (Exception e) { result.error(“startScan”, e.getMessage(), e); }}private void stopScan() { // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // stopScan21(); // } else { stopScan18(); // }}Remove uuids arg.// boolean success = mBluetoothAdapter.startLeScan(uuids, getScanCallback18());boolean success = mBluetoothAdapter.startLeScan(getScanCallback18());本文禁止任何类型转载 ...

December 18, 2018 · 1 min · jiezi

《Flutter实战》中文原创书籍开源

《Flutter实战》 为Flutter中文网开源电子书项目,本书系统介绍了Flutter各个方面,是第一本中文原创Flutter技术书籍:在线阅读地址:https://book.flutterchina.club《Flutter实战》部分目录缘起起步移动开发技术简介Flutter简介搭建Flutter开发环境Dart语言简介第一个Flutter应用计数器示例路由管理包管理资源管理调试Flutter APP基础WidgetsWidget简介文本、字体样式按钮图片和Icon单选框和复选框输入框和表单布局类Widgets布局类Widgets简介线性布局Row、Column弹性布局Flex流式布局Wrap、Flow层叠布局Stack、Positioned容器类WidgetsPadding布局限制类容器ConstrainedBox、SizeBox装饰容器DecoratedBox变换TransformContainer容器可滚动Widgets可滚动Widgets简介SingleChildScrollViewListViewGridViewCustomScrollView滚动监听及控制ScrollController功能型Widgets导航返回拦截-WillPopScope数据共享-InheritedWidget主题-Theme事件处理与通知原始指针事件处理手势识别全局事件总线通知Notification动画Flutter动画简介动画结构自定义路由过渡动画Hero动画交错动画自定义Widget自定义Widget方法简介通过组合现有Widget实现实例:TurnBoxCustomPaint与Canvas实例:圆形渐变进度条(自绘)文件操作与网络请求文件操作Http请求-HttpClientHttp请求-Dio packageWebSocket使用Socket APIJson转Model包与插件开发package插件开发:平台通道简介插件开发:实现Android端API插件开发:实现IOS端API系统能力调用国际化让App支持多语言实现Localizations使用Intl包更多内容,请移步《Flutter实战》

December 18, 2018 · 1 min · jiezi

基于 Redux + Redux Persist 进行状态管理的 Flutter 应用示例

好久没在 SegmentFault 写东西,唉,也不知道 是忙还是懒,以后有时间 再慢慢写起来吧,最近开始学点新东西,有的写了,个人博客跟这里同步。一直都在自己的 React Native 应用中使用 Redux,其实更大情况下也是使用它来管理应用的会话状态以及当前登录的用户信息等等简单的数据,很好用,自从 Google 发布 Flutter 之后,就一直想着拿它来做点啥,准备拿一个新项目开刀,先研究下怎么把以前在 React Native 中需要用到的一些技术在 Flutter 找到对应的实现方法,本文记录下 Flutter + Redux + Redux Persist 的实现。原文地址:Flutter + Redux + Redux Persist 应用项目地址:https://github.com/pantao/flutter-redux-demo-app<!–more–>第一步:创建一个新的应用:redux_demo_appflutter create redux_demo_appcd redux_demo_appcode .Flutter 项目必须是一个合法的 Dart 包,而 Dart 包要求使用纯小写字母(可包含下划线),这个跟 React Native 是不一样的。第二步:添加依懒我们依懒下面这些包:Redux : JavaScript Redux 的复刻版Flutter Redux:类似于 React Redux 一样,让我们在 Flutter 项目中更好的使用 ReduxRedux Persist:Redux 持久化Redux Persist Flutter:Flutter Redux Persist 引擎打开 pubspec.yaml,在 dependencies 中添加下面这些依懒:…dependencies: … redux: ^3.0.0 flutter_redux: ^0.5.2 redux_persist: ^0.8.0 redux_persist_flutter: ^0.8.0dev_dependencies: ……第三步:了解需求本次我想做的一个App有下面四个页面:首页个人中心页个人资料详情页登录页交互是下面这样的:应用打开之后,打开的是一个有两个底部 Tab 的应用,默认展示的是首页当用户点击(我的)这个Tab时:若当前用户已登录,则Tab切换为个人中心页若当前用户未登录,则以 Modal 的方式弹出登录页添加 lib/state.dart 文件内容如下:enum Actions{ login, logout}/// App 状态/// /// 状态中所有数据都应该是只读的,所以,全部以 get 的方式提供对外访问,不提供 set 方法class AppState { /// J.W.T String _authorizationToken; // 获取当前的认证 Token get authorizationToken => _authorizationToken; // 获取当前是否处于已认证状态 get authed => _authorizationToken.length > 0; AppState(this._authorizationToken);}/// ReducerAppState reducer(AppState state, action) { switch(action) { case Actions.login: return AppState(‘J.W.T’); case Actions.logout: return AppState(’’); default: return state; }}在上面的代码中,我们先声明了 Actions 枚举,以及一个 AppState 类,该类就是我们的应用状态类,使用 _authorizationToken 保证认证的值不可被实例外直接被访问到,这样用户就无法去直接修改它的值,再提供了两个 get 方法,提供给外部访问它的值。接着我们定义了一个 reducer 函数,用于更新状态。创建 app.dartimport ‘package:flutter/material.dart’;import ‘package:redux/redux.dart’;import ‘package:flutter_redux/flutter_redux.dart’;import ‘state.dart’;import ‘root.dart’;/// 示例Appclass DemoApp extends StatelessWidget { // app store final Store<AppState> store; DemoApp(this.store); @override Widget build(BuildContext context) { return StoreProvider<AppState>( store: store, child: new MaterialApp( title: ‘Flutter Redux Demo App’, // home 为 root 页 home: Root() ), ); }}在上面我们已经完成的 App 类的编码,现在需要完成 Root 页,也就是我们的App入口页。创建 Root 页import ‘package:flutter/material.dart’;import ‘package:redux/redux.dart’;import ‘package:flutter_redux/flutter_redux.dart’;/// 状态import ‘state.dart’;/// 登录页面import ‘auth.dart’;/// 我的页面import ‘me.dart’;/// 首页import ‘home.dart’;/// 应用入口页class Root extends StatefulWidget { @override State<StatefulWidget> createState() { return _RootState(); }}/// 入口页状态class _RootState extends State<Root> { /// 当前被激活的 Tab Index int _currentTabIndex; /// 所有 Tab 列表页 List<Widget> _tabPages; @override void initState() { super.initState(); // 初始化 tab 为第 0 个 _currentTabIndex = 0; // 初始化页面列表 _tabPages = <Widget>[ // 首页 Home(), // 我的 Me() ]; } @override Widget build(BuildContext context) { // 使用 StoreConnector 创建 Widget // 类似于 React Redux 的 connect,链接 store state 与 Widget return StoreConnector<AppState, Store<AppState>>( // store 转换器,类似于 react redux 中的 mapStateToProps 方法 // 接受参数为 store,再返回的数据可以被在 builder 函数中使用, // 在此处,我们直接返回整个 store, converter: (store) => store, // 构建器,第二个参数 store 就是上一个 converter 函数返回的 store builder: (context, store) { // 取得当前是否已登录状态 final authed = store.state.authed; return new Scaffold( // 如果已登录,则直接可以访问所有页面,否则展示 Home body: authed ? _tabPages[_currentTabIndex] : Home(), // 底部Tab航 bottomNavigationBar: BottomNavigationBar( onTap: (int index) { // 如果点击的是第 1 个Tab,且当前用户未登录,则直接打开登录 Modal 页 if (!authed && index == 1) { Navigator.push( context, MaterialPageRoute( builder: (context) => Auth(), fullscreenDialog: true ) ); // 否则直接进入相应页面 } else { setState(() { _currentTabIndex = index; }); } }, // 与 body 取值方式类似 currentIndex: authed ? _currentTabIndex : 0, items: [ BottomNavigationBarItem( icon: Icon(Icons.home), title: Text(‘首页’) ), BottomNavigationBarItem( icon: Icon(Icons.people), title: Text(‘我的’) ) ], ), ); }, ); }}创建 Home与 Root 页面类似,我们可以在任何页面方便的使用 AppStateimport ‘package:flutter/material.dart’;import ‘package:redux/redux.dart’;import ‘package:flutter_redux/flutter_redux.dart’;import ‘state.dart’;import ‘auth.dart’;class Home extends StatefulWidget { @override State<StatefulWidget> createState() => _HomeState();}class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return StoreConnector<AppState, Store<AppState>>( converter: (store) => store, builder: (context, store) { return Scaffold( appBar: AppBar( title: Text(‘首页’), ), body: Center( child: store.state.authed ? Text(‘您已登录’) : FlatButton( child: Text(‘去登录’), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => Auth(), fullscreenDialog: true ) ); }, ) ), ); }, ); }}完成 Auth在前面的所有页面中,都只是对 store 中状态树的读取,现在的 Auth 就需要完成对状态树的更新了,看下面代码:import ‘package:flutter/material.dart’;import ‘package:redux/redux.dart’;import ‘package:flutter_redux/flutter_redux.dart’;import ‘state.dart’;class Auth extends StatefulWidget { @override State<StatefulWidget> createState() => _AuthState();}class _AuthState extends State<Auth> { @override Widget build(BuildContext context) { return StoreConnector<AppState, Store<AppState>>( converter: (store) => store, builder: (context, store) { return Scaffold( appBar: AppBar( title: Text(‘登录’), ), body: Center( child: FlatButton( child: Text(‘登录’), onPressed: () { // 通过 store.dispatch 函数,可以发出 action(跟 Redux 是一样的),而 Action 是在 // AppState 中定义的枚举 Actions.login store.dispatch(Actions.login); // 之后,关闭当前的 Modal,就可以看到应用所有数据都更新了 Navigator.pop(context); }, ) ), ); }, ); }}创建 Me有了登录之后,我们可以在做一个我的页面,在这个页面里面我们可以完成退出功能。import ‘package:flutter/material.dart’;import ‘package:redux/redux.dart’;import ‘package:flutter_redux/flutter_redux.dart’;import ‘state.dart’;class Me extends StatefulWidget { @override State<StatefulWidget> createState() => _MeState();}class _MeState extends State<Me> { @override Widget build(BuildContext context) { return StoreConnector<AppState, Store<AppState>>( converter: (store) => store, builder: (context, store) { return Scaffold( appBar: AppBar( title: Text(‘退出’), ), body: Center( child: FlatButton( child: Text(‘退出’), onPressed: () { store.dispatch(Actions.logout); // 此处我们不需要去更新Tab Index,在 Root 页面中,对 store 里面的 authed 值已经做了监听,如果 // Actions.logout 被触发后, authed 的值会变成 false,那么App将自动切换首页 }, ) ), ); }, ); }}添加状态持久化在上面,我们已经完成了一个基于 Redux 的同步状态的App,但是当你的App关闭重新打开之外,状态树就会被重置为初始值,这并不理想,我们经常需要一个用户完成登录之后,就可以在一断时间内一直保持这个登录状态,而且有一些数据我们并不希望每次打开App的时候都重新初始化一次,这个时候,可以考虑对状态进行持久化了。更新 state.dartclass AppState { … // 持久化时,从 JSON 中初始化新的状态 static AppState fromJson(dynamic json) => json != null ? AppState(json[‘authorizationToken’] as String) : AppState(’’); // 更新状态之后,转成 JSON,然后持久化至持久化引擎中 dynamic toJson() => {‘authorizationToken’: _authorizationToken};}这里我们添加了两个方法,一个是静态的 fromJson 方法,它将在初始化状态树时被调用,用于从 JSON 中初始化一个新的状态树出来, toJson 将被用于持久化,将自身转成 JSON。更新 main.dartimport ‘package:flutter/material.dart’;import ‘package:redux/redux.dart’;import ‘package:redux_persist/redux_persist.dart’;import ‘package:redux_persist_flutter/redux_persist_flutter.dart’;import ‘app.dart’;import ‘state.dart’;void main() async { // 创建一个持久化器 final persistor = Persistor<AppState>( storage: FlutterStorage(), serializer: JsonSerializer<AppState>(AppState.fromJson), debug: true ); // 从 persistor 中加载上一次存储的状态 final initialState = await persistor.load(); final store = Store<AppState>( reducer, initialState: initialState ?? AppState(’’), middleware: [persistor.createMiddleware()] ); runApp(new DemoApp(store));}重新 flutter run 当前应用,即完成了持久化,可以登录,然后退出应用,再重新打开应用,可以看到上一次的登录状态是存在的。 ...

December 17, 2018 · 4 min · jiezi

Flutter要火!Dart你会了吗?

从Flutter问世,人们对他的关注一直不断,特别是前不久Flutter 1.0发布后,人们对他的关注更多了,Flutter要火!那就学习一下了,我呢,身为一个前端开发工作者,就以一个前端开发者的身份来学习Flutter,由于Flutter是使用的Dart语言,那就从Dart开始吧!语言特性Dart所有的东西都是对象, 即使是数字numbers、函数function、null也都是对象,所有的对象都继承自Object类。Dart动态类型语言, 尽量给变量定义一个类型,会更安全,没有显示定义类型的变量在 debug 模式下会类型会是 dynamic(动态的)。Dart 在 running 之前解析你的所有代码,指定数据类型和编译时的常量,可以提高运行速度。Dart中的类和接口是统一的,类即接口,你可以继承一个类,也可以实现一个类(接口),自然也包含了良好的面向对象和并发编程的支持。Dart 提供了顶级函数(如:main())。Dart 没有 public、private、protected 这些关键字,变量名以"_“开头意味着对它的 lib 是私有的。没有初始化的变量都会被赋予默认值 null。final的值只能被设定一次。const 是一个编译时的常量,可以通过 const 来创建常量值,var c=const[];,这里 c 还是一个变量,只是被赋值了一个常量值,它还是可以赋其它值。实例变量可以是 final,但不能是 const。编程语言并不是孤立存在的,Dart也是这样,他由语言规范、虚拟机、类库和工具等组成:SDK:SDK 包含 Dart VM、dart2js、Pub、库和工具。Dartium:内嵌 Dart VM 的 Chromium ,可以在浏览器中直接执行 dart 代码。Dart2js:将 Dart 代码编译为 JavaScript 的工具。Dart Editor:基于 Eclipse 的全功能 IDE,并包含以上所有工具。支持代码补全、代码导航、快速修正、重构、调试等功能。关键字(56个)abstract do import super as dynamic in switch assert else interface sync enum implements is this async export library throw await external mixin true break extends new try case factory null typedef catch false operator var class final part void const finally rethrow while continue for return with covariant get set yield default if static deferred变量与常量变量声明与初始化调用的变量name包含对String值为“张三” 的对象的引用,name推断变量的类型是String,但可以通过指定它来更改该类型,如果对象不限于单一类型(没有明确的类型),请使用Object或dynamic关键字。 // 没有明确类型,编译的时候根据值明确类型 var name = ‘Bob’; Object name = ‘张三’; dynamic name = ‘李四’; // 显示声明将被推断类型, 可以使用String显示声明字符串类型 String name = ‘Bob’ ;默认值未初始化的变量的初始值为null(包括数字),因此数字、字符串都可以调用各种方法 //测试 数字类型的初始值是什么? int lineCount; // 为false的时候抛出异常 assert(lineCount == null); print(lineCount); //打印结果为null,证明数字类型初始化值是nullfinal and const如果您从未打算更改一个变量,那么使用 final 或 const,不是var,也不是一个类型。一个 final 变量只能被初始化一次; const变量是一个编译时常量,(Const变量是隐式的final)final的顶级或类变量在第一次使用时被初始化。被final修饰的顶级变量或类变量在第一次声明的时候就需要初始化。// The final variable ‘outSideFinalName’ must be initialized.final String outSideFinalName被final或者const修饰的变量,变量类型可以省略,建议指定数据类型。 //可以省略String这个类型声明 final name = “Bob”; final String name1 = “张三”; const name2 = “alex”; const String name3 = “李四”;被 final 或 const 修饰的变量无法再去修改其值。 final String outSideFinalName = “Alex”; // outSideFinalName’, a final variable, can only be set once // 一个final变量,只能被设置一次。 outSideFinalName = “Bill”; const String outSideName = ‘Bill’; // 这样写,编译器提示:Constant variables can’t be assigned a value // const常量不能赋值 // outSideName = “小白”;flnal 或者 const 不能和 var 同时使用 // Members can’t be declared to be both ‘const’ and ‘var’ const var String outSideName = ‘Bill’; // Members can’t be declared to be both ‘final’ and ‘var’ final var String name = ‘Lili’;常量如果是类级别的,请使用 static const // 常量如果是类级别的,请使用 static const static const String name3 = ‘Tom’; // 这样写保存 // Only static fields can be declared as const // 只有静态字段可以声明为const //const String name3 = ‘Tom’;常量的运算 const speed = 100; //速度(km/h) const double distance = 2.5 * speed; // 距离 = 时间 * 速度 final speed2 = 100; //速度(km/h) final double distance2 = 2.5 * speed2; // 距离 = 时间 * 速度const关键字不只是声明常数变量,您也可以使用它来创建常量值,以及声明创建常量值的构造函数,任何变量都可以有一个常量值。 // 注意: [] 创建的是一个空的list集合 // const []创建一个空的、不可变的列表(EIL)。 var varList = const []; // varList 当前是一个EIL final finalList = const []; // finalList一直是EIL const constList = const []; // constList 是一个编译时常量的EIL // 可以更改非final,非const变量的值 // 即使它曾经具有const值 varList = [“haha”]; // 不能更改final变量或const变量的值 // 这样写,编译器提示:a final variable, can only be set once // finalList = [“haha”]; // 这样写,编译器提示:Constant variables can’t be assigned a value // constList = [“haha”];在常量表达式中,该运算符的操作数必须为’bool’、’num’、‘String’或’null’, const常量必须用conat类型的值初始化。const String outSideName = ‘Bill’;final String outSideFinalName = ‘Alex’;const String outSideName2 = ‘Tom’;const aConstList = const [‘1’, ‘2’, ‘3’];// In constant expressions, operands of this operator must be of type ‘bool’, ’num’, ‘String’ or ’null’// 在常量表达式中,该运算符的操作数必须为’bool’、’num’、‘String’或’null’。const validConstString = ‘$outSideName $outSideName2 $aConstList’;// Const variables must be initialized with a constant value// const常量必须用conat类型的值初始化const validConstString = ‘$outSideName $outSideName2 $outSideFinalName’;var outSideVarName=‘Cathy’;// Const variables must be initialized with a constant value.// const常量必须用conat类型的值初始化const validConstString = ‘$outSideName $outSideName2 $outSideVarName’;// 正确写法const String outSideConstName = ‘Joy’;const validConstString = ‘$outSideName $outSideName2 $outSideConstName’;数据类型numnum 是数字类型的父类,有两个子类 int 和 double。int 根据平台的不同,整数值不大于64位。在Dart VM上,值可以从-263到263 - 1,编译成JavaScript的Dart使用JavaScript代码,允许值从-253到253 - 1。double 64位(双精度)浮点数,如IEEE 754标准所规定。 int a = 1; print(a); double b = 1.12; print(b); // String -> int int one = int.parse(‘1’); // 输出3 print(one + 2); // String -> double var onePointOne = double.parse(‘1.1’); // 输出3.1 print(onePointOne + 2); // int -> String String oneAsString = 1.toString(); // The argument type ‘int’ can’t be assigned to the parameter type ‘String’ //print(oneAsString + 2); // 输出 1 + 2 print(’$oneAsString + 2’); // 输出 1 2 print(’$oneAsString 2’); // double -> String 注意括号中要有小数点位数,否则报错 String piAsString = 3.14159.toStringAsFixed(2); // 截取两位小数, 输出3.14 print(piAsString); String aString = 1.12618.toStringAsFixed(2); // 检查是否四舍五入,输出1.13,发现会做四舍五入 print(aString);StringDart里面的String是一系列 UTF-16 代码单元。您可以使用单引号或双引号来创建一个字符串。单引号或者双引号里面嵌套使用引号。用 或{} 来计算字符串中变量的值,需要注意的是如果是表达式需要${表达式} String singleString = ‘abcdddd’; String doubleString = “abcsdfafd”; String sdString = ‘$singleString a “bcsd” ${singleString}’; String dsString = “abc ‘aaa’ $sdString”; print(sdString); print(dsString); String singleString = ‘aaa’; String doubleString = “bbb”; // 单引号嵌套双引号 String sdString = ‘$singleString a “bbb” ${doubleString}’; // 输出 aaa a “bbb” bbb print(sdString); // 双引号嵌套单引号 String dsString = “${singleString.toUpperCase()} abc ‘aaa’ $doubleString.toUpperCase()”; // 输出 AAA abc ‘aaa’ bbb.toUpperCase(), 可以看出 ”$doubleString.toUpperCase()“ 没有加“{}“,导致输出结果是”bbb.toUpperCase()“ print(dsString);boolDart 是强 bool 类型检查,只有bool 类型的值是true 才被认为是true。只有两个对象具有bool类型:true和false,它们都是编译时常量。Dart的类型安全意味着您不能使用 if(nonbooleanValue) 或 assert(nonbooleanValue) 等代码, 相反Dart使用的是显式的检查值。assert 是语言内置的断言函数,仅在检查模式下有效在开发过程中, 除非条件为真,否则会引发异常。(断言失败则程序立刻终止)。 // 检查是否为空字符串 var fullName = ‘’; assert(fullName.isEmpty); // 检查0 var hitPoints = 0; assert(hitPoints <= 0); // 检查是否为null var unicorn; assert(unicorn == null); // 检查是否为NaN var iMeantToDoThis = 0 / 0; assert(iMeantToDoThis.isNaN);List集合在Dart中,数组是List对象,因此大多数人只是将它们称为List。Dart list文字看起来像JavaScript数组文字 //创建一个int类型的list List list = [10, 7, 23]; // 输出[10, 7, 23] print(list); // 使用List的构造函数,也可以添加int参数,表示List固定长度,不能进行添加 删除操作 var fruits = new List(); // 添加元素 fruits.add(‘apples’); // 添加多个元素 fruits.addAll([‘oranges’, ‘bananas’]); List subFruits = [‘apples’, ‘oranges’, ‘banans’]; // 添加多个元素 fruits.addAll(subFruits); // 输出: [apples, oranges, bananas, apples, oranges, banans] print(fruits); // 获取List的长度 print(fruits.length); // 获取第一个元素 print(fruits.first); // 获取元素最后一个元素 print(fruits.last); // 利用索引获取元素 print(fruits[0]); // 查找某个元素的索引号 print(fruits.indexOf(‘apples’)); // 删除指定位置的元素,返回删除的元素 print(fruits.removeAt(0)); // 删除指定元素,成功返回true,失败返回false // 如果集合里面有多个“apples”, 只会删除集合中第一个改元素 fruits.remove(‘apples’); // 删除最后一个元素,返回删除的元素 fruits.removeLast(); // 删除指定范围(索引)元素,含头不含尾 fruits.removeRange(start,end); // 删除指定条件的元素(这里是元素长度大于6) fruits.removeWhere((item) => item.length >6); // 删除所有的元素 fruits.clear();注意事项:可以直接打印list包括list的元素,list也是一个对象。但是java必须遍历才能打印list,直接打印是地址值。和java一样list里面的元素必须保持类型一致,不一致就会报错。和java一样list的角标从0开始。如果集合里面有多个相同的元素“X”, 只会删除集合中第一个改元素Map集合一般来说,map是将键和值相关联的对象。键和值都可以是任何类型的对象。每个键只出现一次,但您可以多次使用相同的值。Dart支持map由map文字和map类型提供。初始化Map方式一: 直接声明,用{}表示,里面写key和value,每组键值对中间用逗号隔开。 // Two keys in a map literal can’t be equal. // Map companys = {‘Alibaba’: ‘阿里巴巴’, ‘Tencent’: ‘腾讯’, ‘baidu’: ‘百度’, ‘Alibaba’: ‘钉钉’, ‘Tenect’: ‘qq-music’}; Map companys = {‘Alibaba’: ‘阿里巴巴’, ‘Tencent’: ‘腾讯’, ‘baidu’: ‘百度’}; // 输出:{Alibaba: 阿里巴巴, Tencent: 腾讯, baidu: 百度} print(companys);创建Map方式二:先声明,再去赋值。 Map schoolsMap = new Map(); schoolsMap[‘first’] = ‘清华’; schoolsMap[‘second’] = ‘北大’; schoolsMap[’third’] = ‘复旦’; // 打印结果 {first: 清华, second: 北大, third: 复旦} print(schoolsMap); var fruits = new Map(); fruits[“first”] = “apple”; fruits[“second”] = “banana”; fruits[“fifth”] = “orange”; //换成双引号,换成var 打印结果 {first: apple, second: banana, fifth: orange} print(fruits);Map API// 指定键值对的参数类型var aMap = new Map<int, String>();// Map的赋值,中括号中是Key,这里可不是数组aMap[1] = ‘小米’;//Map中的键值对是唯一的//同Set不同,第二次输入的Key如果存在,Value会覆盖之前的数据aMap[1] = ‘alibaba’;// map里面的value可以相同aMap[2] = ‘alibaba’;// map里面value可以为空字符串aMap[3] = ‘’;// map里面的value可以为nullaMap[4] = null;print(aMap);// 检索Map是否含有某Keyassert(aMap.containsKey(1));//删除某个键值对aMap.remove(1); print(aMap); 注意事项:map的key类型不一致也不会报错。添加元素的时候,会按照你添加元素的顺序逐个加入到map里面,哪怕你的key,比如分别是 1,2,4,看起来有间隔,事实上添加到map的时候是{1:value,2:value,4:value} 这种形式。map里面的key不能相同。但是value可以相同,value可以为空字符串或者为null。运算符一元后置操作符 expr++ expr– () [] . ?.一元前置操作符expr !expr ~expr ++expr –expr乘除 / % ~/加减 + -位移 << >>按位与 &按位异或 ^逻辑与 &&关系和类型判断 >= > <= < as is is!等 == !=如果为空 ??条件表达式 expr1 ? expr2 : expr3赋值 = = /= ~/= %= += -= <<= >>= &= ^= = ??=级联..流程控制语句(Control flow statements)if…elseforwhile do-whildbreak continueswitch…caseassert(仅在checked模式有效)异常(Exceptions)throw抛出固定类型的异常 throw new FormatException(‘Expected at least 1 section’);抛出任意类型的异常 throw ‘Out of llamas!’; 因为抛出异常属于表达式,可以将throw语句放在=>语句中,或者其它可以出现表达式的地方 distanceTo(Point other) => throw new UnimplementedError();catch将可能出现异常的代码放置到try语句中,可以通过 on语句来指定需要捕获的异常类型,使用catch来处理异常。 try { breedMoreLlamas(); } on OutOfLlamasException { // A specific exception buyMoreLlamas(); } on Exception catch (e) { // Anything else that is an exception print(‘Unknown exception: $e’); } catch (e, s) { print(‘Exception details:\n $e’); print(‘Stack trace:\n $s’); }rethrowrethrow语句用来处理一个异常,同时希望这个异常能够被其它调用的部分使用。 final foo = ‘’; void misbehave() { try { foo = “1”; } catch (e) { print(‘2’); rethrow;// 如果不重新抛出异常,main函数中的catch语句执行不到 } } void main() { try { misbehave(); } catch (e) { print(‘3’); } }finallyDart的finally用来执行那些无论异常是否发生都执行的操作。 final foo = ‘’; void misbehave() { try { foo = “1”; } catch (e) { print(‘2’); } } void main() { try { misbehave(); } catch (e) { print(‘3’); } finally { print(‘4’); // 即使没有rethrow最终都会执行到 } }函数 Function以下是一个实现函数的例子: bool isNoble(int atomicNumber) { return _nobleGases[atomicNumber] != null; } main()函数每个应用程序都必须有一个顶层main()函数,它可以作为应用程序的入口点。该main()函数返回void并具有List<String>参数的可选参数。void main() { querySelector(’#sample_text_id’) ..text = ‘Click me!’ ..onClick.listen(reverseText);}级联符号:允许您在同一个对象上进行一系列操作。除了函数调用之外,还可以访问同一对象上的字段。这通常会为您节省创建临时变量的步骤,并允许您编写更流畅的代码。querySelector(’#confirm’) // Get an object. ..text = ‘Confirm’ // Use its members. ..classes.add(‘important’) ..onClick.listen((e) => window.alert(‘Confirmed!’));上述例子相对于: var button = querySelector(’#confirm’); button.text = ‘Confirm’; button.classes.add(‘important’); button.onClick.listen((e) => window.alert(‘Confirmed!’));级联符号也可以嵌套使用。 例如: final addressBook = (AddressBookBuilder() ..name = ‘jenny’ ..email = ‘jenny@example.com’ ..phone = (PhoneNumberBuilder() ..number = ‘415-555-0100’ ..label = ‘home’) .build()) .build();当返回值是void时不能构建级联。 例如,以下代码失败:var sb = StringBuffer();sb.write(‘foo’) // 返回void write(‘bar’); // 这里会报错注意: 严格地说,级联的..符号不是操作符。它只是Dart语法的一部分。可选参数可选的命名参数, 定义函数时,使用{param1, param2, …},用于指定命名参数。例如: //设置[bold]和[hidden]标志 void enableFlags({bool bold, bool hidden}) { // … } enableFlags(bold: true, hidden: false);可选的位置参数,用[]它们标记为可选的位置参数: String say(String from, String msg, [String device]) { var result = ‘$from says $msg’; if (device != null) { result = ‘$result with a $device’; } return result; }下面是一个不带可选参数调用这个函数的例子: say(‘Bob’, ‘Howdy’); //结果是: Bob says Howdy 下面是用第三个参数调用这个函数的例子: say(‘Bob’, ‘Howdy’, ‘smoke signal’); //结果是:Bob says Howdy with a smoke signal 默认参数函数可以使用=为命名参数和位置参数定义默认值。默认值必须是编译时常量。如果没有提供默认值,则默认值为null。下面是为命名参数设置默认值的示例: // 设置 bold 和 hidden 标记的默认值都为false void enableFlags2({bool bold = false, bool hidden = false}) { // … } // 调用的时候:bold will be true; hidden will be false. enableFlags2(bold: true);下一个示例显示如何为位置参数设置默认值: String say(String from, String msg, [String device = ‘carrier pigeon’, String mood]) { var result = ‘$from says $msg’; if (device != null) { result = ‘$result with a $device’; } if (mood != null) { result = ‘$result (in a $mood mood)’; } return result; } //调用方式: say(‘Bob’, ‘Howdy’); //结果为:Bob says Howdy with a carrier pigeon;您还可以将list或map作为默认值传递。下面的示例定义一个函数doStuff(),该函数指定列表参数的默认list和gifts参数的默认map。 // 使用list 或者map设置默认值 void doStuff( {List<int> list = const [1, 2, 3], Map<String, String> gifts = const {‘first’: ‘paper’, ‘second’: ‘cotton’, ’third’: ’leather’ }}) { print(’list: $list’); print(‘gifts: $gifts’); }作为一个类对象的功能您可以将一个函数作为参数传递给另一个函数。 void printElement(int element) { print(element); } var list = [1, 2, 3]; // 把 printElement函数作为一个参数传递进来 list.forEach(printElement);您也可以将一个函数分配给一个变量。 var loudify = (msg) => ‘!!! ${msg.toUpperCase()} !!!’; assert(loudify(‘hello’) == ‘!!! HELLO !!!’);匿名函数大多数函数都能被命名为匿名函数,如 main() 或 printElement()。您还可以创建一个名为匿名函数的无名函数,有时也可以创建lambda或闭包。您可以为变量分配一个匿名函数,例如,您可以从集合中添加或删除它。一个匿名函数看起来类似于一个命名函数 - 0或更多的参数,在括号之间用逗号和可选类型标注分隔。下面的代码块包含函数的主体: ([[Type] param1[, …]]) { codeBlock; }; 下面的示例定义了一个具有无类型参数的匿名函数item,该函数被list中的每个item调用,输出一个字符串,该字符串包含指定索引处的值。 var list = [‘apples’, ‘bananas’, ‘oranges’]; list.forEach((item) { print(’${list.indexOf(item)}: $item’); });如果函数只包含一条语句,可以使用箭头符号=>来缩短它, 比如上面的例2可以简写成: list.forEach((item) => print(’${list.indexOf(item)}: $item’)); 返回值所有函数都返回一个值,如果没有指定返回值,则语句return null,隐式地附加到函数体。 foo() {} assert(foo() == null);类(Classes)对象Dart 是一种面向对象的语言,并且支持基于mixin的继承方式。Dart 语言中所有的对象都是某一个类的实例,所有的类有同一个基类–Object。基于mixin的继承方式具体是指:一个类可以继承自多个父类。使用new语句来构造一个类,构造函数的名字可能是ClassName,也可以是ClassName.identifier, 例如: var jsonData = JSON.decode(’{“x”:1, “y”:2}’); // Create a Point using Point(). var p1 = new Point(2, 2); // Create a Point using Point.fromJson(). var p2 = new Point.fromJson(jsonData);使用.(dot)来调用实例的变量或者方法。 var p = new Point(2, 2); // Set the value of the instance variable y. p.y = 3; // Get the value of y. assert(p.y == 3); // Invoke distanceTo() on p. num distance = p.distanceTo(new Point(4, 4));使用?.来确认前操作数不为空, 常用来替代. , 避免左边操作数为null引发异常。 // If p is non-null, set its y value to 4. p?.y = 4;使用const替代new来创建编译时的常量构造函数。 var p = const ImmutablePoint(2, 2);使用runtimeType方法,在运行中获取对象的类型。该方法将返回Type 类型的变量。 print(‘The type of a is ${a.runtimeType}’);实例化变量(Instance variables)在类定义中,所有没有初始化的变量都会被初始化为null。 class Point { num x; // Declare instance variable x, initially null. num y; // Declare y, initially null. num z = 0; // Declare z, initially 0. }类定义中所有的变量, Dart语言都会隐式的定义 setter 方法,针对非空的变量会额外增加 getter 方法。 class Point { num x; num y; } main() { var point = new Point(); point.x = 4; // Use the setter method for x. assert(point.x == 4); // Use the getter method for x. assert(point.y == null); // Values default to null. }构造函数(Constructors)声明一个和类名相同的函数,来作为类的构造函数。 class Point { num x; num y; Point(num x, num y) { // There’s a better way to do this, stay tuned. this.x = x; this.y = y; } }this关键字指向了当前类的实例, 上面的代码可以简化为: class Point { num x; num y; // Syntactic sugar for setting x and y // before the constructor body runs. Point(this.x, this.y); }构造函数不能继承(Constructors aren’t inherited)Dart 语言中,子类不会继承父类的命名构造函数。如果不显式提供子类的构造函数,系统就提供默认的构造函数。命名的构造函数(Named constructors)使用命名构造函数从另一类或现有的数据中快速实现构造函数。class Point { num x; num y; Point(this.x, this.y); // 命名构造函数Named constructor Point.fromJson(Map json) { x = json[‘x’]; y = json[‘y’]; }}构造函数不能被继承,父类中的命名构造函数不能被子类继承。如果想要子类也拥有一个父类一样名字的构造函数,必须在子类是实现这个构造函数。调用父类的非默认构造函数默认情况下,子类只能调用父类的无名,无参数的构造函数; 父类的无名构造函数会在子类的构造函数前调用; 如果initializer list 也同时定义了,则会先执行initializer list 中的内容,然后在执行父类的无名无参数构造函数,最后调用子类自己的无名无参数构造函数。即下面的顺序:initializer list(初始化列表)super class’s no-arg constructor(父类无参数构造函数)main class’s no-arg constructor (主类无参数构造函数)如果父类不显示提供无名无参数构造函数的构造函数,在子类中必须手打调用父类的一个构造函数。这种情况下,调用父类的构造函数的代码放在子类构造函数名后,子类构造函数体前,中间使用:(colon) 分割。class Person { String firstName; Person.fromJson(Map data) { print(‘in Person’); }}class Employee extends Person { // 父类没有无参数的非命名构造函数,必须手动调用一个构造函数 super.fromJson(data) Employee.fromJson(Map data) : super.fromJson(data) { print(‘in Employee’); }}main() { var emp = new Employee.fromJson({}); // Prints: // in Person // in Employee if (emp is Person) { // Type check emp.firstName = ‘Bob’; } (emp as Person).firstName = ‘Bob’;}初始化列表除了调用父类的构造函数,也可以通过初始化列表在子类的构造函数体前(大括号前)来初始化实例的变量值,使用逗号,分隔。如下所示:class Point { num x; num y; Point(this.x, this.y); // 初始化列表在构造函数运行前设置实例变量。 Point.fromJson(Map jsonMap) : x = jsonMap[‘x’], y = jsonMap[‘y’] { print(‘In Point.fromJson(): ($x, $y)’); } }注意:上述代码,初始化程序无法访问 this 关键字。静态构造函数如果你的类产生的对象永远不会改变,你可以让这些对象成为编译时常量。为此,需要定义一个 const 构造函数并确保所有的实例变量都是 final 的。class ImmutablePoint { final num x; final num y; const ImmutablePoint(this.x, this.y); static final ImmutablePoint origin = const ImmutablePoint(0, 0);}重定向构造函数有时候构造函数的目的只是重定向到该类的另一个构造函数。重定向构造函数没有函数体,使用冒号:分隔。class Point { num x; num y; // 主构造函数 Point(this.x, this.y) { print(“Point($x, $y)”); } // 重定向构造函数,指向主构造函数,函数体为空 Point.alongXAxis(num x) : this(x, 0);}void main() { var p1 = new Point(1, 2); var p2 = new Point.alongXAxis(4);}常量构造函数如果类的对象不会发生变化,可以构造一个编译时的常量构造函数。定义格式如下:定义所有的实例变量是final。使用const声明构造函数。class ImmutablePoint { final num x; final num y; const ImmutablePoint(this.x, this.y); static final ImmutablePoint origin = const ImmutablePoint(0, 0);}工厂构造函数当实现一个使用 factory 关键词修饰的构造函数时,这个构造函数不必创建类的新实例。例如,工厂构造函数可能从缓存返回实例,或者它可能返回子类型的实例。 下面的示例演示一个工厂构造函数从缓存返回的对象:class Logger { final String name; bool mute = false; // _cache 是一个私有库,幸好名字前有个 _ 。 static final Map<String, Logger> _cache = <String, Logger>{}; factory Logger(String name) { if (_cache.containsKey(name)) { return _cache[name]; } else { final logger = new Logger._internal(name); _cache[name] = logger; return logger; } } Logger._internal(this.name); void log(String msg) { if (!mute) { print(msg); } } }注意:工厂构造函数不能用 this。方法方法就是为对象提供行为的函数。实例方法对象的实例方法可以访问实例变量和 this 。以下示例中的 distanceTo() 方法是实例方法的一个例子:import ‘dart:math’;class Point { num x; num y; Point(this.x, this.y); num distanceTo(Point other) { var dx = x - other.x; var dy = y - other.y; return sqrt(dx * dx + dy * dy); } }setters 和 Getters是一种提供对方法属性读和写的特殊方法。每个实例变量都有一个隐式的 getter 方法,合适的话可能还会有 setter 方法。你可以通过实现 getters 和 setters 来创建附加属性,也就是直接使用 get 和 set 关键词:class Rectangle { num left; num top; num width; num height; Rectangle(this.left, this.top, this.width, this.height); // 定义两个计算属性: right and bottom. num get right => left + width; set right(num value) => left = value - width; num get bottom => top + height; set bottom(num value) => top = value - height;}main() { var rect = new Rectangle(3, 4, 20, 15); assert(rect.left == 3); rect.right = 12; assert(rect.left == -8);}借助于 getter 和 setter ,你可以直接使用实例变量,并且在不改变客户代码的情况下把他们包装成方法。注: 不论是否显式地定义了一个 getter,类似增量(++)的操作符,都能以预期的方式工作。为了避免产生任何向着不期望的方向的影响,操作符一旦调用 getter ,就会把他的值存在临时变量里。抽象方法Instance , getter 和 setter 方法可以是抽象的,也就是定义一个接口,但是把实现交给其他的类。要创建一个抽象方法,使用分号(;)代替方法体: abstract class Doer { // …定义实例变量和方法… void doSomething(); // 定义一个抽象方法。 } class EffectiveDoer extends Doer { void doSomething() { // …提供一个实现,所以这里的方法不是抽象的… } }枚举类型枚举类型,通常被称为 enumerations 或 enums ,是一种用来代表一个固定数量的常量的特殊类。声明一个枚举类型需要使用关键字 enum : enum Color { red, green, blue }在枚举中每个值都有一个 index getter 方法,它返回一个在枚举声明中从 0 开始的位置。例如,第一个值索引值为 0 ,第二个值索引值为 1 。 assert(Color.red.index == 0); assert(Color.green.index == 1); assert(Color.blue.index == 2);要得到枚举列表的所有值,可使用枚举的 values 常量。 List<Color> colors = Color.values; assert(colors[2] == Color.blue); 你可以在 switch 语句 中使用枚举。如果 e 在 switch (e) 是显式类型的枚举,那么如果你不处理所有的枚举值将会弹出警告: enum Color { red, green, blue } // … Color aColor = Color.blue; switch (aColor) { case Color.red: print(‘Red as roses!’); break; case Color.green: print(‘Green as grass!’); break; default: // Without this, you see a WARNING. print(aColor); // ‘Color.blue’ }枚举类型有以下限制你不能在子类中混合或实现一个枚举。你不能显式实例化一个枚举。为类添加特征:mixinsmixins 是一种多类层次结构的类的代码重用。要使用 mixins ,在 with 关键字后面跟一个或多个 mixin 的名字。下面的例子显示了两个使用mixins的类: class Musician extends Performer with Musical { // … }class Maestro extends Person with Musical, Aggressive, Demented { Maestro(String maestroName) { name = maestroName; canConduct = true; } }要实现 mixin ,就创建一个继承 Object 类的子类,不声明任何构造函数,不调用 super 。例如: abstract class Musical { bool canPlayPiano = false; bool canCompose = false; bool canConduct = false; void entertainMe() { if (canPlayPiano) { print(‘Playing piano’); } else if (canConduct) { print(‘Waving hands’); } else { print(‘Humming to self’); } } }类的变量和方法使用 static 关键字来实现类变量和类方法。只有当静态变量被使用时才被初始化。静态变量, 静态变量(类变量)对于类状态和常数是有用的: class Color { static const red = const Color(‘red’); // 一个恒定的静态变量 final String name; // 一个实例变量。 const Color(this.name); // 一个恒定的构造函数。 } main() { assert(Color.red.name == ‘red’); }静态方法, 静态方法(类方法)不在一个实例上进行操作,因而不必访问 this 。例如: import ‘dart:math’; class Point { num x; num y; Point(this.x, this.y); static num distanceBetween(Point a, Point b) { var dx = a.x - b.x; var dy = a.y - b.y; return sqrt(dx * dx + dy * dy); } } main() { var a = new Point(2, 2); var b = new Point(4, 4); var distance = Point.distanceBetween(a, b); assert(distance < 2.9 && distance > 2.8); }注:考虑到使用高阶层的方法而不是静态方法,是为了常用或者广泛使用的工具和功能。你可以将静态方法作为编译时常量。例如,你可以把静态方法作为一个参数传递给静态构造函数。抽象类使用 abstract 修饰符来定义一个抽象类,该类不能被实例化。抽象类在定义接口的时候非常有用,实际上抽象中也包含一些实现。如果你想让你的抽象类被实例化,请定义一个 工厂构造函数 。抽象类通常包含 抽象方法。下面是声明一个含有抽象方法的抽象类的例子: // 这个类是抽象类,因此不能被实例化。 abstract class AbstractContainer { // …定义构造函数,域,方法… void updateChildren(); // 抽象方法。 }下面的类不是抽象类,因此它可以被实例化,即使定义了一个抽象方法: class SpecializedContainer extends AbstractContainer { // …定义更多构造函数,域,方法… void updateChildren() { // …实现 updateChildren()… } // 抽象方法造成一个警告,但是不会阻止实例化。 void doSomething(); }类-隐式接口每个类隐式的定义了一个接口,含有类的所有实例和它实现的所有接口。如果你想创建一个支持类 B 的 API 的类 A,但又不想继承类 B ,那么,类 A 应该实现类 B 的接口。一个类实现一个或更多接口通过用 implements 子句声明,然后提供 API 接口要求。例如: // 一个 person ,包含 greet() 的隐式接口。 class Person { // 在这个接口中,只有库中可见。 final _name; // 不在接口中,因为这是个构造函数。 Person(this._name); // 在这个接口中。 String greet(who) => ‘Hello, $who. I am $_name.’; } // Person 接口的一个实现。 class Imposter implements Person { // 我们不得不定义它,但不用它。 final _name = “”; String greet(who) => ‘Hi $who. Do you know who I am?’; } greetBob(Person person) => person.greet(‘bob’); main() { print(greetBob(new Person(‘kathy’))); print(greetBob(new Imposter())); }这里是具体说明一个类实现多个接口的例子: class Point implements Comparable, Location { // … }类-扩展一个类使用 extends 创建一个子类,同时 supper 将指向父类: class Television { void turnOn() { _illuminateDisplay(); _activateIrSensor(); } // … } class SmartTelevision extends Television { void turnOn() { super.turnOn(); _bootNetworkInterface(); _initializeMemory(); upgradeApps(); } // … }子类可以重载实例方法, getters 方法, setters 方法。下面是个关于重写 Object 类的方法 noSuchMethod() 的例子,当代码企图用不存在的方法或实例变量时,这个方法会被调用。 class A { // 如果你不重写 noSuchMethod 方法, 就用一个不存在的成员,会导致NoSuchMethodError 错误。 void noSuchMethod(Invocation mirror) { print(‘You tried to use a non-existent member:’ + ‘${mirror.memberName}’); } }你可以使用 @override 注释来表明你重写了一个成员。 class A { @override void noSuchMethod(Invocation mirror) { // … } }如果你用 noSuchMethod() 实现每一个可能的 getter 方法,setter 方法和类的方法,那么你可以使用 @proxy 标注来避免警告。 @proxy class A { void noSuchMethod(Invocation mirror) { // … } }库和可见性import,part,library指令可以帮助创建一个模块化的,可共享的代码库。库不仅提供了API,还提供隐私单元:以下划线()开头的标识符只对内部库可见。每个Dartapp就是一个库,即使它不使用库指令。库可以分布式使用包。见 Pub Package and Asset Manager 中有关pub(SDK中的一个包管理器)。使用库使用 import 来指定如何从一个库命名空间用于其他库的范围。例如,Dart Web应用一般采用这个库 dart:html,可以这样导入: import ‘dart:html’;唯一需要 import 的参数是一个指向库的 URI。对于内置库,URI中具有特殊dart:scheme。对于其他库,你可以使用文件系统路径或package:scheme。包 package:scheme specifies libraries ,如pub工具提供的软件包管理器库。例如: import ‘dart:io’; import ‘package:mylib/mylib.dart’; import ‘package:utils/utils.dart’;指定库前缀如果导入两个库是有冲突的标识符,那么你可以指定一个或两个库的前缀。例如,如果 library1 和 library2 都有一个元素类,那么你可能有这样的代码: import ‘package:lib1/lib1.dart’; import ‘package:lib2/lib2.dart’ as lib2; // … var element1 = new Element(); // 使用lib1里的元素 var element2 = new lib2.Element(); // 使用lib2里的元素导入部分库如果想使用的库一部分,你可以选择性导入库。例如: // 只导入foo库 import ‘package:lib1/lib1.dart’ show foo; //导入所有除了foo import ‘package:lib2/lib2.dart’ hide foo;延迟加载库延迟(deferred)加载(也称为延迟(lazy)加载)允许应用程序按需加载库。下面是当你可能会使用延迟加载某些情况:为了减少应用程序的初始启动时间;执行A / B测试-尝试的算法的替代实施方式中;加载很少使用的功能,例如可选的屏幕和对话框。为了延迟加载一个库,你必须使用 deferred as 先导入它。import ‘package:deferred/hello.dart’ deferred as hello; 当需要库时,使用该库的调用标识符调用 LoadLibrary()。greet() async { await hello.loadLibrary(); hello.printGreeting(); }在前面的代码,在库加载好之前,await关键字都是暂停执行的。有关 async 和 await 见 asynchrony support 的更多信息。您可以在一个库调用 LoadLibrary() 多次都没有问题。该库也只被加载一次。当您使用延迟加载,请记住以下内容:延迟库的常量在其作为导入文件时不是常量。记住,这些常量不存在,直到迟库被加载完成。你不能在导入文件中使用延迟库常量的类型。相反,考虑将接口类型移到同时由延迟库和导入文件导入的库。Dart隐含调用LoadLibrary()插入到定义deferred as namespace。在调用LoadLibrary()函数返回一个Future。库的实现用 library 来来命名库,用part来指定库中的其他文件。 注意:不必在应用程序中(具有顶级main()函数的文件)使用library,但这样做可以让你在多个文件中执行应用程序。声明库利用library identifier(库标识符)指定当前库的名称:// 声明库,名ballgame library ballgame; // 导入html库 import ‘dart:html’; // …代码从这里开始… 关联文件与库添加实现文件,把part fileUri放在有库的文件,其中fileURI是实现文件的路径。然后在实现文件中,添加部分标识符(part of identifier),其中标识符是库的名称。下面的示例使用的一部分,在三个文件来实现部分库。第一个文件,ballgame.dart,声明球赛库,导入其他需要的库,并指定ball.dart和util.dart是此库的部分: library ballgame; import ‘dart:html’; // …其他导入在这里… part ‘ball.dart’; part ‘util.dart’; // …代码从这里开始…第二个文件ball.dart,实现了球赛库的一部分:part of ballgame; // …代码从这里开始…第三个文件,util.dart,实现了球赛库的其余部分:part of ballgame; // …Code goes here…重新导出库(Re-exporting libraries)可以通过重新导出部分库或者全部库来组合或重新打包库。例如,你可能有实现为一组较小的库集成为一个较大库。或者你可以创建一个库,提供了从另一个库方法的子集。 // In french.dart: library french; hello() => print(‘Bonjour!’); goodbye() => print(‘Au Revoir!’); // In togo.dart: library togo; import ‘french.dart’; export ‘french.dart’ show hello; // In another .dart file: import ’togo.dart’; void main() { hello(); //print bonjour goodbye(); //FAIL }异步的支持Dart 添加了一些新的语言特性用于支持异步编程。最通常使用的特性是 async 方法和 await 表达式。Dart 库大多方法返回 Future 和 Stream 对象。这些方法是异步的:它们在设置一个可能的耗时操作(比如 I/O 操作)之后返回,而无需等待操作完成当你需要使用 Future 来表示一个值时,你有两个选择。使用 async 和 await使用 Future API同样的,当你需要从 Stream 获取值的时候,你有两个选择。使用 async 和一个异步的 for 循环 (await for)使用 Stream API使用 async 和 await 的代码是异步的,不过它看起来很像同步的代码。比如这里有一段使用 await 等待一个异步函数结果的代码:await lookUpVersion()要使用 await,代码必须用 await 标记 checkVersion() async { var version = await lookUpVersion(); if (version == expectedVersion) { // Do something. } else { // Do something else. } }你可以使用 try, catch, 和 finally 来处理错误并精简使用了 await 的代码。 try { server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 4044); } catch (e) { // React to inability to bind to the port… }声明异步函数一个异步函数是一个由 async 修饰符标记的函数。虽然一个异步函数可能在操作上比较耗时,但是它可以立即返回-在任何方法体执行之前。 checkVersion() async { // … } lookUpVersion() async => / … */;在函数中添加关键字 async 使得它返回一个 Future,比如,考虑一下这个同步函数,它将返回一个字符串。String lookUpVersionSync() => ‘1.0.0’;如果你想更改它成为异步方法-因为在以后的实现中将会非常耗时-它的返回值是一个 Future 。Future<String> lookUpVersion() async => ‘1.0.0’;请注意函数体不需要使用 Future API,如果必要的话 Dart 将会自己创建 Future 对象使用带 future 的 await 表达式一个 await表达式具有以下形式await expression在异步方法中你可以使用 await 多次。比如,下列代码为了得到函数的结果一共等待了三次。 var entrypoint = await findEntrypoint(); var exitCode = await runExecutable(entrypoint, args); await flushThenExit(exitCode);在 await 表达式中, 表达式 的值通常是一个 Future 对象;如果不是,那么这个值会自动转为 Future。这个 Future 对象表明了表达式应该返回一个对象。await 表达式 的值就是返回的一个对象。在对象可用之前,await 表达式将会一直处于暂停状态。如果 await 没有起作用,请确认它是一个异步方法。比如,在你的 main() 函数里面使用await,main() 的函数体必须被 async 标记: main() async { checkVersion(); print(‘In main: version is ${await lookUpVersion()}’); }结合 streams 使用异步循环一个异步循环具有以下形式: await for (variable declaration in expression) { // Executes each time the stream emits a value. }表达式 的值必须有Stream 类型(流类型)。执行过程如下:在 stream 发出一个值之前等待执行 for 循环的主体,把变量设置为发出的值。重复 1 和 2,直到 Stream 关闭如果要停止监听 stream ,你可以使用 break 或者 return 语句,跳出循环并取消来自 stream 的订阅 。如果一个异步 for 循环没有正常运行,请确认它是一个异步方法。 比如,在应用的 main() 方法中使用异步的 for 循环时,main() 的方法体必须被 async 标记。 main() async { await for (var request in requestServer) { handleRequest(request); } } ...

December 17, 2018 · 14 min · jiezi

Flutter入坑分享

本文只适合初次接触 Flutter 的开发者。原文链接: http://blog.myweb.kim/flutter/Flutter%E5%85%A5%E5%9D%91%E5%88%86%E4%BA%AB/简介Flutter 是 Google 推出并开源的移动端开发框架(基于「Dart」语言)。使用 Flutter 开发的APP可以同时运行在 IOS 与 Android 平台上。并且 Flutter 默认带有 Material 风格 与 Cupertino 风格的主题包(前者Android,后者IOS),可以快速开发一个IOS 风格或者 Android 风格的…Demo…跨平台Flutter 不使用 WebView 也不使用操作系统的原生控件,而是自己有用一个 高性能 的渲染引擎,可以非常高效的进行组件绘制UI渲染。这样 Flutter 可以保证在 IOS 与 Android 上的UI表现一致性 ,开发者无需过多关注平台差异性上的问题。对于初创公司来说,前期节约开发成本就是最好的融资。。。高性能与 React Native (以下简称RN)的跨平台不同的是,RN是会将JS编写的对应组件转换为原生组件去渲染,而 Flutter 是基于最底层 Skia 的图形库去渲染(我觉得有点类似于 DOM 中的 canvas , 从平台上得到一个画布,自己在画布上去渲染),所有的渲染都有 Skia 来完成。Skia 延伸…Flutter使用Skia作为其2D渲染引擎,Skia是Google的一个2D图形处理函数库,包含字型、坐标转换,以及点阵图都有高效能且简洁的表现,Skia是跨平台的,并提供了非常友好的API,目前Google Chrome浏览器和Android均采用Skia作为其绘图引擎,值得一提的是,由于Android系统已经内置了Skia,所以Flutter在打包APK(Android应用安装包)时,不需要再将Skia打入APK中,但iOS系统并未内置Skia,所以构建iPA时,也必须将Skia一起打包,这也是为什么Flutter APP的Android安装包比iOS安装包小的主要原因。正是因为基于自己的渲染机制,不需要与原生平台之间频繁通信,才体现出来他的高效率、高性能。Flutter 的布局、渲染都是 Dart 直接控制,在一些交互中,比如滑动的时候它的高性能就会体现出来。而RN在这方面的渲染则是与原生平台进行通信,不断的进行信息同步,这部分的开销放到手机上还是很大的。而且在渲染层,Flutter 底层也有一个类似虚拟DOM的组件,在UI进行变化后,会进行diff算法。开发高效率Flutter 在开发的时候有一个特点,热重载。 就像在webpack 与 浏览器,在编辑器中保存后,界面立马就能看到变化。Flutter 也是这样,当将 APP 在虚拟容器中或者真机设备中调试时,保存后,APP会立刻响应。节省了大量时间。Dart 初步了解因为 Flutter 是基于 Dart 语言开发的,所以我们多多少少也要了解下 Dart 这玩意怎么写,他的语法与结构是个怎样的。虽然官网的 Demo 有提到说:「如果您熟悉面向对象和基本编程概念(如变量、循环和条件控制),则可以完成本教程,您无需要了解Dart或拥有移动开发的经验。」emmmm… 纯属扯淡…如果不了解 Dart,那也仅限于看 Demo 是怎么写的…Dart 出自Google。是一种面向对象编程的强类型语言,语法有点像 Java 与 JavaScript 的集合体。官方学习资料以下是使用 Flutter 需要掌握的 Dart 基础语法:(以下内容摘抄来至 官网文档 , 没必要细看,可快速的过一遍,只做了解。)变量声明var类似于JavaScript中的var,它可以接收任何类型的变量,但最大的不同是Dart中var变量一旦赋值,类型便会确定,则不能再改变其类型,如:var t;t=“hi world”;// 下面代码在dart中会报错,应为变量t的类型已经确定为String,// 类型一旦确定后则不能再更改其类型。t=1000;上面的代码在JavaScript是没有问题的,前端开发者需要注意一下,之所以有此差异是因为Dart本身是一个强类型语言,任何变量都是有确定类型的,在Dart中,当用var声明一个变量后,Dart在编译时会根据第一次赋值数据的类型来推断其类型,编译结束后其类型就已经被确定,而JavaScript是纯粹的弱类型脚本语言,var只是变量的声明方式而已。dynamic和ObjectDynamic和Object 与 var功能相似,都会在赋值时自动进行类型推断,不同在于,赋值后可以改变其类型,如:dynamic t;t=“hi world”;//下面代码没有问题t=1000;Object 是dart所有对象的根基类,也就是说所有类型都是Object的子类,所以任何类型的数据都可以赋值给Object声明的对象,所以表现效果和dynamic相似。final和const如果您从未打算更改一个变量,那么使用 final 或 const,不是var,也不是一个类型。 一个 final 变量只能被设置一次,两者区别在于:const 变量是一个编译时常量,final变量在第一次使用时被初始化。被final或者const修饰的变量,变量类型可以省略,如://可以省略String这个类型声明final str = “hi world”;//final str = “hi world”; const str1 = “hi world”;//const String str1 = “hi world”;函数Dart是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型Function。这意味着函数可以赋值给变量或作为参数传递给其他函数,这是函数式编程的典型特征。函数声明bool isNoble(int atomicNumber) { return _nobleGases[atomicNumber] != null;}dart函数声明如果没有显示申明返回值类型时会默认当做dynamic处理,注意,函数返回值没有类型推断:typedef bool CALLBACK();//不指定返回类型,此时默认为dynamic,不是boolisNoble(int atomicNumber) { return _nobleGases[atomicNumber] != null;}void test(CALLBACK cb){ print(cb()); }//报错,isNoble不是bool类型test(isNoble);对于只包含一个表达式的函数,可以使用简写语法bool isNoble (int atomicNumber )=> _nobleGases [ atomicNumber ] != null ; 函数作为变量var say= (str){ print(str);};say(“hi world”);函数作为参数传递void execute(var callback){ callback();}execute(()=>print(“xxx”))可选的位置参数包装一组函数参数,用[]标记为可选的位置参数:String say(String from, String msg, [String device]) { var result = ‘$from says $msg’; if (device != null) { result = ‘$result with a $device’; } return result;}下面是一个不带可选参数调用这个函数的例子:say(‘Bob’, ‘Howdy’); //结果是: Bob says Howdy下面是用第三个参数调用这个函数的例子:say(‘Bob’, ‘Howdy’, ‘smoke signal’); //结果是:Bob says Howdy with a smoke signal可选的命名参数定义函数时,使用{param1, param2, …},用于指定命名参数。例如://设置[bold]和[hidden]标志void enableFlags({bool bold, bool hidden}) { // … }调用函数时,可以使用指定命名参数。例如:paramName: valueenableFlags(bold: true, hidden: false);可选命名参数在Flutter中使用非常多。异步支持Dart类库有非常多的返回Future或者Stream对象的函数。 这些函数被称为异步函数:它们只会在设置好一些需要消耗一定时间的操作之后返回,比如像 IO操作。而不是等到这个操作完成。async和await关键词支持了异步编程,运行您写出和同步代码很像的异步代码。FutureFuture与JavaScript中的Promise非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。由于本身功能较多,这里我们只介绍其常用的API及特性。还有,请记住,Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。Future.then为了方便示例,在本例中我们使用Future.delayed 创建了一个延时任务(实际场景会是一个真正的耗时任务,比如一次网络请求),即2秒后返回结果字符串"hi world!",然后我们在then中接收异步结果并打印结果,代码如下:Future.delayed(new Duration(seconds: 2),(){ return “hi world!”;}).then((data){ print(data);});Future.catchError如果异步任务发生错误,我们可以在catchError中捕获错误,我们将上面示例改为:Future.delayed(new Duration(seconds: 2),(){ //return “hi world!”; throw AssertionError(“Error”); }).then((data){ //执行成功会走到这里 print(“success”);}).catchError((e){ //执行失败会走到这里 print(e);});在本示例中,我们在异步任务中抛出了一个异常,then 的回调函数将不会被执行,取而代之的是 catchError回调函数将被调用;但是,并不是只有 catchError回调才能捕获错误,then方法还有一个可选参数onError,我们也可以它来捕获异常:Future.delayed(new Duration(seconds: 2), () { //return “hi world!”; throw AssertionError(“Error”);}).then((data) { print(“success”);}, onError: (e) { print(e);});Future.whenComplete有些时候,我们会遇到无论异步任务执行成功或失败都需要做一些事的场景,比如在网络请求前弹出加载对话框,在请求结束后关闭对话框。这种场景,有两种方法,第一种是分别在then或catch中关闭一下对话框,第二种就是使用Future的whenComplete回调,我们将上面示例改一下:Future.delayed(new Duration(seconds: 2),(){ //return “hi world!”; throw AssertionError(“Error”);}).then((data){ //执行成功会走到这里 print(data);}).catchError((e){ //执行失败会走到这里 print(e);}).whenComplete((){ //无论成功或失败都会走到这里});Future.wait有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作,比如我们有一个界面,需要先分别从两个网络接口获取数据,获取成功后,我们需要将两个接口数据进行特定的处理后再显示到UI界面上,应该怎么做?答案是Future.wait,它接受一个Future数组参数,只有数组中所有Future都执行成功后,才会触发then的成功回调,只要有一个Future执行失败,就会触发错误回调。下面,我们通过模拟Future.delayed 来模拟两个数据获取的异步任务,等两个异步任务都执行成功时,将两个异步任务的结果拼接打印出来,代码如下:Future.wait([ // 2秒后返回结果 Future.delayed(new Duration(seconds: 2), () { return “hello”; }), // 4秒后返回结果 Future.delayed(new Duration(seconds: 4), () { return " world"; })]).then((results){ print(results[0]+results[1]);}).catchError((e){ print(e);});执行上面代码,4秒后你会在控制台中看到“hello world”。Async/awaitDart中的async/await 和JavaScript中的async/await功能和用法是一模一样的,如果你已经了解JavaScript中的async/await的用法,可以直接跳过本节。回调地狱(Callback hell)如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现Future.then回调中套回调情况。举个例子,比如现在有个需求场景是用户先登录,登录成功后会获得用户Id,然后通过用户Id,再去请求用户个人信息,获取到用户个人信息后,为了使用方便,我们需要将其缓存在本地文件系统,代码如下://先分别定义各个异步任务Future<String> login(String userName, String pwd){ … //用户登录};Future<String> getUserInfo(String id){ … //获取用户信息 };Future saveUserInfo(String userInfo){ … // 保存用户信息 }; 接下来,执行整个任务流:login(“alice”,"").then((id){ //登录成功后通过,id获取用户信息 getUserInfo(id).then((userInfo){ //获取用户信息后保存 saveUserInfo(userInfo).then((){ //保存用户信息,接下来执行其它操作 … }); });})可以感受一下,如果业务逻辑中有大量异步依赖的情况,将会出现上面这种在回调里面套回调的情况,过多的嵌套会导致的代码可读性下降以及出错率提高,并且非常难维护,这个问题被形象的称为回调地狱(Callback hell)。回调地狱问题在之前JavaScript中非常突出,也是JavaScript被吐槽最多的点,但随着ECMAScript6和ECMAScript7标准发布后,这个问题得到了非常好的解决,而解决回调地狱的两大神器正是ECMAScript6引入了Promise,以及ECMAScript7中引入的async/await。 而在Dart中几乎是完全平移了JavaScript中的这两者:Future相当于Promise,而async/await连名字都没改。接下来我们看看通过Future和async/await如何消除上面示例中的嵌套问题。使用Future消除callback helllogin(“alice”,"").then((id){ return getUserInfo(id);}).then((userInfo){ return saveUserInfo(userInfo);}).then((e){ //执行接下来的操作 }).catchError((e){ //错误处理 print(e);});正如上文所述, “Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用” ,如果在then中返回的是一个Future的话,该future会执行,执行结束后会触发后面的then回调,这样依次向下,就避免了层层嵌套。使用async/await消除callback hell通过Future回调中再返回Future的方式虽然能避免层层嵌套,但是还是有一层回调,有没有一种方式能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式?答案是肯定的,这就要使用async/await了,下面我们先直接看代码,然后再解释,代码如下:task() async { try{ String id = await login(“alice”,"******"); String userInfo = await getUserInfo(id); await saveUserInfo(userInfo); //执行接下来的操作 } catch(e){ //错误处理 print(e); } }async用来表示函数是异步的,定义的函数会返回一个Future对象,可以使用then方法添加回调函数。await 后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走;await必须出现在 async 函数内部。可以看到,我们通过async/await将一个异步流用同步的代码表示出来了。其实,无论是在JavaScript还是Dart中,async/await都只是一个语法糖,编译器或解释器最终都会将其转化为一个Promise(Future)的调用链。StreamStream 也是用于接收异步事件数据,和Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件而传递结果数据或错误异常。 Stream 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:Stream.fromFutures([ // 1秒后返回结果 Future.delayed(new Duration(seconds: 1), () { return “hello 1”; }), // 抛出一个异常 Future.delayed(new Duration(seconds: 2),(){ throw AssertionError(“Error”); }), // 3秒后返回结果 Future.delayed(new Duration(seconds: 3), () { return “hello 3”; })]).listen((data){ print(data);}, onError: (e){ print(e.message);},onDone: (){});上面的代码依次会输出:I/flutter (17666): hello 1I/flutter (17666): ErrorI/flutter (17666): hello 3代码很简单,就不赘述了。思考题:既然Stream可以接收多次事件,那能不能用Stream来实现一个订阅者模式的事件总线?总结通过上面介绍,相信你对Dart应该有了一个初步的印象,由于笔者平时也使用Java和JavaScript,下面笔者根据自己的经验,结合Java和JavaScript,谈一下自己的看法。之所以将Dart与Java和JavaScript对比,是因为,这两者分别是强类型语言和弱类型语言的典型代表,并且Dart 语法中很多地方也都借鉴了Java和JavaScript。Dart vs Java客观的来讲,Dart在语法层面确实比Java更有表现力;在VM层面,Dart VM在内存回收和吞吐量都进行了反复的优化,但具体的性能对比,笔者没有找到相关测试数据,但在笔者看来,只要Dart语言能流行,VM的性能就不用担心,毕竟Google在go(没用vm但有GC)、javascript(v8)、dalvik(android上的java vm)上已经有了很多技术积淀。值得注意的是Dart在Flutter中已经可以将GC做到10ms以内,所以Dart和Java相比,决胜因素并不会是在性能方面。而在语法层面,Dart要比java更有表现力,最重要的是Dart对函数式编程支持要远强于Java(目前只停留在lamda表达式),而Dart目前真正的不足是生态,但笔者相信,随着Futter的逐渐火热,会回过头来反推Dart生态加速发展,对于Dart来说,现在需要的是时间。Dart vs JavaScriptJavaScript的弱类型一直被抓短,所以TypeScript、Coffeescript甚至是Facebook的flow(虽然并不能算JavaScript的一个超集,但也通过标注和打包工具提供了静态类型检查)才有市场。就笔者使用过的脚本语言中(笔者曾使用过Python、PHP),JavaScript无疑是动态化支持最好的脚本语言,比如在JavaScript中,可以给任何对象在任何时候动态扩展属性,对于精通JavaScript的高手来说,这无疑是一把利剑。但是,任何事物都有两面性,JavaScript的强大的动态化特性也是把双刃剑,你可经常听到另一个声音,认为JavaScript的这种动态性糟糕透了,太过灵活反而导致代码很难预期,无法限制不被期望的修改。毕竟有些人总是对自己或别人写的代码不放心,他们希望能够让代码变得可控,并期望有一套静态类型检查系统来帮助自己减少错误。正因如此,在Flutter中,Dart几乎放弃了脚本语言动态化的特性,如不支持反射、也不支持动态创建函数等。并且Dart在2.0强制开启了类型检查(Strong Mode),原先的检查模式(checked mode)和可选类型(optional type)将淡出,所以在类型安全这个层面来说,Dart和TypeScript、Coffeescript是差不多的,所以单从这一点来看,Dart并不具备什么明显优势,但综合起来看,dart既能进行服务端脚本、APP开发、web开发,这就有优势了!官方PPT宣传截图Flutter 底层架构的一个大概示意图:Material 和 Cupertino 是 Flutter 官方提供的两个不同的 UI 风格组件库(前者Android,后者IOS)。在 Flutter 中,一切皆是 Widget 。 一个按钮是 Widget,一段文字也是 Widget,一个图片也是 Widget,一个路由导航 也是 Widget。所以前期接触 Flutter 可以先学习这两个UI库如何使用即可。(个人见解)基础组件库Material 组件库Cupertino 组件库搭建开发环境windows上的搭建macOS上的搭建linux上的搭建搭建过程很简单,下载 SDK 包,然后配置下环境变量就ok了。编辑器推荐VScode,轻巧、简洁。配置好 Flutter环境,只需要在安装一个 Flutter 插件就好了。官方配置教程第一个Demo在 VScode 中安装好插件后,按下shift + command + p 输入 flutter ,选择 New Project。第一次创建时可能需要选择 Flutter SDK 的位置。下面的Demo是官网上的给出的代码,整理出来的一个完整的。先在 pubspec.yaml 中添加一个依赖: english_words 它是 Dart 语言编写的一个随机生成英文单词的工具包。pubspec.yaml 是 Flutter 配置文件,可以理解为 npm 中的 package.json找到文件的第21行:dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 # 在这里添加 版本号遵循 语义化(Semantic Versioning) english_words: ^3.1.5dev_dependencies: flutter_test: sdk: flutterFlutter 有一个官方的包管理平台,pub.dartlang.org 类似于npm添加完成后,在控制台输入flutter packages get 或者在编辑器中右键点击 pubspes.yaml 选择 Get Packages也就是安装新的依赖。替换Demo代码这个Demo是一个随机生成英文名字的程序,有一个可以无限滚动的列表,可以让用户对喜欢的名字进行红心标记搜藏,然后点击右上角,可以查看已收藏的名字(路由跳转来实现的)。将lib/main.dart 中的所有代码删除,替换成下面的代码:下面的代码是将官网Demo中的代码整理好的,可以先不去管它什么样的结果或者具体每句代码什么意思,先将Demo在模拟器中跑起来再说。import ‘package:flutter/material.dart’;import ‘package:english_words/english_words.dart’;// 程序入口void main() => runApp(new MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: ‘Startup Name Generator’, home: new RandomWords(), theme: new ThemeData( primaryColor: Colors.white, ), ); }}class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState();}class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; final _saved = new Set<WordPair>(); final _biggerFont = const TextStyle(fontSize: 18.0); @override Widget build(BuildContext context) { return new Scaffold ( appBar: new AppBar( title: new Text(‘Startup Name Generator’), actions: <Widget>[ new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved), ], ), body: _buildSuggestions(), ); } void _pushSaved() { Navigator.of(context).push( new MaterialPageRoute( builder: (context) { final tiles = _saved.map( (pair) { return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), ); }, ); final divided = ListTile .divideTiles( context: context, tiles: tiles, ) .toList(); return new Scaffold( appBar: new AppBar( title: new Text(‘Saved Suggestions’), ), body: new ListView(children: divided), ); }, ) ); } Widget _buildRow(WordPair pair) { final alreadySaved = _saved.contains(pair); return new ListTile( title: new Text( pair.asPascalCase, style: _biggerFont, ), trailing: new Icon( alreadySaved ? Icons.favorite : Icons.favorite_border, color: alreadySaved ? Colors.red : null, ), onTap: () { setState(() { if (alreadySaved) { _saved.remove(pair); } else { _saved.add(pair); } }); }, ); } Widget _buildSuggestions() { return new ListView.builder( padding: const EdgeInsets.all(16.0), // 对于每个建议的单词对都会调用一次itemBuilder,然后将单词对添加到ListTile行中 // 在偶数行,该函数会为单词对添加一个ListTile row. // 在奇数行,该行书湖添加一个分割线widget,来分隔相邻的词对。 // 注意,在小屏幕上,分割线看起来可能比较吃力。 itemBuilder: (context, i) { // 在每一列之前,添加一个1像素高的分隔线widget if (i.isOdd) return new Divider(); // 语法 “i ~/ 2” 表示i除以2,但返回值是整形(向下取整),比如i为:1, 2, 3, 4, 5 // 时,结果为0, 1, 1, 2, 2, 这可以计算出ListView中减去分隔线后的实际单词对数量 final index = i ~/ 2; // 如果是建议列表中最后一个单词对 if (index >= _suggestions.length) { // …接着再生成10个单词对,然后添加到建议列表 _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); } ); }}选择调试 -> 启动调试 然后选择 ios emulator , 等待启动即可。(这个是macOS上的操作,windows只能选择Android的模拟器,当前所有的前提是你的 Flutter 环境确保搭建成功了。)运行成功后如下图所示:官方学习资料链接Flutter 中文网Flutter 实战以上,致那颗骚动的心…… ...

December 12, 2018 · 4 min · jiezi

10分钟读懂阿里巴巴高级专家在Flutter Live2018的分享

12月4日,google flutter团队宣布第一个flutter正式版本发布。次日,Flutter Live Beijing 会议上,google flutter团队邀请了在这一技术方案中重要的合作伙伴闲鱼团队分享这半年以来的通过flutter产出的业务结果以及对应的技术挑战。本文根据Flutter Live Beijing嘉宾闲鱼客户端团队负责人于佳(宗心)的演讲内容进行整理,从flutter的优势和挑战引出闲鱼这半年来针对flutter基础设施进行重新的构建,定义,以及优化的过程,最后是这半年来对社区的一些贡献和未来的规划。Flutter的优势与挑战众所周知,Flutter提供了一套解决方案,既能用原生ARM代码直接调用的方式来加速图形渲染和UI绘制,又能同时运行在两大主流移动操作系统上,其像素级别的还原,保证了不同平台的UI强一致性。同时其提供了一整套机制(hot reload/attach Debug)保证开发的高效,基于此闲鱼团队在众多跨平台方案中选择了Flutter作为其未来主要的开发方案。从4月份开始尝试在业务侧接入flutter到现在,闲鱼在线上已经有10+的页面使用了flutter进行开发,其中覆盖了核心主链路发布和详情。闲鱼目前是市场上最大的闲置交易社区,作为一款有巨大用户体量的C端创新类产品,我们对体验以及研发迭代效率都有比较高的要求。在享受flutter带来的收益的过程中,同样会面临技术转型过程中的一些挑战。主要的挑战来源于以下的三个方面工程体系在现有工程体系下如何将flutter体系融入,并保持团队不同技术栈(Android/iOS/Flutter)的同学能各自独立高效进行开发。业务架构如何提供一套flutter之上的业务架构,保证上层代码的统一标准,同时尽可能的使得代码的复用度及隔离性更好。基础中间件如何保证不同技术栈背后使用的基础能力是一致的(底层统一使用具有相同优化策略的图片库/音视频库),且在这个过程中如何解决flutter融入后产生的问题。面对这些挑战,闲鱼团队在下半年开始了针对基础设施的改造与重建。基础设施重建之路工程体系工程体系部分,首当其冲需要考虑的是不同技术栈同学的协同问题,举例说明,我们的详情和发布页面是flutter的,而首页以及搜索部分目前暂时采用native进行开发。这就需要考虑到flutter的环境要对开发native的同学透明,甚至在native同学没有安装flutter环境的情况下,依然可以保持原来的方式进行开发native页面。如图中所示,以iOS为例,我们针对flutter的框架flutter.framewrok和业务代码App.framework通过持续集成服务进行打包并自动上传至云端的pod repo上,native同学只需在Podfile内指定对应的两个库的版本即可,同理,针对flutter的plugin代码,同样打包上传至pod repo即可。这套体系整体不复杂,需要说明的是,由于多人开发flutter工程,因此打包是一件非常频繁的事情,因此我们这半年构建了持续集成体系来帮助大家将打包上传等整个体系做成一键式服务,另外,由于原有iOS平台的flutter产物是需要依赖我们的native主工程的代码的,这种默认的打包方式,代码量巨大,造成持续集成时间在10-20分钟不等,因此在这个过程中,闲鱼团队通过直接基于xcode_backend.sh + insert_dylib的自定义脚本完成了不依赖native主工程源码的打包,将持续集成时间降至2分半。同理在android上面,也进行了一些基础的改造,感兴趣的同学可以给我们留言,我们会根据大家的需求程度在后续安排贡献给flutter社区。另外一部分比较重要的内容是混合栈相关的,由于flutter没有提供flutter到混合工程的最佳实践,所以我们在上半年自建了一整套混合栈的体系,这里主要是分享一些混合栈的关键思考,在混合栈的实现过程中,需重点测试验证dart这一侧widget的生命周期,并简化堆栈的管理(目前闲鱼的做法是将堆栈管理统一交由native进行控制,简化Dart层API),并需要考虑如何兼容Dart上层的比如Navigtor API的调用。整体这部分闲鱼团队还在验证当中,总之,这部分看似简单,但实际是比较深的坑,需要重点优化。另外,截至发文时间前,我们跟google flutter团队就混合栈交换了一些看法,flutter团队后续如果可以提供多flutterview,单flutter engine的基础能力,就可以使得闲鱼现有的混合栈实现成本整体大幅度降低,后续大家有什么特别好的建议,也欢迎跟我们进行交流。业务架构今年下半年由于有更多的业务迁移至flutter,这意味着更多的团队成员开始了Dart侧的研发。很快我们发现团队的代码风格,层次结构都比较混乱,bug也层出不穷,因此我们需要找到一种可以保证大家研发规范,同时确保多人协同过程中,代码既能更好的复用,又可以在适当的场景下做到相互隔离的这么一种方案。在经过社区的多个框架库的实践和比较以后,不管是flex还是redux都不能完全解决我们的问题。最后我们选择了自己进行设计和实现,我们对框架进行基础分层以后,将重点最终落在了基于单一数据源的组件化框架上面,因此我们产生了自己的框架fishRedux,我们严格参考标准js的redux规范和源码(redux的设计三原则)进行了完整的dart侧的实现,并在此基础上提供扩展能力用于我们的组件化开发。如图所示,component将redux中的view,reducer,middleware以及我们的扩展能力effect进行组装,从而可以在不同的页面进行组件的复用,当然,全局依然遵循redux的单一数据源的原则,但我们将逻辑本身通过更细粒度的拆解,保证了这些逻辑在不同的component组装下都可以尽可能的进行复用。基于这种结构,我们可以将任意的component进行挂载和拼装,通过更多小粒度的组件,产生不同场景下的复杂页面。另外,针对于component的多层组装,大家可以细看下dependents这个字段,通过基于这个字段的组装,在我们提供的这段代码里面,实际上是提供了一个详情页面的插槽的功能,详情页面目前在闲鱼有近10种不同的组合,在这个场景下,可以在保证组件可以服用的同时,做到不同流程下的代码隔离。我们只要针对dependents的components里面进行替换,就可以很容易的达到在详情页面插入不同widget以及逻辑的效果。fishRedux框架目前已经接近修改的尾声,目前还有部分微调和文档的补充,明年4月份前,我们有计划进行该框架的开源,为后续业务架构提供一个新的标准,大家敬请期待。基础中间件在阿里集团内部,已经产出了较多的基础中间件,因此如何复用这个中间件到flutter侧是一个新的挑战,针对于传统的比如网络库,crash收集等中间件,由于不涉及到UI的复用,相对容易,但针对音视频,图片库等这类的中间件,虽然flutter提供了flutterTexture的方案,但依然不是特别完美。我们在做音视频及图片库的复用过程中,主要的问题在于flutter原生提供的机制,针对图片的渲染存在GPU到CPU,然后CPU再到GPU的这样一个过程,如图所示。根本原因在于不同的glContext无法共享texture。因此,我们目前采取的方案是修改flutter引擎,并暴露出glContext的shareGroup(以iOS为例)。目前整个方案已经上线。由于该改动目前在闲鱼自己fork的engine里面,因此目前将我们之前踩到的一些坑同步给大家,如果大家有在flutter和native侧同时使用音视频的情况,建议特别注意ppt中的前两点,否则会造成flutter侧或者native侧音视频的错乱。当然如果按照闲鱼团队的提供的修改方案进行engine改造后,也可以通过第三点,对native设置跟flutter相同的sharegroup来解决这个问题。在flutter live Beijing结束之后,我也将该方案正式介绍给google flutter团队,希望后续能将类似的功能融入flutter的官方实现。闲鱼与flutter社区闲鱼这半年,对于flutter社区,也有一些小小的贡献。我们针对flutter的方案进行整理并在各个技术社区进行传播。另外我们将已有的一些问题和解决方案提供给google flutter官方团队,直接或者间接的推动了flutter的整体进度,并改变了这个技术未来的部分走向。我为自己的团队感到由衷的骄傲,但同时我意识到,要想让flutter成为终端未来的主流技术,依然任重道远,因此我们后续也会将目前的一些相对稳定的框架和解决方案,逐步开源到社区,我们的要求是,至少闲鱼团队需要在线上有应用和验证。目前我们已经有一些初步的demo和小工具放在上面,大家感兴趣的可以往我们的github上提issue,我们后续会逐步开放更多的代码。最近会公布的比较重要的框架会是fishRedux,因此请大家持续关注我们。总结与展望我们首先带大家回顾了flutter带来的优势以及在闲鱼的实际例子,并引出在复杂工程下的一些挑战。我们针对这些挑战,在下半年进行了整个体系的重新建设,初步完成了隔离的混合工程体系统一标准的业务架构高效复用基础中间件设施本次的分享,其实只是我们目前团队的一部分内容,我们基于flutter和dart还有更多的技术方案目前在预研和研发中,所以没有在这次live中进行宣讲。在后续跟google flutter团队的沟通中也了解到,他们对我们的多个方案都有较大的兴趣。对于未来来说,一方面,除了本文分享的内容以外,我们自己在代码自动生成/Dart Server/线上问题自动回放/国际化/动态模版等/持续集成等多个方面都在持续关注和调研。另一方面,在flutter 1.0公布后,类似hummingbrid这一类全新的方案也有机会让flutter具有全终端制霸的可能性,我们也会持续跟进和进行尝试。Anyway,依然希望有更多的同学可以加入我们一起完善flutter的生态,同时通过新的技术手段,让天下没有闲置。来闲鱼一起改变世界吧,少年!PPT下载:https://yq.aliyun.com/download/3130本文作者:闲鱼技术-宗心.阅读原文本文为云栖社区原创内容,未经允许不得转载。

December 11, 2018 · 1 min · jiezi

做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现

背景在端上为了提升App的灵活性, 快速解决万变的业务需求,开发者们探索了多种解决方案,如PhoneGap ,React Native ,Weex等,但在Flutter生态还没有好的解决方案。未来闲鱼都会基于Flutter 来跨端开发,如果突破发版周期,在不发版的情况下,完成业务需求,同时能兼容性能体验,无疑是更快的响应了业务需求。因此我们需要探索在Flutter生态下的动态化。方案选择借鉴Android 和Ios上的动态性方案,我们也思考了多种Flutter动态性方案。1.下载替换Flutter编译产物下载新的Flutter编译产物,替换 App 安装目录下的编译产物,来实现动态化,这在Android 端是可行的,但在Ios 端不可行。我们需要双端一体的解决方案,所以这不是最好选择。2.类似React Native 框架我们先来看看React Native 的架构React Native 要转为android(ios) 的原生组件,再进行渲染。用React Native的设计思路,把XML DSL转为Flutter 的原子widget组件,让Flutter 来渲染。技术上说是可行的,但这个成本很大,这会是一个庞大的工程,从投入产出比看,不是很好的选择3.页面动态组件框架由粗粒度的Widget组件动态拼装出页面,Native端已经有很多成熟的框架,如天猫的Tangram,淘宝的DinamicX,它在性能、动态性,开发周期上取得较好平衡。关键它能满足大部分的动态性需求,能解决问题。三种方案的比较图表如:根据实际动态性需求,从两端一致性,和性能,成本,动态性考虑,我们选择一个折中方案,页面动态组件的设计思路是一个不错的选择。页面动态组件框架在Flutter上使用粗力度的组件动态拼装来构建页面,需要一整套的前后端服务和工具。本文我们重点介绍前端界面渲染引擎过程。语法树的选择Native端的Tangram ,DinamicX等框架他们有个共同点,都是Xml或者Html 做为DSL。但是Flutter 是React Style语法。他自己的语法已经能很好的表达页面。无需要自定义的Xml 语法,自定义的逻辑表达式。用Flutter 源码做为DSL 能大大减轻开发,测试过程,不需要额外的工具支持。所以选择了Flutter 源码作为DSL,来实现动态化。如何解析DSLFlutter源码做为DSL,那我们需要对源码进行很好的解析和分析。Flutter analyzer给了我们一些思路,Flutter analyzer是一个代码风格检测工具。它使用package:analyzer来解析dart 源码,拿到ASTNode。看下Flutter analyze 源码结构,它使用了dart sdk 里面的 package:analyzerdart-sdk: analysis_server: analysis_server.dart handleRequest(Request request) analyzer: parseCompilationUnit() parseDartFile parseDirectives Flutter analyze 解析源码得到ASTNode过程。插件或者命令对analysis server发起请求,请求中带需要分析的文件path,和分析的类型,analysis_server经过使用 package:analyzer 获取 commilationUnit (ASTNode),再对astNode,经过computer分析,返回一个分析结果list。同样我们也可以把使用 package:analyzer 把源文件转换为commilationUnit (ASTNode),ASTNode是一个抽象语法树,抽象语法树(abstract syntax tree或者缩写为AST)是源代码的抽象语法结构的树状表现形式.所有利用抽象语法树能很好的解析dart 源码。解析渲染引擎下面重点介绍渲染模块架构图:1.源码解析过程1.AST树的结构如下面这段Flutter组件源码:import ‘package:flutter/material.dart’;class FollowedTopicCard extends StatelessWidget { @override Widget build(BuildContext context) { return new Container( padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0.0), child: new InkWell( child: new Center( child: const Text(‘Plugin example app’), ), onTap: () {}, ), ); }}它的AST结构:从AST结构看,他是有规律的.2.AST 到widget Node我们拿到了ASTNode,但ASTNode 和widget node tree 完全是两个不一样的概念,需要递归ASTNode 转化为 widget node tree.widget Node 需要的元素用Name 来记录是什么类型的widgetwidget的arguments放在 map里面widget 的literals 放在list 里面widget 的children 放在lsit 里面widget 的触发事件 函数map里面widget node 加fromjson ,tojson 方法可以在递归astNode tree 时候,识别InstanceCreationExpression来创建一个widget node。2.组件数据渲染框架sdk 中注册支持的组件,组件包括:a.原子组件:Flutter sdk 中的 Flutter 的widgetb.本地组件:本地写好到一个大颗粒的组件,卡片widget组件c.逻辑组件:本地包装了逻辑的widget组件d.动态组件:通过源码dsl动态渲染的widget具体代码如下: const Map<String, CreateDynamicApi> allWidget = <String, CreateDynamicApi>{ ‘Container’: wrapContainer, ………….} static Widget wrapContainer(Map<String, dynamic> pars) { return new Container( padding: pars[‘padding’], color: pars[‘color’], child: pars[‘child’], decoration: pars[‘decoration’], width: pars[‘width’], height: pars[‘height’], alignment: pars[‘alignment’] );}一般我们通过网络请求拿到的数据是一个map。比如源码中写了这么一个 ‘${data.urls[1]}‘AST 解析时候,拿到这么一个string,或者AST 表达式,通过解析它 ,肯定能从map 中拿到对应的值。3.逻辑和事件a.支持逻辑Flutter 概念万物都是widget ,可以把表达式,逻辑封装成一个自定义widget。如果在源码里面写了if else,变量等,会加重sdk解析的过程。所以把逻辑封装到widget中。这些逻辑widget,当作组件当成框架组件。b.支持事件把页面跳转,弹框,等服务,注册在sdk里面。约定使用者仅限sdk 的服务。4.规则和检测工具a.检测规则需要对源码的格式制定规则。比如不支持 直接写if else ,需要使用逻辑wiget组件来代替if else 语句。如果不制定规则,那ast Node 到widget node 的解析过程会很复杂。理论上都可以解析,只要解析sdk 够强大。制定规则,可以减轻sdk的解析逻辑。b.工具检测用工具来检测源码是否符合制定的规则,以保证所有的源码都能解析出来。性能和效果帧率大于50fps,体验上看比weex相同功能的页面更加流畅,Samsung galaxy s8上,感觉不出组件是通过动态渲染的.数据结构服务端请求到的数据,我们可以约定一种格式如下:class DataModel { Map<dynamic, dynamic> data; String type;}每个page 都是由组件组成的,每个组件的数据都是 DataModel来渲染。根据type 来找到对应的模版,模版+data,渲染出界面。动态模版管理模块我们把Widget Node Tree 转换为一个组件Json模版,它需要一套管理平台,来支持版本控制,动态下载,升级,回滚,更新等。框架的边界该框架是通过组件的组装,组件布局动态变更,页面布局动态变更来实现动态化。所以它适合运营变化较快的首页,详情,订单,我的等页面。一些复杂的逻辑需要封装在组件里面,把组件内置到框架中,当作本地组件。框架侧重于动态组件的组装,而引擎对于源码复杂的逻辑表达式的解析是弱化的。后续拓展1.和UI自动化的结合UI自动化 ,前面已经有文章介绍。UI自动化工具生成组件,再组件转为模版,动态下发,来快速解决运营需求。2.国际化的支持App在不同国家会有不同的功能,我们可以根据区域,来动态拼装我们的页面。3.千人千面根据不同的人群,来动态渲染不一样的界面。总结本文介绍动态化方案的渲染部分。该方案都在初探阶段,还有很多需要完善,后续会继续扩展和修改,等达到开源标准后,会考虑开源。动态方案是一个后端前端一体的方案,需要一整套工具配合,后续会有文章继续介绍整体的动态化方案。敬请关注闲鱼技术公共账号,也邀请您加入闲鱼一起探索有意思的技术。参考资料:Static Analysis:https://www.dartlang.org/guides/language/analysis-optionshttps://www.dartlang.org/tools/analyzerdart analyzer :https://pub.dartlang.org/packages/analyzerhttps://github.com/dart-lang/sdk/tree/master/pkg/analyzer_cli#dartanalyzerdartdevc:https://webdev.dartlang.org/tools/dartdevc本文作者:闲鱼技术-石磬阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 5, 2018 · 1 min · jiezi

关于Flutter初始化流程,我必须告诉你的是...

引言最近在做性能优化的时候发现,在混合栈开发中,第一次启动Flutter页面的耗时总会是第二次启动Flutter页面耗时的两倍左右,这样给人感觉很不好。分析发现第一次启动Flutter页面会做一些初始化工作,借此,我梳理了下Flutter的初始化流程。2. Flutter初始化时序Flutter初始化主要分四部分,FlutterMain初始化、FlutterNativeView初始化、FlutterView初始化和Flutter Bundle初始化。我们先看下Flutter初始化的时序图,来整体把握下Flutter初始化的一般流程: Flutter初始化时序3. 具体分析3.1 FlutterMain初始化这部分初始化工作是由Application.onCreate方法中调用开始的,在Application创建的时候就会初始化完成,不会影响Flutter页面的第一次启动,所以这里只是做一个简单分析。 从FlutterMain.startInitialization方法代码中可以轻易看出来,初始化主要分四部分。 前面三部分比较类似,分别是初始化配置信息、初始化AOT编译和初始化资源,最后一部分则是加载Flutter的Native环境。 这部分感兴趣的同学可以看下FlutterMain.java源码,逻辑还是比较清晰的。public static void startInitialization(Context applicationContext, Settings settings) { // other codes … initConfig(applicationContext); initAot(applicationContext); initResources(applicationContext); System.loadLibrary(“flutter”); // other codes …}3.2 FlutterNativeView初始化先用一个图来展现FlutterNativeView构造函数的调用栈: FlutterNativeView构造函数调用栈从上图的调用栈中我们知道FlutterNativeView的初始化主要做了些什么,我们再从源码角度较为深入的了解下: FlutterNativeView的构造函数最终主要调用了一个nativeAttach方法。到这里就需要分析引擎层代码了,我们可以在JNI文件中找到对应的jni方法调用。(具体文件为platform_view_android_jni.cc)static const JNINativeMethod native_view_methods[] = { { .name = “nativeAttach”, .signature = “(Lio/flutter/view/FlutterNativeView;)J”, .fnPtr = reinterpret_cast<void*>(&shell::Attach), }, // other codes …};从代码中很容易看出FlutterNativeView.attach方法最终调用了shell::Attach方法,而shell::Attach方法主要做了两件事: 1. 创建PlatformViewAndroid。 2. 调用PlatformViewAndroid::Attach。static jlong Attach(JNIEnv* env, jclass clazz, jobject flutterView) { auto view = new PlatformViewAndroid(); // other codes … view->Attach(); // other codes …}那我们再分析下PlatformViewAndroid的构造函数和Attach方法都做了些什么呢?PlatformViewAndroid::PlatformViewAndroid() : PlatformView(std::make_unique<NullRasterizer>()), android_surface_(InitializePlatformSurface()) {}void PlatformViewAndroid::Attach() { CreateEngine(); // Eagerly setup the IO thread context. We have already setup the surface. SetupResourceContextOnIOThread(); UpdateThreadPriorities();}其中: 1. PlatformViewAndroid的构造函数主要是调用了InitializePlatformSurface方法,这个方法主要是初始化了Surface,其中Surface有Vulkan、OpenGL和Software三种类型的区别。 2. PlatformViewAndroid::Attach方法这里主要调用三个方法:CreateEngine、SetupResourceContextOnIOThread和UpdateThreadPriorities。 2.1 CreateEngine比较好理解,创建Engine,这里会重新创建一个Engine对象。 2.2 SetupResourceContextOnIOThread是在IO线程去准备资源的上下文逻辑。 2.3 UpdateThreadPriorities是设置线程优先级,这设置GPU线程优先级为-2,UI线程优先级为-1。3.3 FlutterView初始化FlutterView的初始化就是纯粹的Android层啦,所以相对比较简单。分析FlutterView.java的构造函数就会发现,整个FlutterView的初始化在确保FlutterNativeView的创建成功和一些必要的view设置之外,主要做了两件事: 1. 注册SurfaceHolder监听,其中surfaceCreated回调会作为Flutter的第一帧回调使用。 2. 初始化了Flutter系统需要用到的一系列桥接方法。例如:localization、navigation、keyevent、system、settings、platform、textinput。 FlutterView初始化流程主要如下图所示: FlutterView初始化3.4 Flutter Bundle初始化Flutter Bundle的初始化是由调用FlutterActivityDelegate.runFlutterBundle开始的,先用一张图来说明下runFlutterBundle方法的调用栈: Flutter的Bundle初始化我们再从源码角度较为深入了解下: FlutterActivity的onCreate方法在执行完FlutterActivityDelegate的onCreate方法之后会调用它的runFlutterBundle方法。FlutterActivityDelegate.runFlutterBundle代码如下:public void runFlutterBundle(){ // other codes … String appBundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext()); if (appBundlePath != null) { flutterView.runFromBundle(appBundlePath, null, “main”, reuseIsolate); }}很明显,这个runFlutterBundle并没有做太多事情,而且直接调用了FlutterView.runFromBundle方法。而后兜兜转转最后会调用到PlatformViewAndroid::RunBundleAndSnapshot方法。void PlatformViewAndroid::RunBundleAndSnapshot(JNIEnv* env, std::string bundle_path, std::string snapshot_override, std::string entrypoint, bool reuse_runtime_controller, jobject assetManager) { // other codes … blink::Threads::UI()->PostTask( [engine = engine_->GetWeakPtr(), asset_provider = std::move(asset_provider), bundle_path = std::move(bundle_path), entrypoint = std::move(entrypoint), reuse_runtime_controller = reuse_runtime_controller] { if (engine) engine->RunBundleWithAssets( std::move(asset_provider), std::move(bundle_path), std::move(entrypoint), reuse_runtime_controller); });}PlatformViewAndroid::RunBundleAndSnapshot在UI线程中调用Engine::RunBundleWithAssets,最终调用Engine::DoRunBundle。 DoRunBundle方法最后只会调用RunFromPrecompiledSnapshot、RunFromKernel和RunFromScriptSnapshot三个方法中的一个。而这三个方法最终都会调用SendStartMessage方法。bool DartController::SendStartMessage(Dart_Handle root_library, const std::string& entrypoint) { // other codes … // Get the closure of main(). Dart_Handle main_closure = Dart_GetClosure( root_library, Dart_NewStringFromCString(entrypoint.c_str())); // other codes … // Grab the ‘dart:isolate’ library. Dart_Handle isolate_lib = Dart_LookupLibrary(ToDart(“dart:isolate”)); DART_CHECK_VALID(isolate_lib); // Send the start message containing the entry point by calling // _startMainIsolate in dart:isolate. const intptr_t kNumIsolateArgs = 2; Dart_Handle isolate_args[kNumIsolateArgs]; isolate_args[0] = main_closure; isolate_args[1] = Dart_Null(); Dart_Handle result = Dart_Invoke(isolate_lib, ToDart("_startMainIsolate"), kNumIsolateArgs, isolate_args); return LogIfError(result);}而SendStartMessage方法主要做了三件事: 1. 获取Flutter入口方法(例如main方法)的closure。2. 获取FlutterLibrary。 3. 发送消息来调用Flutter的入口方法。4. 总结一下本次主要分析了下FlutterActivity的onCreate方法中的Flutter初始化部分逻辑,很明显会发现主要耗时在FlutterNativeView、FlutterView和Flutter Bundle的初始化这三块,将这三部分的初始化工作前置就可以比较容易的解决引言中提出的问题。经测试发现,这样改动之后,Flutter页面第一次启动时长和后面几次启动时长差不多一样了。 对于FlutterMain.startInitialization的初始化逻辑、SendStartMessage发送的消息如何最终调用Flutter中的入口方法逻辑没有进一步深入分析,这些内容后续再继续分析撰文分享。本文作者:闲鱼技术-然道阅读原文本文为云栖社区原创内容,未经允许不得转载。

November 26, 2018 · 2 min · jiezi

在Flutter中嵌入Native组件的正确姿势是...

引言在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一个很好的选择。本文希望向大家介绍AndroidView的使用方式以及在此基础之上拓展的双端嵌入Native组件的解决方案。1. 使用教程1.1. DemoRun嵌入地图这一场景可能在很多App中都会存在,但是现在的地图SDK都没有提供Flutter的库,而自己开发一套地图显然不太现实。这种场景下,使用混合栈的形式是一个比较好的选择。我们可以直接在Native的绘图树中嵌入一个Map,但是这个方案嵌入的View并不在Flutter的绘图树中,是一种比较暴力且不优雅的方式,使用起来也很费劲。这时候,使用Flutter官方提供的控件AndroidView就是一种比较优雅的解决方案了。这里做了一个简单的嵌入高德地图的demo,就让我们跟着这个应用场景,看一下AndroidView的使用方式和实现原理。1.2. AndroidView使用方式AndroidView的使用方式和MethodChannel类似,比较简单,主要分为三个步骤:第一步:在dart代码的相应位置使用AndroidView,使用时需要传入一个viewType,这个String将用于唯一标识该Widget,用于和Native的View建立关联。第二步:在native侧添加代码,写一个PlatformViewFactory,PlatformViewFactory的主要任务是,在create()方法中创建一个View并把它传给Flutter(这个说法并不准确,但是我们姑且可以这么理解,后续会进行解释)第三步:使用registerViewFactory()方法注册刚刚写好的PlatformViewFactory,该方法需要传入两个参数,第一个参数需要和之前在Flutter端写的viewType对应,第二个参数是刚刚写好的的PlatformViewFactory。配置高德地图的部分这里就省略不说了,官方有比较详细的文档,可以去高德开发者平台进行查阅。以上便是使用AndroidView的所有操作,总体看起来还是比较简单的,但是真正要用起来,还是有两个无法忽视的问题:View最终的显示尺寸由谁决定?触摸事件是如何处理的?下面就让小闲鱼来给各位一一解答。2. 原理讲解想要解决上面的两个问题,首先必须得理解所谓"传View"的本质是什么?2.1. 所谓"传View"的本质是什么?要解决这个问题,自然避免不了的需要去阅读源码,从更深的层面去看这个传递的整个过程,可以整理出一张这样的流程图:我们可以看到,Flutter最终拿到的是native层返回的一个textureId。根据native的知识ky h这个textureId是已经在native侧渲染好了的view的绘图数据对应的ID,通过这个ID可以直接在GPU中找到相应的绘图数据并使用,那么Flutter是如何去利用这个ID的呢?在之前的深入了解Flutter界面开发中,也给大家介绍了Flutter的绘图流程。我这里也给大家再简单整理一下Flutter的Framework层最后会递交给Engine层一个layerTree,在管线中会遍历layertree的每一个叶子节点,每一个叶子节点最终会调用Skia引擎完成界面元素的绘制,在遍历完成后,在调用glPresentRenderBuffer(IOS)或者glSwapBuffer(Android)按完成上屏操作。Layer的种类有很多,而AndroidView则使用的是其中的TextureLayer。TextureLayer在之前的《Flutter外接纹理》中有更为详细的介绍,这里就不再赘述。TextureLayer在被遍历到时,会调用一个engine层的方法SceneBuilder::addTexture() 将textureId作为参数传入。最终在绘制的时候,skia会直接在GPU中根据textureId找到相应的绘制数据,并将其绘制到屏幕上。那么是不是谁拿到这个ID都可以进行这样的操作呢?答案当然是否定的,Texture数据存储在创建它的EGLContext对应的线程中,所以如果在别的线程进行操作是无法获取到对应的数据的。这里需要引入几个概念:显示屏对象(Display):提供合理的显示器的像素密度和大小的信息Presentation:它给Android提供了在对应的上下文(Context)和显示屏对象(Display)上绘制的能力,通常用于双屏异显。这里不展开讲解Presentation,我们只需要明白Flutter是通过Presentation实现了外接纹理,在创建Presentation时,传入FlutterView对应的Context和创建出来的一个虚拟显示屏对象,使得Flutter可以直接通过ID找到并使用Native创建出来的纹理数据。2.2. View最终的显示尺寸由谁决定?通过上面的流程大家应该都能想到,显示尺寸看起来像是由两部分决定的:AndroidView的大小,Android端View的大小。那么实际上到底是有谁来决定的呢,让我们来做一个实验?直接新建一个Flutter工程,并把中间改成一个AndroidView。//Flutterclass _MyHomePageState extends State<MyHomePage> { double size = 200.0; void _changeSize() { setState(() { size = 100.0; }); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text(widget.title), ), body: Container( color: Color(0xff0000ff), child: SizedBox( width: size, height: size, child: AndroidView( viewType: ’testView’, ), ), ), floatingActionButton: new FloatingActionButton( onPressed: _changeSize, child: new Icon(Icons.add), ), ); }}在Android端也要加上对应的代码,为了更好地看出裁切效果,这里使用ImageView。//Android@Overridepublic PlatformView create(final Context context, int i, Object o) { final ImageView imageView = new ImageView(context); imageView.setLayoutParams(new ViewGroup.LayoutParams(500,500)); imageView.setBackground(context.getResources().getDrawable(R.drawable.idle_fish)); return new PlatformView() { @Override public View getView() { return imageView; } @Override public void dispose() { } };}首先先看AndroidView,AndroidView对应的RenderObject是RenderAndroidView,而一个RenderObject的最终大小的确定是存在两种可能,一种是由父节点所指定,还有一种是在父节点指定的范围中根据自身情况确定大小。打开对应的源码,可以看到其中有个很重要的属性sizedByParent = true,也就是说AndroidView的大小是由其父节点所决定的,我们可以使用Container、SizedBox等控件控制AndroidView的大小。AndroidView的绘图数据是Native层所提供的,那么当Native中渲染的View的实际像素大小大于AndroidView的大小时,会发生什么呢?通常情况下,这种情况的处理思路无非就两种选择,一种是裁切,另一种是缩放。Flutter保持了其一贯的做法,所有out of the bounds的Widget统一使用裁切的方式进行展示,上面所描述的情况就被当作是一种out of the bounds。当这个View的实际像素大小小于AndroidView的时候,会发现View并不会相应地变小(Container的背景色并没有显露出来),没有内容的地方会被白色填充。这其中的原因是SingleViewPresentation::onCreate中,会使用一个FrameLayout作为rootView。2.3. 触摸事件如何传递Android的事件流大家应该都很熟悉了,自顶向下传递,自底向上处理或回流。Flutter同样是使用这一规则,但是其中AndroidView通过两个类来去处理手势:MotionEventsDispatcher:负责将事件封装成Native的事件并向Native传递;AndroidViewGestureRecognizer:负责识别出相应的手势,其中有两个属性:cachedEvents和forwardedPointers,只有当PointerEvent的pointer属性在forwardedPointers中时才会去进行分发,否则会存在cacheEvents中。这里的实现主要是为了解决一些事件的冲突,比如滑动事件,可以通过gestureRecognizers来进行处理,这里可以参考官方注释。/// For example, with the following setup vertical drags will not be dispatched to the Android view as the vertical drag gesture is claimed by the parent [GestureDetector]./// /// GestureDetector(/// onVerticalDragStart: (DragStartDetails d) {},/// child: AndroidView(/// viewType: ‘webview’,/// gestureRecognizers: <OneSequenceGestureRecognizer>[],/// ),/// )/// /// To get the [AndroidView] to claim the vertical drag gestures we can pass a vertical drag gesture recognizer in [gestureRecognizers] e.g:/// /// GestureDetector(/// onVerticalDragStart: (DragStartDetails d) {},/// child: SizedBox(/// width: 200.0,/// height: 100.0,/// child: AndroidView(/// viewType: ‘webview’,/// gestureRecognizers: <OneSequenceGestureRecognizer>[ new VerticalDragGestureRecognizer() ],/// ),/// ),/// )所以总结起来,这部分流程总结起来其实也很简单:事件最初从Native到Flutter这一阶段不在本文的讨论范围之内,Flutter按照自己的规则去处理事件,如果AndroidView赢得了事件,事件就会被封装成相应的Native端的事件并且通过方法通道传回Native,Native再根据自己的处理事件的规则去处理。3. 总结3.1. 方案局限性往大里说:这套方案是Google为了解决开发者日益增长的业务需求与落后的生态环境之间的矛盾而产生的,这一矛盾是一个新生态必然需要去面对的主要矛盾。为了解决这一个问题,最简单的方式当然就是允许开发者使用老生态中已经非常成熟的控件。当然,这样是可以临时解决Flutter生态发展不全面的问题,但是使用这套方案不可避免的需要去编写双端代码(甚至现在iOS还没有对应的控件,当然之后肯定会更新),不能做到真正的跨端。往小里说:这套方案存在着性能上的缺陷,在AndroidView这个类的第三句注释中,官方就已经提到了这是一套比较昂贵的方案,避免在使用Flutter控件也能实现的情况下去使用它。如果之前有看过《Flutter外接纹理》这一文章的同学应该知道,Flutter实现外接纹理的方案中,数据从GPU->CPU->GPU的过程代价是比较大的,在大量使用的场景会造成明显的性能缺陷。我们通过一些手段绕过了中间CPU这一步,并且将这项技术在APP中落地,用于处理图片资源。3.2. 实际应用目前闲鱼从Native向Flutter的迁移工作遇到了Native的本地图片资源在Flutter侧无法访问的问题,在现在Flutter和Native必将长期共存的情况下,重新拷贝一份资源以Flutter的规则来存储当然可以,但是不可避免地增大了包体积,而且不好管理。面对这个问题,我们的解法便是借鉴了AndroidView使用Texture的思路并在将其优化。实现了Native和Flutter的图片资源归一化。除了用于加载位于Native资源目录下的本地图片之外,还可以利用Native的图片库来加载网络图片。我们这么去做的原因是我们在Native侧的图片库较为完善并且经受过大量的线上考验,现在这一阶段,我们不希望将过多的精力投入到重复造轮子这一件事上,而处理网络图片资源和处理本地图片资源的思路其实是一样的,所以我们选择将图片资源进行了统一地整合,在与官方的团队进行沟通并完善后会和大家同步,敬请关注我们的公众号。3.4. 引用高德地图SDK文档万万没想到——Flutter外接纹理Android7.1 Presentation双屏异显原理分析本文作者:闲鱼技术-尘萧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 16, 2018 · 1 min · jiezi

手拉手带你极速构建漂亮的跨平台(iOS/Android)移动应用 ✿ 环境搭建

上篇文章带大家认识了 Flutter ,想必大家已迫不及待的想练练手,所以要行动起来,现在这篇文章就带您搭建一个 Flutter 运行及开发环境。文章详情可查阅我的博客 https://h.lishaoy.net ,欢迎大家访问。安装 Flutter SDK想要在本地电脑上运行 Flutter ,需要安装 Flutter SDK 才可以运行, SDK 里面有一些用于创建、构建、测试和编译应用程序的命令行工具等,这些在开发的时候会用到。首先,我们有 2 种方法获取 SDK可以到 下载 Flutter SDK 到本地电脑可以用 git clone 命令下载到本地电脑git clone -b master https://github.com/flutter/flutter.git其次,把下载下来的 Flutter SDK 解压,放到系统的某个目录,比如我是放到: /Applications/flutter ,如图:配置环境变量配置环境变量的目的是为了让 Flutter SDK 命令行工具在全局范围都起作用,以便开发使用。首先,您可以用编辑器打开主目录下的 .bash_profile,或者用 vi 命令编辑,我习惯用 vi 命令,如下vi $HOME/.bash_profile新增以下配置export PATH=$PATH:/Applications/flutter/binexport PUB_HOSTED_URL=https://pub.flutter-io.cnexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn Tips: 第一行 export PATH=$PATH:/Applications/flutter/bin 中的 /Applications/flutter/bin 就是刚才下载的 Flutter SDK 解压后放在本地电脑的目录,您要根据自己操作更改为自己电脑对应的目录。第二、三行为解决国内下载或更新资源慢的国内镜像,配置这个下载或更新资源会快一些。再执行 source $HOME/.bash_profile 命令刷新当前命令行窗口,或者关掉当前命令行窗口重新打开,效果一样source $HOME/.bash_profile再执行 flutter –help,来测试环境变量是否配置成功,如图: Tips: 如果你使用的是 zsh,需要在 ~/.zshrc 文件中添加:source ~/.bash_profile ,否则 flutter 命令将无法运行。配置 iOS 开发环境想用 Flutter 为 iOS 平台开发应用,需要安装 Xcode,我们可以去苹果应用商店下载。安装好 Xcode 后,你需要打开一次 Xcode 同意许可协议(会提示),或者执行 sudo xcodebuild -license 同意许可协议。然后执行 open -a Simulator 命令,就可以打开一个模拟器,来运行和测试 Flutter 程序,如图配置 Android 开发环境想用 Flutter 为 Android 平台开发应用,需要下载安装 Android Studio。安装好 Android Studio 后,启动它,首次启动会安装最新的 Android SDK ,但是你可能会遇到这样的问题,如图:如果遇到这个问题应该就是网络问题(需要科学上网),点 Setup Proxy 来设置代理,如图:如一切正常,就会提示你需要下载一些东西,如图点击 Finish 按钮后就会下载安装以上列表的东西,下载安装完 SDK 后,如图:需要我们打开一个项目,我们可以用刚才已经配置好的 Flutter SDK 的命令行创建一个 Flutter 项目,如执行以下命令cd ~/desktopflutter create new_flutter命令执行完成后,在桌面就会生成一个 Flutter 项目,再用 Android Studio 打开,项目打开后会提示安装 Flutter 插件和依赖 Dart 语言插件 ,安装完之后我们可以去创建一个模拟器。打开 Tools>AVD Manager ,点击 Create Virtual Device… 来创建一个模拟器,选择一个设备,点击 Next,如图为模拟器选择一个系统镜像(我选择的是第一个),点击 Download ,下载完成后,点击 Next 后,如图最后,在模拟性能这里选择 Hardware - GLES 2.0 启动硬件加速,点击 Finish 完成配置编辑器前面我们已经配置好了 Flutter SDK 、iOS 模拟器 、Android 模拟器 ,最后我们还需要配置一下编辑器,当然您可以选择 Android Studio 或者 VS Code,这里我选择的是轻量级的 VS Code。如对 VS Code 不是很熟悉,可参考我之前写的 VS Code 编辑技巧打开终端进入我们刚才新建的 Flutter 项目cd new_flutter再用 VS Code 打开项目code ./打开项目之后 ⌘ - ⇧ - X ,打开扩展,安装 Flutter 插件,如图完成之后,打开项目目录 lib->main.dart 文件, VS Code 会自动提示你安装 Dart 语言扩展包。运行项目现在,所有的准备工作都完成了,就可以开发、测试或运行项目了,在上面我们用 Flutter create 命令创建的 Flutter 项目,自带一个计数器的小功能,我们可以运行看看效果首先,您需要执行 flutter doctor 来检查一下环境是否正常如上图第二项提示 Android license status unknown. 意思是 Android 协议没安装好,可以执行以下命令,来解决问题flutter doctor –android-licenses如上图第三项是 iOS 真机的检查项,可以按照提示操作>如上图第四项是 Java 的编辑器检查,可不用理会,如你没有安装 IDEA 也不会有这个提示其实在我另一台电脑上全部都配置好了 ???? ,如图最后,在 VS Code 编辑器里按 F5 后,会让你选择模拟器来运行 Flutter 程序,如图这个是分别在 iOS 和 Android 运行 Flutter 的效果,如图运行 Flutter 案例现在所有的都准备好了,您可以去我的 GitHub 上下载上篇文章中的案例代码,也可以 git clonecd $HOME/Desktop #进到桌面git clone https://github.com/persilee/flutter_pro.git #下载案例cd flutter_pro #进入案例目录flutter packages get #获取依赖包code ./ #用 VS Code 打开完成以上步骤后,在 VS Code 按 F5 选择模拟器,查看运行效果,如图好的,大功告成,这篇到处为止,下篇将手拉手带大家完成一个实操小案例 。 ...

November 12, 2018 · 2 min · jiezi