Flutter1.0入门基础

Flutter1.0入门基础

注:原课程视频是基于Flutter1的;

目标:

  • 开发入门:工具、环境搭建、入门必备
  • 开发技巧:导航框架、常用功能
  • 开发流程:网络、数据存储、列表、Flutter与Native混编
  • 工程封装:模块开发、AI结合
  • 项目上手:包和插件的开发、屏幕适配和兼容、打包与发布、Flutter的升级和适配;
导航框架实现:Scaffold+PageView
服务端交互:http
页面路由跳转:Navigator
监听列表滚动:NotificationListener
自定义组件
封装Native代码:NativeModules
Native实现智能语音
FLutter与native通信:Channel通信
各种插件和组件的使用;

滚动渐变
智能语音
富文本展示
混合开发(与ios android h5)
瀑布流布局
加载刷新
自定义webview

开发工具需安装:Flutter Plugin、Dart Plugin、Dart DevTools

布局:
  声明式布局
  Layout Widgets
导航:
  Navigator
  MaterialPageRoute
  PageRouteBuilder
列表:
  ListView
  GridView
  ExpansionTile(可折叠列表)
图片:
  Image
    静态图片
    Native图片
    网络图片
    图片缓存
    Icon
    Placeholder
Flutter组件:
  Flutter插件:
    flutter_webview_plugin
    cupertino_icons
    page_view_indicator
    flutter_swiper
    http
    flutter_staggered_grid_view
    flutter_statusbar_manager
    ...
Native插件:
  百度AI智能语音
  ...
自定义组件:
  loading_container
  asr_manager
  webview
  search_bar
网络和存储:
  http
  shared_preferences
  JSON解析和模型转换
  Future(异步编程)
  FutureBuilder(异步UI)
动画:
  基础动画
    AnimateWidget
    AnimatedBuilder
    Animation
    AnimationController
    Tween
    CurvedAnimation
  Hero动画(实现页面间跳转动画)
高级:
  Native Modules:智能AI语音
  Flutter混合开发:
    + Android
    + iOS
    + H5
  通信:
    BasicMessageChannel
    MethodChannel
    EventChannel
  全面屏/折叠屏适配:
    iOS、Android全面屏适配
    折叠屏适配
  Flutter更新升级
打包发布    

IDE:推荐AndroidStudio;

环境搭建

  • 确认系统要求
  • 设置Flutter镜像
  • 获取Flutter SDK
  • iOS开发环境配置
  • Android开发环境设置

命令行工具:

  • bash curl git 2.x mkdir rm unzip which

设置镜像:

  • 编辑当前用户主目录.bash_profile文件
  • export PUB_HOSTED_URL=https://pub.flutter-io.cn
  • export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

注:此镜像为临时镜像,并不能保证一直有用,可以参考官网进行配置;

下载Flutter SDK:

  • 官网下载,解压到主目录(或其他目录)flutter目录;
  • 添加环境变量:export PATH=/Users/huaqiang/flutter/bin:$PATH
  • flutter doctor:第一次运行会很慢
# 相关环境变量

# Android 环境变量
export ANDROID_HOME=/Users/huaqiang/Library/Android/sdk

# Android 模拟器路径
export PATH=${PATH}:${ANDROID_HOME}/emulator

# Android tools 路径
export PATH=${PATH}:${ANDROID_HOME}/tools

# Android 平台工具路径
export PATH=${PATH}:${ANDROID_HOME}/platform-tools

# Android NDK路径
export ANDROID_NDK_HOME=/Users/huaqiang/Library/Android/ndk/android-ndk-r10e

# Flutter 镜像
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

# Flutter环境变量
export PATH=/Users/huaqiang/flutter/bin:$PATH

iOS开发环境设置:

  • 下载安装xcode
  • 配置xcode使命令行工具使用新安装的xcode版本:sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer(也可以指定不同版本的xcode)
  • xcode许可协议通过:sudo xcodebuild -license

iOS模拟器:

  • 使用xcode启动,或通过命令:open -a Simulator

创建Flutter项目:

  • flutter create my_app
  • cd my_app/
    • ios:iOS端宿主项目
    • android:Android端宿主项目
    • pubspec.yaml:Flutter依赖包的配置文件
    • lib:flutter Dart代码
    • 运行项目(命令行方式):flutter run
      • r:热加载
      • R:热重启
      • q:退出
      • h:请求帮助

iOS的真机调试:

# 安装Homebrew
# 更新Homebrew
brew update

# 将Flutter应用安装到iOS设备需要一些工具
brew install --HEAD usbmuxd
brew link usbmuxd
brew install --HEAD libimobiledevice
brew install ideviceinstaller ios-deploy cocoapods
pod setup

# 出现问题 可执行brew doctor按照说明进行解决

# 使用xcode打开项目 配置项目签名等信息,也可以通过命令打开项目
open ios/Runner.xcworkspace

安卓开发环境设置:

  • 下载Android Studio,https://developer.android.com/studiohttps://developer.android.google.cn/studiohttps://developer.android.google.cn/studio/install
  • 执行Android Studio安装向导,这将安装最新的Android SDK,Android SDK平台工具和Android SDK构建工具;

运行Android模拟器:

  • 启动硬件加速
  • Tools-> Androids-> AVD Manager,选择Create Virtual Device
  • 在Emulated Performance下,选择Hardware - GLES2.0 以启动硬件加速(windows上,可以为Android SDK 启动 HAXM)
  • 在Android Virtual Device Manager中,点击工具栏Run
  • 通过flutter run运行启动项目

在配置了emulator相关环境变量之后,也可以通过命令启动模拟器:emulator -avd a81<模拟器名>

运行Android真机:

  • 开启 开发人员选项和USB调试;
  • 链接USB,进行授权;
  • 运行 flutter devices验证是否链接设备;
  • 运行flutter run

安装Flutter和Dart插件:

  • 打开Android Studio
  • Preferences -> Plugins(macOS上操作)
  • File-> Settings-> Plugins(Windows & Linux上操作)
  • 选择Browse repositories,搜索Flutter plugin、Dart插件,重启IDE;

环境准备好之后,项目创建和运行既可以用命令行方式,也可以使用IDE进行;关于项目的运行,可以使用flutter视角运行,也可以使用ios/android视角进行运行;

Flutter的快速上手

  • Dart基础
  • 声明时UI
  • Flutter基础
  • 项目结构、资源、依赖和本地化
  • 视图(Views)
  • 布局和列表
  • 状态管理
  • 路由与导航
  • 主题和文字处理
  • 线程和异步UI、Futures、async、await
  • 手势检测和触摸事件处理
  • 硬件调用,第三方交互
  • 表单输入和富文本
Dart

Dart中,每个app都必须有一个顶级main()函数,作为程序入口;

Dart语法类似js,只不过增加类类型声明,也是强类型语言,支持类型推断:

var name = null
String name = 'hello'
// 只有布尔类型的true才是true,并不是非零即为真;

// 运算符(左边为null时->阻断 赋默认值)
?. ??

// 函数声明
返回值类型 函数名(){}

// Futures 相当于 ES6中的Promise
HttpRequest.request(url).then((value){
  print(json.decode(value.responseText)['origin']);
}).catchError((error) => print(error));

// async 和 await
// js中async返回一个Promise,await用于等待Promise
// Dart中,async函数返回一个Future,函数主体稍后执行;await用于等待Future
_getIPAddress() async {
  var response = awart HttpRequest.request(url)
  ...
}
声明式UI
  • 之前熟悉的是手动构建全功能UI实体,UI响应时使用方法对其进行变更;
  • 为了减轻开发人员在各种UI状态之间转换的负担,Flutter支持描述UI状态,但不关心UI如何过渡到框架;
  • 声明式UI中,视图(Widgets)的配置是不可变的,并且是轻量级的蓝图;如果UI需要变更,Widgets会在自身上触发重建,比较常见的方式是通过Flutter中的StatefulWidgets调用setState(),并构造一个新的Widget子树;
return ViewB(
  color:red,
  child:ViewC(...)
)
  • 框架使用RenderObjects管理传统UI对象(如维护布局的状态);
    • RenderObjects在帧之间保持不变;
    • Flutter的轻量级Widget会在状态改变时,改变RenderObjects,其余部分会由Flutter框架进行处理;
Flutter入门
  • 如何创建Flutter项目、如何运行;
  • 如何导入Widget、HelloWorld、Widget树、重用Widget;
flutter create <projectname>
flutter run -d 'iPhone X'// 指定设备

// 使用Material Design,需导入material.dart包
import 'package:flutter/material.dart';
// 使用iOS样式的widget,导入Cupertino库
import 'package:flutter/cupertino.dart';
// 更基本的窗口widget,请导入widget库
import 'package:flutter/widgets.dart';
// 也可以导入自己编写的widget;
import 'package:flutter/my_widgets.dart';

具体导入包,也是按需导入的,Dart只会导入应用中使用的Widget;

注:该视频是基于Flutter1的,目前版本相关语法可能会有改变;

HelloWorld:

import 'package:flutter/material.dart';

void main(){
  runApp(
    // 根widget
    Center(
      child: Text(
        'Hello world',
        textDirection: TextDirection.ltr,
      )
    )
  )
}

Widget树:

  • Flutter中,几乎所有的东西都是Widget;它是用户界面构成的基本块;
  • 子widget会继承父widget的属性;应用程序本身也是一个组件,即根widget;

常用widget:

  • 结构元素:按钮、菜单
  • 文体元素:字体、颜色、主题
  • 布局:填充、对齐
import 'package:flutter/maretial.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 根widget
    return MeterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(// Scaffold 译为 脚手架
        appBar: AppBar(
          title: Text('Flutter'),
        ),
        body: Center(
          child: Text('Hello world'),
        )
      )
    )
  }
}

重用Widget:

// 自定义一个widget
class HQCustomCard extends StatelessWidget {
  HQCustomCard({
    @required this.index,
    @required this.actionOnPress
  });

  final index;
  final Function onPress;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children:<Widget>[
          Text('Card $index'),
          FlatButton(
            child: const Text('Press'),
            onPressed: this.actionOnPress
          )
        ]
      )
    )
  }
}

// 使用
HQCustomCard(
  index: 1,
  onPress:() {
    print('something!');
  }
)

项目结构、资源、依赖和本地化
  • 项目文件结构
projectname
  android
  build // 构建输出目录
  ios
  lib // Dart源文件
    src
      ...
    main.dart // 入口文件
  test // 测试文件
  pubspec.yaml // 类似 vue的package.json 依赖项的配置文件

  • 图片资源和分辨率处理
    • 图片资源均作为assets处理,放在 assets 文件夹;
    • 该文件夹中可以放置任意类型文件,而不仅仅是图片,如json;

注:assets文件夹是放在哪个目录的?

答:Assets文件夹可以被放置任何目录,Flutter并未预先定义该文件结构,只需要在pubspec.yaml中声明assets的位置,flutter会进行识别;

// 放置资源后,在pubspec.yaml中声明assets
assets:
  - my-assets/data.json

// 使用示例
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  // 使用bulndle加载资源
  return await rootBundle.loadString('my-assets/data.json');
}

对于图片,类似iOS,1x、2x、3x:

  • ldpi 0.75x
  • mdpi 1.0x
  • hdpi 1.5x
  • xhdpi 2.0x
  • xxhdpi 3.0x
  • xxxhdpi 4.0x
// 目录存放:
assets
  images/my_icon.png
  images/2.0x/my_icon.png
  images/3.0x/my_icon.png


// pubspec.yaml中声明
assets:
  - images/my_icon.png

// 访问,可借助AssetImage
return AssetImage("images/my_icon.png");
// 也可以通过Image widget直接使用:
return Image.asset("images/my_icon.png");

  • 归解档strings资源、多语言处理
    • 不像iOS拥有一个Localizable.strings文件,Flutter目前没有专门的字符串资源系统;
    • 目前最佳做法是将strings资源作为静态字段保存在类中;
class Strings {
  static String welcomeMessage = 'something';
}

// 使用
Text(Strings.welcomeMessage)

// 默认Flutter只支持英语,要支持其他语言,需引用 flutter_localizations包;
// 可能也需要引入 intl 包来支持其他的 i10n 机制,比如日期/时间格式化;
dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"

// 要使用 flutter_localizations 包, 还需要在app widget中指定
// localizationsDelegates 和 supportedLocals
import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
  // 这些代理包含实际的本地化值
  localizationsDelegates: [
    // App app-specific localization delegate[s] here
    GlobalMeterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ],
  // 定义支持的地区
  supportedLocales: [
    const Locale('en', 'US') // English
    const Locale('he', 'IL') // Hebrew
    // ... other locals the app supports
  ],
  // ...
)

// 当初始化时,WidgetsApp或MaterialApp会使用指定代理创建一个 Localizations widget;
// Localization widget 可以随时从当前上下文中访问设备的地点,或者使用 Window.locale;


// 要访问本地化文件,使用Localizations.of() 方法访问提供代理的特定本地化类;
// 如需翻译,使用 intl_translation 包来取出翻译副本到 arb 文件中;把他们引入App中,并用 intl 来使用它们;(不使用 intl包也可以实现本地化)

  • 添加Flutter项目依赖(PubSite)
    • Android 在Gradle文件夹 添加依赖项
    • iOS 在Podfile中添加;
    • RN中,会在package.json中管理依赖
    • Flutter使用Dart构建系统和Pub包管理器来处理依赖;
    • pub get安装依赖

pubspec.yaml是Flutter 依赖项配置文件;Android 的Gradle和iOS的Podfile,只用来管理平台相关的依赖项;

视图Views

什么是View:

  • 概念上类似原生的UIView,但是Widget组成的View有些不同;
    • Widget具有不同的生命周期;
    • Widget是不可变的,状态一旦改变,Flutter会创建一个新的Widget实例树;
    • iOS的视图,在调用setNeedsDisplay之前都不会重绘;
    • Widget很轻巧,部分原因是它的不变性;因为它本身不是视图,并且不会直接绘制任何东西,而是对UI及语义的描述;

Flutter Widgets核心布局小部件,如Container Column Row Center;

如何更新Widgets:

  • iOS中,直接调用UIView实例的方法来进行UI控制;
  • 由于Widget不可变,Flutter会通过操纵Widget的状态来更新他们;
  • 对于StatelessWidgets适用于描述界面不依赖对象中配置信息的部件;
  • 如果需要根据网络请求结果或交互之后更新UI,则必须使用StatefulWidgets,并通知框架Widget已更新;
  • StatefulWidgets具有一个State对象,该对象存储状态数据并将其传递到树重建中;

Widget在build之外会改变的,即是有状态的;无状态组件、有状态组件可以相互包含;

import 'package:flutter/material.dart';

void main(){
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title:'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State<SampleAppPage> {
  // placeholder
  String textToShow = "Flutter";

  void _updateText(){
    setState(() {
      textToShow = 'Hello';
    });
  }

  @override 
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('App'),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton:FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      )
    )
  }
}

如何布局:

  • Android 是 xml;iOS是xib约束布局;Flutter通过编写widget树来声明布局;
@override
Widget build(BuildContext context) {
  return Scafford(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child:MaterialButton(
        onPressed:() {},
        child: Text('Hello'),
        padding: EdgeInsets.only(left: 10.0, right: 10.0),
      )
    )
  )
}

布局中添加或删除组件:

  • Android addChild removeChild
  • iOS addSubview() removeFromSuperView()
  • Flutter:传入一个函数或表达式,返回值是一个给父项的Widget;并通过布尔值控制该Widget的创建;
import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "",
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();

}

class _SampleAppPageState extends State<SampleAppPage> {
  // placeholder
  bool toggle = true;

  void _toggle(){
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('one');
    }else {
      return MaterialButton(
        onPressed: () {},
        child: Text('two')
      );
    }
  }

  @override 
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('App'),
      ),
      body: Center(child: _getToggleChild),
      floatingActionButton:FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      )
    )
  }
}

如何对Widget做动画:

  • Android 通过XML创建动画或调用view.animate()
  • iOS,通过 animateWithDuration:animations:方法给view添加动画;
  • Flutter,使用动画库包裹widgets;

AnimationController:

  • 这是一个可以暂定、寻找、停止、反转动画的Animation类型;
  • 需要一个Ticker在vsync发生时发送信号,在每帧运行时创建一个介于0~1的线性插值(interpolation);
  • 可创建多个Animation附加给一个controller;

CurvedAnimation可以实现一个interpolated曲线:

  • controller是动画的主人,而CurvedAnimation计算曲线,并替代controller默认的线性模式;

当构件widget树时,可以吧Animation指定给一个widget的动画属性,比如FadeTransition的opacity属性,并告诉控制器开始动画;

import 'package:flutter/material.dart';

void main() {
  runApp(FadeAppTest());
}

class FadeAppTest entends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp {
      title: 'Fade demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyFadeTest(title: 'Fade'),
    }
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurveAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = CurveAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Container(
          // 按钮淡入动画
          opacity: curve,
          child: FlutterLogo(
            size:100.0,
          )
        )
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        child:Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        }
      )
    )
  }
}



如何绘图(Canvas draw/paint):

  • Android Canvas与Drawable
  • iOS CoreGraphics
  • Flutter,提供的底层渲染引擎Skia,通过CustomPaintCustomPainter两个类帮助绘制画布;
// 画图
import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: DemoApp()));

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => Scaffold(body: Signature());
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderobject();
          Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CunstomPaint(painter: SignaturePainter(_points), size: Size.infinite),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;

  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..StrokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

使用CustomPaint widget在绘制阶段绘制:

  • 实现抽象类CustomPainter,并将其传递给CustomPaintpainter属性;
  • CustomPaint子类必须实现paintshouldRepaint方法;
// 绘制图形
import 'package:flutter/material.dart';
import 'package:flutter_app/navigator/tab_navigator.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MatreialApp(
      title:'',
      theme: new ThemeData.fallback(),
      home:_MyCanvas(),
    )
  }
}

class MyCanvasPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint();
    paint.color = Colors.amber;
    canvas.drawCircle(Offset(100.0, 200.0), 40.0, paint);
    Paint paintRect = Paint();
    paintRect.color = Colors.lightBlue;
    Rect rect = Rect.fromPoints(Offset(150.0,300.0), Offset(300.0,400.0));
    canvas.drawRect(rect, paintRect);
  }

  bool shouldRepaint(MyCanvasPainter oldDelegate) => false;
  bool shouldRebuildSemantics(MyCanvasPainter oldDelegate) => false;
}

class _MyCanvas extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomPaint(
        painter: MyCanvasPainter(),
      ),
    );
  }
}

自定义widgets:

  • Android 中通过继承View,重载方法;
  • iOS通过编写UIView子类,重载方法;
  • Flutter中,推荐通过组合方式自定义widget,而不是扩展它;
// 自定义一个button
class CustomButton extends StatelessWidget {
  final String label;
  CustomButton(this.label);

  @override
  Wideget build(BuildContext context) {
    return new RaisedButton(
      onPressed: () {},
      child: new Text(label)
    );
  }
}

// 使用
...
new Center(
  child: new CustomButton('Hellow');
)
...

设置widget属性,如透明度:

  • Android,使用view的setAlpha方法;
  • iOS,设置opacity或alpha属性;
  • Flutter要改变透明度,可以给widget包裹一个Opacity widget;
Opacity (
  opacity: 0.5,
  child: Text('透明度50%')
)
布局与列表

Android中的LinearLayout相当于?

  • Android中使用 LinearLayout来使你的空间呈水平或垂直排列;
  • Flutter中,使用Row或Column widget来实现相同的结果(关于主轴 交叉轴的概念参考flex);
@override
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Row1'),
      Text('Row2'),
      Text('Row3'),
      Text('Row4'),
    ],
  )
}

@override
Widget build(BuildContext context) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Row1'),
      Text('Row2'),
      Text('Row3'),
      Text('Row4'),
    ],
  )
}

Android中的RelativeLayout相当于?

  • Android 中,RelativeLayout用于使widget相对于彼此的位置排列;
  • Flutter中可以通过Column、Row、Stack组合实现 RelativeLayout的效果;
  • 可以为widget构造函数指定相对于父组件的布局规则;

定义布局属性:

  • Flutter中,布局主要由专门用于提供布局的小部件定义,并结合空间widget及其样式属性;
    • RowColumn widget控制一个数组中条目,排列方向;
    • Container widget控制一个布局的样式和属性;
    • Center widget负责居中它的子widget;
    • Flutter核心widget库还提供了其他布局小部件,如 PaddingAlignStack
Center(
  child: Column(
    children:<Widget>[
      Container(
        color: Colors.red,
        width: 100.0,
        height:100.0,
      ),
      Container(
        color: Colors.blue,
        width: 100.0,
        height:100.0,
      ),
      Container(
        color: Colors.green,
        width: 100.0,
        height:100.0,
      ),
    ]
  )
)

分层布局:

  • Flutter使用Stack widget控制widget在同一层;子widgets可以完全或者部分覆盖基础widgets;
  • Stack控件将其子项相对于其框的边缘定位;
Stack(
  alignment: const Alignment(0.6,0.6),
  children:<Widget>[
    CircleAvatar(
      backgroundImage: NetworkImage(
        "https://wwwwww",
      ),
    ),
    Container(
      decoration: BoxDecoration(
        color: Colors.black45,
      ),
      child: Text('Flutter'),
    )
  ]
)

设置布局样式:

  • Flutter使用布局组件,Padding Center Column Row等widget;
  • 组件接收用于样式布局的的构造参数;如Text widget的TextStyle属性;
var textStyle = TextStyle(fontSize: 32.0, color: Colors.cyan, fontWeight: FontWeight.w600);
...

Center(
  child: Column(
    children:<Widget>[
      Text(
        'Sample',
        style: textStyle,
      ),
      Padding(
        padding: EdgeInsets.all(20.0),
        child: Icon(
          Icons.lightbulb_outline,
          size:48.0,
          color: Colors.redAccent
        )
      )
    ]
  )
)

ScrollView在Flutter中等价于?

  • Flutter ListView,表现可以和ScrollView基本一致;
@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('one'),
      Text('two')
    ]
  )
}

Flutter的列表组件:

  • 对比iOS的UITableView和UICollectionView,Flutter中就是ListView;
ListView(children: _getListData()),
...

_getListData(){
  List<Widget> widgets = []
  for (int i = 0; i < 100; i++) {
    widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
  }
  return widgets;
}

如何动态更新ListView:

  • iOS, reloadData()通知tableView刷新;
  • Android,变更数据后,通过notifyDataSetChanged来更新列表;
  • Flutter中,通过setState()更新widget列表,如果只改变了某一元素,是不会有效果的,因为ListView组件树并未发生变化;全部替换的话,则会起作用,因为ListView组件树也发生了变化;
// 列表操作示例
...

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

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

    for (int i = 0l i < 100; i++){
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row $i"),
      ),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        })
      }
    )
  }
}

一个更高效的做法是:使用ListView.Builder来构建动态列表;

ListView.builder(
  itemCount: widgets.length,
  itemBuilder: (BuildContext context, int position) {
    // 和iOS的 cellForRowAtIndexPath很像 
    return getRow(position);
  },
);

...

Widget getRow(int i) {
  return GestureDetector(
    child: Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row $i"),
    ),
    onTap: () {
      setState(() {
        widgets.add(getRow(widgets.length + 1));
        print('row $i');
      })
    }
  )
}

状态管理

StatelessWidget:

  • 不需要状态更改的widget,没有要管理的内部状态;
  • 当描述的用户界面部分不依赖于对象本身的配置信息以及widget的BuildContext时,无状态widget很有用;
  • AboutDialogCircleAvatorText都是StatelessWidget的子类;
// StatelessWidget:
import 'package:flutter/maretial.dart';

void main() => runApp(MyStatelessWidget(text:'haha'));

class MyStatelessWidget extends StatelessWidget {
  final String text;
  MyStatelessWidget({Key key, this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        text,
        textDirection: TextDirection.ltr,
      )
    )
  }
}

StatelessWidget的build方法被调用的三种情况:

  • widget插入树中;
  • widget父级更改配置;
  • widget依赖的 InheritedWidget 发生变化时;

StatefulWidget:

  • 可变状态widget,使用setState方法管理状态改变;
  • 调用setState方法通知Flutter框架,有状态变化,Flutter会重新运行build方法;
  • Checkbox Radio Slider InkWell Form TextField都是有状态widget,也是StatefulWidget的子类;
  • 声明一个StatefulWidget时,需要一个createState()方法,此方法创建管理widget状态的状态对象 _MyStatefulWidgetState,示例如下;
class MyStatefulWidget extends StatefulWidget {
  final String title;
  MyStatefulWidget({Key key, this.title}) : super(key: key);

  @override
  // 创建State对象,框架调用createState来构建widget
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

// _MyStatefulWidgetState类 实现 widget的 build()方法;
// 当状态改变时,使用新值调用setState;

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  bool showtext = true;
  bool toggleState = true;
  Timer t2;

  void toggleBlinkState() {
    setState(() {
      toggleState = !toggleState;
    });

    var twenty = const Duration(milliseconds: 1000);
    if(toggleState == false) {
      t2 = Timer.periodic(twenty, (Timer t) {
        toggleShowText();
      });
    }else {
      t2.cancel();
    }
  }

  void toggleShowText() {
    setState(() {
      showtext = !showtext;
    })
  }

  @override
  Widget buidl(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            (showtext?(Text('waefawef')):(Container())),
            Padding(
              padding: EdgeInsets.only(top: 70.0),
              child: RaisedButton(
                onPressed: toggleBlinkState,
                child: (toggleState?(Text('Blink')):(Text(Stop Blinking)))
              )
            )
          ]
        )
      )
    )
  }
}

状态管理的三种主要方式:

  • 每个widget管理自己的状态
  • 父widget管理widget状态(优选)
  • 混合管理;
路由和导航

Android的Intent:

  • Android中在Activity间切换时,会用到Intents,Flutter没有这个概念,需要的话通过整合Native来触发Intents;
  • Intent在调用外部组件如相机、文件选择也有用,Flutter需要借助第三方插件实现相应功能;

Flutter实现页面跳转(导航):

  • 实现切屏,可以访问路由以绘制新的Widget;
  • 管理多个屏幕的两个核心概念:Route和Navigator;
    • Route是屏幕或页面的抽象;
    • Navigator是管理Route的Widget;可以通过push和pop Route实现页面切换;

和Android类似,可以在AndroidManifest.xml中声明Activites;在Flutter中,我们可以将具有指定Route的Map传递到顶层MaterialApp实例,但这不是必须的;

Flutter中页面导航的两种选择:

  • 具体指定一个由路由名构成的Map(MaterialApp)
  • 直接跳转到一个路由(WidgetApp)
void main() {
  runApp(MaterialApp(
    home: MyAppHome(),
    router: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(titie: 'page A'),
      '/b': (BuildContext context) => MyPage(titie: 'page B'),
      '/c': (BuildContext context) => MyPage(titie: 'page C'),
    },
  ));
}
// 使用路由名字
Navigator.of(context).pushNamed('/b');

// 使用Navigator的push方法 直接跳转
// MaterialPageRoute widget是一个模板路由 会根据平台自适应替换页面
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => UsualNavscreen()));

获取路由跳转返回结果:

  • Navigator类可用于获取route push到栈中返回结果;
  • 使用 await等待路由返回结果;
// 进入页面
Map coordinate = await navigator.of(context).pushNamed('/location');

// 退出页面
Navigator.of(context).pop({"lat":100, "long":100});

Flutter如何处理来自外部应用程序的Intents(Android):

  • 通过直接与Android层通信,请求共享数据,来处理来自Android的Intents;
    • 在AndroidManifest.xml中注册想要处理的Intent;
    • 在MainActivity中,处理intent;
    • 等待Flutter请求数据;
    • 通过MethodChannel发送;
// android 部分
package com.example.shared;

import android.content.Intent;
import android.os.Bundle;

import java.nio.ByteBuffer;

import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {
  private String sharedText;

  @override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType;

    if (Intent.ACTION_SEND.equals(action) && type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent);
      }
    }

    new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHander(
      new MethodCallHandler() {
        @override
        public void onMethodCall(MethodCall call, MethodChannel.Result result) {
          if (call.method.contentEquals("getSharedText")) {
            result.success(sharedText);
            sharedText = null;
          }
        }
      }
    );
  }

  void handleSendText(Intent intent) {
    sharedText - intent.getStringExtra(Intent.EXTRA_TEXT);
  }
}

// flutter
...
class _SampleAppPageState extends State<SampleAppPage> {
  static const platform = const MethodChannel('app.channel.shared.data');
  String dataShared = 'No data';

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

  @override
  Widget build(BuildContext context) {
    ruturn Scaffold(body: Center(child: Text(dataShared)));
  }

  getSharedText() async {
    var sharedData = await platform.invokeMethod("getSharedText");
    if (sharedData != null) {
      setState(() {
        dataShared = sharedData;
      })
    }
  }
}


如何跳转其他App:

  • iOS中,通过特定的URL Scheme,对于系统级App,这个scheme取决于App;
  • 在Flutter中实现这个功能,可以创建一个原生平台的整合层,或者使用现有的plugin(如 url_launcher);
线程和异步UI

如何编写异步代码:

  • Dart有一个单线程执行模型:
    • 支持Isolate(一种在另一个线程运行代码的方式)
    • 一个事件循环
    • 支持异步编程
  • 除非自己创建一个Isolate,否则你的Dart始终运行在主UI线程,并由 event loop驱动;
    • Flutter中 event loop和 iOS中 main loop相似:Looper是附加在主线程上的;
  • 可以使用Dart提供的异步工具,如 async/await 实现异步操作;
loadData() async {
  String dataURL = 'https://xxx';
  http.Response respinse = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  })
}

如何把工作放到后台执行:

  • Flutter是单线程并且跑着一个event loop,因此不必担心线程管理或生成后台线程;
  • I/O操作,磁盘或网络请求 都可以安全的使用 async/await来完成;
  • 如果需要让CPU执行计算密集型任务,你需要使用Isolate利用多核CPU来避免阻塞event loop;

Isolate是分离的运行线程,并且不和主线程的内存堆共享内存;

// Isolate的使用
import 'dart:isolate';

...
loadData() async {
  // 打开ReceivePort以接收传入信息
  ReceivePort receivePort = ReceivePort();
  // 创建并生成与当前Isolate共享相同代码的Isolate
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // 流的第一个元素
  SendPort sendPort = await receivedPort.first;
  // 流的第一个元素被收到后监听会关闭,所以需要重新打开一个ReceivePort以接收传入的消息
  ReceivePort response = ReceivePort();
  // 通过此发送端口向其对应的“ReceivePort”发送异步消息
  sendPort.send(
    ["https://xxxxx", response.sendPort]
  );
  // 获取端口送来的数据
  List msg = await response.first;

  setState(() {
    widgets = msg;
  });
}

// isolate的入口函数 该函数会在新的Isolate中调用,Isolate.spawn的message参数会作为调用它的唯一参数
static dataLoader(SendPort sendPort) async {
  // 打开ReceivePort以接收传入的消息
  ReceivePort port  = ReceivePort();

  // 通知其他的isolates, 本isolate 所监听的端口
  sendPort.send(port.sendPort);

  // 获取其他端口发送的异步消息 msg -> 
  // 等价于 List msg = await port.first;
  await for (var msg in port) {
    
    String data = msg[0];
    SendPort replyTo = msg[1];

    String dataURL = data;
    http.Response response = await http.get(dataURL);
    // 其对应的ReceivePort 发送解析出来的json数据
    replyTo.send(json.decode(response.body));
  }
}

上边,dataLoader()是一个运行于独立线程上的Isolate;在Isoalte里,你可以执行CPU密集型任务,如解析一个很大的json、或处理一些复杂的计算;

如何进行网络请求:

  • 使用http package,在pubspec.yaml中添加如下依赖;
// pubspec.yaml
dependencies:
  ...
  http: ^0.12.0+1

如何为长时间运行的任务添加一个进度指示器:

  • 进度条widget,ProgressIndicator
  • 可以通过一个布尔值flag来控制是否展示进度;在任务开始时,告诉Flutter更新状态,并在结束后隐藏;
手势检测 及 触摸事件处理

如何给Flutter widget添加一个点击事件的监听:

  • 对于支持点击事件的widget可以直接传递参数函数,如RaisedButtononPressed
  • 如果widget本身不支持,则需要在外包裹一个GestureDetector,对应的属性是onTap

如何处理widget上的其他手势:

  • 使用GestureDetector可以监听多种手势;
    • 点击:
      • onTapDown(e) 轻触手势开始
      • onTapUp 轻触手势停止
      • onTap
      • onTapCancel 比如点击了一个按钮 但没松开 最终划出去了
    • 双击:
      • onDoubleTap
    • 长按:
      • onLongPress
    • 垂直拖动:
      • onVerticalDragStart
      • onVerticalDragUpdate
      • onVerticalDragEnd
    • 水平拖动:
      • onHorizontalDragStart
      • onHorizontalDragUpdate
      • onHorizontalDragEnd
主题 和 文字处理

如何在Text widget上设置自定义字体;

  • iOS需要在项目中引入ttf文件,并在info.plist中设置引用;
  • Android 需要创建一个Font资源并将其传递到TextView的FontFamily参数中;
  • Flutter中设置字体,只需在文件夹中放置字体文件,并在pubspec.yaml中引入。就像添加图片那样;
// pubspec.yaml
fonts:
  - family: MyCustomFont
    fonts:
      - asset: fonts/MyCustomFont.ttf
      - style: italic

// 使用
Text(
  'hello',
  style: TextStyle(fontFamily: 'MyCustomFont'),
)

如何在Text上定义样式:

  • Test widget的style属性,接收一个TextStyle对象,可以指定很多参数;
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing

如何给App设置主题:

  • 声明一个顶级组件,MaterialApp作为App入口;它是一个更高级的组件封装;
  • 也可以使用WidgetApp,它提供能了许多相似功能,但不如MaterialApp强大;
  • 如果想实现iOS设计风格的界面,可以使用 Cupertino library;

通用的颜色和样式,可以通过给 MaterialApp widget传递一个 ThemeData对象;

MaterialApp(
  title:'',
  theme: ThemeData(
    priarySwatch: Colors.blueGrey,
    textSelectionColor: Colors.red
  ),
  home: Scaffold(
    appBar: AppBar(),
    body: Text("hahaha"),
  ),
)
表单输入和富文本

如何获取用户输入:

  • 使用TextField/TextFormField,然后通过TextEditingController来获得用户输入;
class _MyFormState extends State<MyForm> {
  final myController = TextEditingController();

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar:...
      body: Padding(
        padding: conse EdageInsets.all(16.0),
        child: TextField(
          // 指定一个controller
          controller: myController,
        )
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          return showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                // 通过myController.text获取输入
                content: Text(myController.text),
              );
            },
          );
        },
        tooltip:'',
        child: Icon(Icons.text_fields),
      )
    )
  }
}


如何设置输入框的提示文字:

  • 通过Text widget的装饰构造器参数 设置一个InputDecoration来展示“小提示”,或是占位符文字;
body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: 'placeholder'),
  )
)

如何显示验证错误信息:

  • InputDecoration除了可以添加提示信息,还可以添加错误信息提示;
String _errorText;
...
TextField(
  onSubmitted: (String text) {
    setState(() {
      if (!isEmail(text)) {
        _errorText = "Error";
      }else {
        _errorText = null;
      }
    })
  },
  decoration: InputDecoration(hintText: "", errorText: _getErrorText())
)

...

_getErrirText(){
  return _errorText;
}

bool isEmail(String emailString) {
  String emailRegexp = 
    r'正则';
  RegExp regExp = RegExp(emailRegexp);
  return regExp.hasMatch(emailString)
}

富文本展示:

  • 使用RickText widget;
List<TextSpan> spans = []

...
RickText(
  text: TextSpan(
    children: spans,
  ),
);

硬件调用、第三方服务、平台交互、通知

如何调用硬件和第三方服务:

  • 一般使用插件,一下是几款常用插件,如果插件不满足也可以自己开发
    • geolocator:访问位置信息的GPS插件
    • image_picker:访问相机 相册
    • Shared Preferences plugin:本地存储,iOS的UserDefaults、Android的Shared Preferences;
    • SQFlite:访问数据的插件
    • flutter_facebook_login:唤起Facebook第三方登录
    • firebase_messaging:用于推送通知,在插件库中可以搜索到更多的关于firebase plugin;

如何构建和继承Native SDK/模块:

  • 如果有Flutter或其他社区缺失的功能,可以利用Flutter插件架构:发出消息让接收者进行处理并将结果返回给您,在此情况下,接收者将是iOS或Android 的SDK或功能模块;

Flutter必备基础知识

构建Flutter项目

学习项目参考:

  • https://github.com/flutter/flutter/tree/master/examples
  • https://github.com/nisrulz/flutter-examples
  • https://github.com/iampawan/FlutterExampleApps
  • flutter sdk 安装目录下的 examples文件夹;

多看项目,多参考;

图片控件开发

Image Widget:

  • 加载网络图片
  • 加载静态图片
  • 加载本地图片
  • 设置PlaceHolder
  • 配置图片缓存
  • 加载Icon

Image构造函数:

  • new Image 从ImageProvider获取图像
  • new Image.asset 使用key从AssetsBundle获取图像
  • new Image.network 从网络URL获取图像
  • new Image.file 从本地文件获取图像
  • new Image.memory从内存获取图像

如果是多套UI资源图片,请使用new Image.asset并通过AssetImage指定图像;

// 网络图片
Image.network('url')

// 静态图片 并处理不同分辨率
// 1.在pubspec.yaml文件中声明图片资源路径;
// 2.使用AssetImage访问图片;或 使用Image.asset加载;

// pubspec.yaml
assets:
  - images/my_icon.png

// 使用AssetImage
Image(
  height:100,
  width:100,
  image: AssetImage("images/my_icon.png"),
)

// 使用Image.asset
Image.asset(
  "images/my_icon.png",
  height:100,
  width:100,
)


// 加载本地图片(如手机SD卡)
import 'dart:io';
Image.file(File('/sd/Download/Stack.png'))

// 加载相对路径的本地图片
// 1.在pubspec.yaml中添加 path_provider 插件,处理路径前缀;
// 2.编写代码
import 'dart.io'
import 'package:path_provider/path_provider.dart';

// Image.file(File('/sd/Download/Stack.png'))
FutureBuilder(
  future: _getLocalFile('Download/Stack.png'),
  builder: (BuildContext context, AsyncSnapshot<File> snapshot) {
    return snapshot.data != null ? Image.file(snapshot.data) : Container();
  }
)

// 获得SDCard路径
Future<File> _getLocalFile(String filename) async {
  String dir = (await getExternalStorageDictionary()).path;
  File f = new File('$dir/$filename');
  return f;
}


// 设置PlaceHolder:需要借助 FadeInImage widget,他能从内存,本地资源中加载PlaceHolder

// 从内存加载的话:需要安装从内存读取数据的插件 transparent_image
import 'package:transparent_image/transparent_image.dart';

...
FadeInImage.memoryNetWork(
  placeholder: kTransparentImage,
  image:'url'
)

// 再从本地资源中加载PlaceHolder
assets:
  - assets/loading.gif

FadeInImage.assetNetwork(
  placeholder: 'assets/loading.gif',
  image: 'url'
)


// 配置网络图片缓存:需要借助 cached_network_image 插件,来从网络上加载图片,并将其缓存到本地
import 'package:cached_network_image/cached_network_image.dart';

CachedNetworkImage(
  placeholder: CircularProgressIndcator(),
  imageUrl:'url'
)

加载Icon:

const Icon(
  this.icon, // IconData(可以是自己构造的 也可以是 Flutter提供的 material_fonts)
  {
    Key key,
    this.size,
    this.color,
    this.semanticLabel, // 标志位
    this.textDirection // 绘制方向
  }
)

// 使用内置图标
new Icon(Icon.adnroid, size: 100.0)

// 使用自定义Icon:需要构造一个IconData
const IconData(
  this.codePoint, // 必填参数 fonticon对应的16进制 Unicode
  {
    this.fontFamily, // 字体库系列
    this.fontPackage, // 字体所在包 不填仅在程序包中查找
    this.matchTextDirection: false, // 是否按照绘制方向显示
  }
)

// 首选 类似字体的使用 配置先配置icon
fonts:
  - family: devio
    fonts:
      - asset: fonts/devio.ttf

// 其次 是使用
new Icon(
  new IconData(0xf5566, fontFamily: "devio"),
  size: 100.0,
  color: Colors.blue
)


动画Animation
  • Flutter中有哪些类型的动画
  • 如何使用动画库的基础类给widget添加动画
  • 如何为动画添加监听器
  • 什么时候使用AnimatedWidget与AnimatedBuilder
  • 如何使用Hero动画;

Flutter中有哪些类型的动画:

  • 两类:基于tween 和 基于物理的;
    • tween动画:定义开始和结束点,时间线 以及 定义转换时间和速度的曲线,再由框架计算如何从开始点过渡到结束点;
    • 基于物理的动画:模拟真实物体行为;

如何使用动画库的基础类给widget添加动画:

  • Animation:核心库,生成指导动画的值;
  • CurvedAnimation:Animation的子类,将过程抽象为一个非线性曲线;
  • AnimationController:Animation的子类,用来管理Animation;
  • Tween:在正在执行动画的对象所使用的数值范围之间生成值;

Animation:

  • Animation本身与UI渲染无关;他是个抽象类,比较常用的是Animation<double>
  • Animation对象是在一段时间内依次生成一个区间之间值的类;其输出可以是线性的、曲线的、一个步进函数或 任何其他可设计的映射;
  • 设置Animation控制方式,动画可以反向运行,甚至在中间切换方向;
  • Animation可以生成Animation<double>外的其他类型,如Animation<Color>Animation<Size>
  • Animation对象是有状态的,可以通过访问其value属性获取动画的当前值;

CurvedAnimation:

final CurvedAnimation: curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);

// Curves.easeIn是已经定义好的,也可以自行创建
class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}

AnimationController:

  • 特殊的Animation对象,屏幕刷新一帧就会生成一个新值;
  • 默认情况,AnimationController在给定时间内会线性的生成从0.0~1.0的数字;
// vsync参数 会防止widget显示区域外动画消耗不必要的资源,可以将stateful对象作为vsync的值;
final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);

  • AnimationController派生自Animation<double>,可以在需要Animation对象的地方使用;
  • AnimationController具有控制动画的其他方法:
    • forward() 启动动画
    • reverse({double from}) 倒放动画
    • reset() 重置动画
    • stop({bool canceled = true}) 停止动画

Tween:

  • AnimationController对象的范围是从0~1(可能会超出);如果需要不同的范围或不同的数据类型,这可以使用Tween(不同范围、不同数据类型);
  • Tween是无状态的,需要begin和end值;它的唯一职责就是定义从输入范围到输出范围的映射;
  • Tween继承自Animatable<T>,Animatable输出的比一定是double值;
final Tween doubleTween = new Tween<double>(begin:-200.0, end: 0.0);

final Tween colorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54);
  • Tween对象不存储任何状态;它提供了evaluate(Animation<double> animation)方法将映射函数应用于动画的当前值;Animation对象的当前值可以通过value()方法取到;

Tween.animate:

  • 可调用Tween对象的animate()方法,传入一个控制器对象;该方法返回的是一个Animation对象;
final AnimationController controller = new AnimationController(duration: const Duration(millisecounds: 500), vsync: this);

final Animation curve = new CurvedAnimation(parent: controller, curve: Curves.easeOut);

Animation<int> alpha = new IntTween(begin:0, end:255).animate(curve);

为widget添加动画:

// 图片放大
import 'package:flutter/animation.dart';
import 'packaget:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTockerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;
  AnimationStatus animationState;
  double animationValue;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);

    animation = Tween<double>(begin: 0, end: 300).animate(controller)..addListener(() {
      setState((){
        // setState方法必须存在触发页面重新渲染 动画在能有效
        animationValue = animation.value;
      });
    })..addStatusListener((AnimationStatus state) {
      setState(() {
        animationState = state;
      });
    })
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 50),
      child: Column(
        children: <Widget>[
          GestrueDetector(
            onTap:(){
              controller.reset();
              controller.forward();
            },
            child: Text('Start', textDirection: TextDirection.ltr),
          ),
          Text('State:' + animationState.toString(), textDirection: TextDirection.ltr),
          Text('Value:' + animationValue.toString(), textDirection: TextDirection.ltr),
          Container(
            height: animation.value,
            width: animation.value,
            child: FlutterLogo(),
          )
        ]
      )
    )
  }

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

// 除了上述实现 也可以使用 AnimatedWidget来实现

为动画添加监听器:

  • 需要知道动画指定进度和状态,通过Animation的addListeneraddStatusListener方法添加监听器;
    • addListener 动画的值变化
    • addStatusListener 动画状态发生变化
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);

animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    controller.reverse()
  }else if (status == AnimationStatus.dismissed) {
    controller.formard();
  }
})
..addListener(() {
  setState(() {

  })
};

controller.forward();

使用AnimatedWidget与AnimatedBuilder简化和重构对动画的使用:

class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key key, Animation<double> animation})
    : super(key: key, listenable: animation);
  
  // 动画值变化 就会调用build
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: new Container(
        margin: new EdgeInsets.symmetric(vertical: 10.0),
        height: animation.value,
        width: animation.value,
        child: new FlutterLogon(),
      )
    )
  }
}

class LoginApp extends StatefulWidget {
  _LogoAppState createState() => new _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTockerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this
    );
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return new AnimatedLogo(animation: animation);
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }

}

void main(){
  runApp(new LogoApp());
}

AnimatedBuilder:

  • 用于构建动画的通用widget,AnimatedBuilder:对于希望将动画作为更大构建函数的一部分包含在更负载的widget时很有用;
  • 可以理解为:AnimatedBuilder是拆分动画的一个工具类,将动画和widget分离;
AnimatedBuilder(
  animation: animation,
  builder: (context, child) => Container(
    height: animation.value,
    width: animation.value,
    child: child,
  )
  child: child
)

Hero动画:

  • 通过两个页面都有的元素,如图片(hero),实现页面到页面之间的过渡(淡入淡出);
import 'package:flutter/material.dart';
import 'package:flutter/schedule.dart' show timeDilation;

class PhotoHero extends StatelessWidget {
  const PhotoHero({Key key, this.photo, this.onTap, this.width }) : super(key: key);

  final String photo;
  final VoidCallBack onTap;
  final double width;

  Widget build(BuildContext context) {
    return SizeBox(
      width: width,
      child: Hero(
        tag: photo,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            child: Image.network(
              photo,
              fit: BoxFit.contain,
            )
          )
        )
      )
    )
  }
}

class HeroAnimation extends StatelessWidget {
  Widget build(BuildContext context) {
    // speed
    timeDilation = 10.0;

    return Scaffold(
      appBar:...
      body: Center(
        child: PhotoHero(
          photo: 'url',
          width: 300.0,
          opTap: () {
            Navigator.of(context).push(MaterialPageRoute<void>(
              builder: (BuildContext context) {
                return Scaffold(
                  appBar...
                  body: Container(
                    color: Colors.lightBlueAccent,
                    padding: const EdgeInsets.all(16.0),
                    alignment: Alignment.topLeft,
                    child: PhotoHero(
                      photo: 'url',
                      width: 100.0,
                      opTap: () {
                        Navigator.of(context).pop();
                      }
                    )
                  )
                )
              }
            ))
          }
        )
      )
    )
  }
}

void main() {
  runApp(MaterialApp(home: HeroAnimation()));
}

Hero原型参数:

  • tag 关联两个Hero动画的标识;
  • createRectTween (可选)定义Hero的边界,从开始到结束 过渡过程中该如何变化;
  • child 定义动画所呈现的widget;

实现径向hero动画:

  • 原型变方形,方形变原型;
  • 利用 createRectTween
Flutter调试技巧

Debug模式运行:

  • 断点调试
  • Variables视窗、Watches视窗:变量列表
  • Frames视窗:函数调用栈
  • Flutter控制台 console 打印输出

各自平台的log信息使用相应的IDE打开app调试即可;

Flutter基础实践

注:以对widget的介绍和使用为主要内容,不做长篇幅代码段的整理;

App导航框架 与 常用功能实现

首页Tab导航框架:

  • Scaffold:BottomNavigationBar
  • PageView:PageController

Scaffold是一个实现了基本的material design的布局结构:

class Scaffold extends StatefulWidget {
  const Scaffold({
    Key key,
    thos.appBar,
    this.body,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.floatingActionButtonAnimator,
    this.persistentFooterButtons,
    this.drawer,
    this.endDrawer,
    this.bottomNavigationBar,
    this.bottomSheet,
    this.backgroundColor,
    this.resizeToAvoidBottomPadding = true,
    this.primary = true,
  }) : assert(primaru != null), super(key:key);
}

AppBar、TabBarView、BottomNavigationBar、AppBar、TabBarView、BottomNavigationBarItem、DrawHeader;

PageView是一个可以完成页面之间滚动的widget:

class PageView entends StatefulWidget {
  PageView({
    Key key,
    this.scrollDorection = Axis.horizontal,
    this.reverse = false,
    PageController controller,//pageView的控制类
    this.physics,
    this.pageSnapping = true,
    this.onPageChanged,
    List<Widget> children = const <Widget>[],
  }): controller = controller ?? _defaultPageController,
    childrenDelegate = SliverChildListDelegate(children),
    super(key :key);
}

Banner可以使用轮播组件:flutter_swiper

Container(
  height:160,
  child: Swiper(
    itemCount: _imageUrls.length,
    autoplay: true,
    itemBuilder: (BuildContext context, int index) {
      return Image.network(
        _imageUrls[index],
        fit: BoxFit.fill,
      );
    },
    pagination: SwiperPageination(),//指示器
  ),
)

去掉顶部非安全区的留白:

  • MediaQuery.removePadding(removeTop: true, context: context, child: ListView...)

监听ListView列表滚动:

_onScroll(offset){
  print(offset);
}

NotificationListener(onNotification:(scrollNotification){
  if (scrollNotification is ScrollUpdateNotofication && 
    scrollNotification.depth == 0 //仅第0个组件ListView被监听(因为可能会有更深一层的ListView滑动被监听 所以这里要进行过滤)
    ){
    // 调用自定义函数
    _onScroll(scrollNotification.metrics.pixels);
  }
}, child:ListView )

在ListView上放一个自定义的appBar,实现ListView向上滚动时隐藏appBar:

  • 堆叠组件Stack
Stack(
  children: <Widget>[
    MediaQuery.removePadding(
      removeTop: true,
      context: context,
      child: NotificationListener(
        if...
      ),
      child: ListView...
    ),
    Opacity(
      opacity: 1,// 根据监听值进行设置
      child: Container(
        height: 80,
        decoration: BoxDecoration(color: Colors.white),
        child: Center(
          child: Padding(padding: EdgeInset.only(top: 20),
          child: Text('首页'),
          )
        )
      )
    )
  ]
)

Flutter进阶提升:网络编程与数据存储技术

HTTP:

  • 引入和安装http插件;

Future、FutureBuilder:

  • Future是与异步操作一起工作的核心Dart类;用于表示未来某个时间可能会出现的可用值或错误;(类似于ES6的Promise,提供then和catchError的链式调用);
  • Future是dart:async包的一个类,使用它时需要导入包;
  • Future有两种状态:
    • pending 执行中
    • completed 执行成功/失败

Future用法:

  • future.then
  • async await
  • future.whenComplete
  • future.timeout
// 示例1:捕获异常
import 'dart:async';

Future<String> testFuture() {
  return Future.value('success');
  // or
  return Future.error('error');
}

testFuture().then((s){

}, onError: (e){

}).catchError((e){
  // catchError与onError 同时存在时 只会调用onError
}).whenComplete(() {
  print('done')
});

// 示例2:异步调用
import 'dart:async';

test() async {
  int result = await Future.delayed(Duration(milliseconds:2000), () {
    return 123;
  })
  print('t3' + DateTime.now().toString());
}

print('t1' + DateTime.now().toString());
test()
print('t1' + DateTime.now().toString());


// 示例3:设置超时时间
import 'dart:async';
Future.delayed(Duration(milliseconds:2000), () {
    return Future.value(1);
}).timeout(new Duration(seconds: 5)).then((s){

}).catchError((e){

});

FutureBuilder:

  • 是一个将异步操作和异步UI更新结合在一起的类;
  • 在创建FutureBuilder对象时,将Future对象作为要处理的异步计算模式传递;在builder函数中,检查connectionState的值,并使用 AsyncSnapshot中的数据或错误返回不同的窗口小部件;

构造方法解析:

  • future:future对象表示FutureBuilder构造器当前连接的异步;
  • initialData:表示一个非空的Future完成前的初始化数据;
  • builder:AsyncWidgetBuilder类型的回调函数,是一个基于异步交互构建widget的函数;
    • builder函数接收两个参数:BuildContext contextAsyncSnapshot<T> snapshot,返回一个widget;
    • snapshot包含异步计算信息,具有以下属性:
      • connectionState:枚举值,表示异步计算的连接状态,四个值分别为none waiting active done;
      • data:异步计算接收的最新数据
      • error:异步计算接收的最新错误对象;
      • hasData和hasError:分别检查它是否包含非空数据值或错误值;
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

...

class _MyAppState extends State<MyApp> {
  String showResult = '';

  Future<CommonModel> fetchPost() async {
    final response = await http.get('url');
    Utf8Decoder utf8decoder = Utf8Decoder();
    var result = json.decode(utf8Decoder.convert(response.bodyBytes));
    return CommonModel.fromJson(result);
  }

  @override
  Widget build(BuildContext context) {
    ...

    body: FutureBuilder<CommonModel>(
      future: fetchPost(),
      builder: (BuildContext context, AsyncSnapshot<CommonModel> snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.none:
            return new Text('Input an URL to start');
          case ConnectionState.waiting:
            return new Center(child: new CircularProgressIndeicator());
          case ConnectionState.active:
            return new Text('');
          case ContextionState.done:
            if (snapshot.hasError) {
              return new Text(
                '${shapshot.error}',
                style: TextStyle(color: Colors.red),
              );
            }else {
              return new Column(children: <Widget>[
                Text('Icon: ${snapshot.data.icon}'),
                Text('Title: ${snapshot.data.title}')
              ])
            }
        }
      }

    )

    ...
  }
}


将Response转换为Dart object:

// Model类 允许通过json创建Model对象
class CommonModel {
  final String icon;
  final String title;
  final String url;
  final String statusBarColor;
  final bool hideAppBar;

  CommonModel({
    this.icon, 
    this.title, 
    this,url, 
    this.statusBarColor, 
    this.hideAppBar
  });

  // 工厂构造函数
  factory CommonModel.fromJson(Map<String, dynamic> json) {
    return CommonModel(
      icon: json['icon'],
      title: json['title'],
      url: json['url'],
      statusBarColor: json['statusBarColor'],
      hideAppBar: json['hideAppBar'],
    )
  }
}

JSON解析和模型转换:

  • 使用dart:convert package将Future<Post>响应的内容转化为一个json Map;
  • 再使用上面工厂构造函数将json Map转化为一个CommonModel对象;
Future<CommonModel> fetchPost() async {
  final response = await http.get('');
  final result = json.decode(response.body);
  return new CommonModel.fromJson(result);
}

fetchPost().then(
  (CommonModel value) {
    setState(
      () {
        resault = value;
      }
    )
  }
)

JSON序列化方式:

  • 小型项目:手动序列化;(更通用)
  • 大型项目:借助插件json_serializablebuilt_value

简单json解析:

String jsonStr = '{...}';
Map<String, dynamic> map = JSON.decode(jsonStr);

复杂json解析:

// 嵌套结构的json解析
class TravelTabModel {
  final String url;
  final List<TravelTab> tabs;

  TravelTabModel({this.url, this.tabs});

  factory TravelTabModel.fromJson(Map<String, dynamic> json) {
    String url = json['url'];
    List<TravelTab> tabs = (json['tabs'] as List).map((i)=> TravelTab.fromJson(i));
    return TravelTabModel(url:url, tabs:tabs);
  }
}

class TravelTab {
  // 也可以修改为final修饰的方式(参上)
  String labelName;
  String groupChannelCode;

  TravelTab({this.labelName, this.groupChannelCode});

  TravelTab.fromJson(Map<String, dynamic> json) {
    labelName = json['labelName'];
    groupChannelCode = json['groupChannelCode'];
  }
}

也可以通过在线的json转dart工具进行代码转换;

shared_preferences本地存储:

  • shared_preferences本地存储插件;
    • 简单 异步 持久化的key-value存储,iOS是基于NSUserDefault的;
  • 在pubspec.yaml添加依赖:
    • dependencies:shared_preferences: ^0.5.1+1
    • 执行命令安装:flutter packages get
  • 需要时导入包:import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences/shared_preferences.dart';

final prefs = await SharedPreferences.getInstance();

prefs.setInt('counter',counter);
final counter = prefs.getInt('counter') ?? 0;
prefs.remove('counter');

shared_preferences常用API:

  • setString
  • setBool
  • setDouble
  • setInt
  • setStringList
  • get (获取的是动态类型dynamic)
  • getBool
  • getDouble
  • getInt
  • getKeys
  • getString
  • getStringList
列表组件

ListView相关知识点:

  • ListView、ListView点击事件、ListView动态更新、ListView.Builder构建列表;

ListView水平滚动和垂直滚动:

body: ListView(
  // scrollDirection: Axis.horizontal, //默认为纵向可滑动 主轴设置为水平轴 则横向可滑动
  children: _buildList()
)

List<Widget> _buildList() {
  return CITY_NAMES.map((city)=> _item(city)).toList();
}

Widget _item(String city) {
  return Container(
    height:80,
    margin: EdgeInsets.only(bottom: 5),
    alignment: Alignment.center,
    decoration: BoxDecoration(color: Color.teal),
    child: Text(
      city,
      style: TextStyle(color: Colors.white, fontSize: 20),
    ),
  )
}

ExpansionTile实现可展开列表:

// 构造函数
const ExpansionTile({
  Key key,
  this.leading,// 标题左侧 要展示的widget
  @required this.title,// 标题widget
  this.backgroundColor,// 北京色
  this.onExpansionChanged,//列表 展开收起的回调函数
  this.children = const <Widget>[],//列表展开时展示的widgets
  this.trailing,//标题右侧需要展示的widget
  this.initiallyExpanded = false,//是否默认展开
})

// 示例
List<Widget> _buildList() {
  List<Widget> widgets = [];
  CITY_NAMES.keys.forEach((key) {
    widgets.add(_item(key, CITY_NAMES[key]));
  });
  return widgets;
}

Widget _item(String city, List<String> subCities) {
  return ExpansitonTitle(
    title: Text(
      city,
      style: TextStyle(color: Color.black54, fontSize:20),
    ),
    children: subCities.map((subCity)=>_buildSub(subCity)).toList(),
  )
}

Widget _buildSub(String subCity) {
  // 可伸缩的box 是为了让宽度撑满
  return FractionallySizedBox(
    widthFactor: 1,
    child: Container(
      height: 50,
      margin: EdgeInsets.only(bottom: 5),
      decoration: BoxDecoration(color: Color.lightBlueAccent),
      child: Text(subCity),
    )
  )
}

GridView实现网格列表:

  • 通常使用GridView.count构造函数来创建一个GridView;
body: GridView.count (
  crossAxisCount: 2,//设置显示多少列
  children: _buildList(),
)

...

下拉刷新和上拉加载:

  • RefreshIndicator:下拉竖线widget;
body: RefreshIndicator(
  onRefresh: _handleRefresh,
  child: ListView(...)
)

Future<Null> _handleRefresh() async {
  await Future.delayed(Duration(seconds:2));
  setState((){
    cityNames = cityNames.reversed.toList();
  });
  return null;
}
  • 上拉加载需要借助ScrollController,列表支持设置controller参数,通过ScrollController监听列表滚动的位置,来实现加载更多的功能;
class _MyAppState extends State<MyApp> {
  ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    _scrollController.addListener((){
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent){
        _loadData();
      }
    });

    super.initState();
  }  

  @override
  void dispise() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget buid(BuildContext context) {
    ...
    body: RefreshIndicator(
      onRefresh: _handleRefresh,
      child: ListView(
        controller: _scrollController,
        children: _buildList()
      )
    )
    ...
  }
}

_loadData() async {
  await Future.delayed(Duration(milliseconds: 200));
  setState((){
    // copy
    List<String> list = List<String>.from(cityNames);
    // 继续追加 
    list.addAll(cityNames);
    cityNames = list;
  })

}

...

Flutter进阶提升: Flutter、Native混合开发

混合方式:

  • 页面间独立:一个页面时原生的、一个页面时Flutter的;
  • 页面内嵌入:原生页面嵌入flutter组件,或flutter页面嵌入原生View;

集成步骤:

  • 创建Flutter module(Flutter module会有用于调试的宿主iOS、Android工程)
  • 在原生中添加Flutter module依赖
  • 在原生中调用Flutter module
  • 编写Dart代码;(lib目录)
  • 运行项目
  • 热重启、热加载
    • 结合原生混合开发的Flutter项目需要开启热加载、热重启时,首先需要打开一个模拟器或链接一台设备,然后关闭App,在flutter_module根目录运行命令flutter attach <模拟器ID>
    • 运行项目,使用Flutter开发的模块,使用r或R即可完成热重启和热加载;
  • 调试Dart(混合开发的项目)
    • AndroidStudio,关闭App,点击原调试按钮旁的Flutter attach按钮,开启调试dart代码;
  • 发布应用

创建Flutter module:

// 假设Native项目如:xxx/flutter_hybrid/Native项目
cd xxx/flutter_hybrid/
flutter create -t module flutter_module
// 这会切换到Android/iOS的上一级目录 并创建一个flutter模块

// flutter_module中文件:
//    .android .ios分别为flutter_module的宿主工程
//    lib 是 flutter_module的Dart部分代码
//    pubspec.yaml 是flutter_module项目依赖配置文件

// flutter_module在不增加额外配置时,是可以独立运行的;

与android的混合开发

与ios的混合开发:

  • 为已经存在的iOS应用添加Flutter module依赖;
  • 需要配置CocoaPods;
    • 初始化Podfile:pod init
    • 然后添加依赖,内容如下;
target 'FlutterHybridiOS' do
  flutter_application_path='../flutter_module/'
  eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

  target 'FlutterHybridiOSTests' do
    inherit! :search_paths
  end

  target 'FlutterHybridiOSUITests' do
    inherit! :search_paths
  end
end
  • 在iOS项目的根目录运行:pod install,安装依赖;
  • 如果在FLutter中引入其他插件,也需要重新运行一次pop install以在iOS项目中刷新依赖项;
  • 关闭Bitcode;
  • 添加build phase以构建Dart代码:
    • Targets->New Run Script Phase;
    • 创建一个 build phase,Run Script,添加如下内容;
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" buiObject-cld
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
  • Run Script移动到紧挨着Target Dependencies phase下面;之后就可以通过commend+B构建项目了;

在OC中调用Flutter module

  • 方式1:直接使用FlutterViewController
  • 方式2:使用FlutterEngine
// 使用FlutterViewController
#import <Flutter/Flutter.h>
#import "AppDelegate.h"
#import "ViewController.h"
#import <FlutterpluginRegistrant/GeneratedPluginRegistrant.h>

FlutterViewController * flutterViewController = [FlutterViewController new];

// 如果我们创建的FlutterViewController 中使用了flutter插件 则还需要注册插件管理器
GeneratedPluginRegistrant.register(with: flutterViewController);
// 这里传递的字符串 可以是一个json结构 主要用于传参
[flutterViewController setInitialRoute:@"route1"];

[self presentViewController:flutterViewController animated:true completion:nil];

import 'dart:ui'
import 'package:flutter/material.dart';

// window.defaultRouteName 用于接收 flutter初始化的参数
void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String route) {
  switch (route) {
    case 'route1':
      return SomeWidget(...);
    case 'route2':
      return SomeWidget(...);
    default:
      return Center(
        child: Text('Unknown route: $route', textDirection: TextDirection.ltr);
      )
  }
}


// 使用FlutterEngine
// AppDelegate.h
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate: FlutterAppDelegate
@property (nonatomic, strong) FlutterEngine * flutterEngine;
@end

// AppDelegate.m
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>
#import "AppDelegate.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
  [self.flutterEngine runWithEntrypoint:nil];
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];

  return ... 
}


// 使用
FlutterEngine * flutterEngine = [(AppDelegate *)[UIApplication sharedApplication] delegate] flutterEngine];
FlutterViewController * flutterViewController = [[FlutterViewController alloc] initWithEngine:flutterEngine];
// native在初始化flutter模块时 传递参数
// 但是基于FlutterEngine方式的 Route参数传递 会失效(bug)
[flutterViewController setInitialRoute:@"route1"];
[self presentViewController:flutterViewController animated:true completion:nil];

单独运行Flutter module编写Dart部分代码即可;

Flutter与Native通信

场景:

  • 如flutter调用原生的某些功能,如相册或其他复杂计算;

方式:

  • 原生向Flutter传递初始化参数
  • 原生主动向Flutter传递参数
  • Flutter主动向Native传递参数
  • Flutter与Native双向传递参数

通信机制:channel

  • 支持数据类型如下;
DartAndroidiOS
nullnullnil
boolBooleanNSNumber numberWithBool
intIntegerNSNumber numberWithInt
int(64)LongNSNumber numberWithLong
doubleDoubleNSNumber numberWithDouble
StringStringNSString
Uint8Listbyte[]FlutterStandardTypedData typedDataWithBytes
Int32Listint[]FlutterStandardTypedData typedDataWithInt32
Int64Listlong[]FlutterStandardTypedData typedDataWithInt64
Float64Listdouble[]FlutterStandardTypedData typedDataWithFloat64
ListArrayListNSArray
MapHashMapNSDictionary

channel类型:

  • BasicMessageChannel:
    • 用于传递字符串和半结构化信息
    • 持续通信,收到消息后可回复此消息
    • 双向通信
  • MethodChannel
    • 用于传递方法调用(method invocation)
    • 一次性通信,如调用拍照
    • Dart调用Native方法
  • EventChannel
    • 用于数据流通信(event streams)
    • 持续通信
    • 但是无法回复;
    • 通常用于Native向Dart的通信;如手机电量、各种传感器数据等;

信息的发起方 可以是Dart,也可以是Native;

Dart端:BasicMessageChannel:

// 构造方法
const BasicMessageChannel(this.name, this.codec);
// String name 指Channel的名字 要和Native保持一致
// MessageCodec<T> codec 消息的编解码器,要和Native端保持一致,有四种类型的实现;

void setMessageHandler(Future<T> handler(T message))
// 注册处理器,用于接收Native端发来的消息

Future<T> send(T message)
// Dart用于向Native发送消息的方法,返回的Future<T>是消息发出后收到的Native回复的回调函数;

// 使用
import 'package:flutter/services.dart';

...
static const BasicMessageChannel _basicMessageChannel = const BasicMessageChannel('BasicMessageChannelPlugin', StringCodec());
...

_basicMessageChannel.setMessageHandler((String message) => Future<String>((){
  setState((){
    showMessage = message;
  });
  // 返回值是 回复给Native的
  return "收到Native消息:" + message;
}));

String response;
try{
  response = await _basicMessageChannel.send(value);
}on PlatformException catch(e){
  print(e);
}
...

Dart端:MethodChannel

// 构造方法
const MethodChannel(this.name, [this.codec = const StandardMethodCodec()])
// String name Channel的名字,要和Native端保持一致
// MethodCodec codec 消息的编解码器 默认是StandardMethodCodec,要和Native端保持一致;

Future<T> invokeMethod<T>(String method, [dynamic arguments])
// String method 要调用Native的方法名;
// [dynamic arguments] 调用Native方法传递的参数 可不传

// 使用
import 'package:flutter/services.dart';

...
static const MethodChannel _methodChannelPlugin = const MethodChannel('MethodChannelPlugin');

String response;
try{
  // Dart调用 原生的send方法 传参是value Native返回值对象为response;
  response = await _methodChannelPlugin.invokeMethod('send', value);
}on PlatformException catch(e) {
  print(e);
}
...

Dart端:EventChannel

// 构造方法
const EventChannel(this.name, [this.codec = const StandardMethodCodec()])
// String name Channel的名字,要和Native端保持一致
// MethodCodec codec 消息的编解码器 默认是StandardMethodCodec,要和Native端保持一致;

Stream<dynamic> receiveBroadcastStream([ dynamic arguments])
// dynamic arguments 初始化广播流时 向Native传递的数据
// 返回的Stream对象 还需要调用listen方法来完成注册 监听回调用于接收Native端传过来的消息
// 页面销毁时还需要调用Stream的cancel方法来取消监听;

// 使用
import 'package:flutter/services.dart';
...
static const EventChannel _eventChannelPlugin = EventChannel('EventChannelPlugin');
StreamSubscription _streamSubscription;
@override
void initState(){
  _streamSubscription = _eventChannelPlugin.receiveBroadcastStream().listen(_onToDart, onError: _onToDartError);

  super.initState();
}

@override
void dispose() {
  if (_streamSubscription != null){
    _streamSubscription.cancel();
    _streamSubscription = null;
  }
  super.dispose();
}

void _onToDart(message) {
  setState((){
    showMessage = message
  })
}
void _onToDartError(error){
  print(error);
}
...

与android通信开发指南:

  • BasicMessageChannel
  • MethodChannel
  • EventChannel

Android:BasicMessageChannel

BasicMessageChannel(BinaryMessenger messenger, String name, MessageCodec<T> codec) 
// BinaryMessenger messenger 消息信使 是消息的发送和接收的工具,传递flutterView实例
// name Channel名字 也是唯一标识符
// MessageCodec<T> codec 消息的编解码器 有四种类型的实现
  // BinaryCodec:最简单的一种 返回值和入参均为二进制(Android中为ByteBuffer,iOS中为NSData);使用BinaryCodec可以使传递内存数据块时在编解码阶段免于内存拷贝;
  // StringCodec:用于字符串与二进制之间的编解码,其编码格式为UTF-8;
  // JSONMessageCodec:用于基础数据与二进制数据之间的编解码,支持基础数据类型如列表、字典等,在iOS端使用JSONSerialization作为序列化的工具,而在Android端使用了其自定义的JSONUtil与StringCodec作为序列化工具;
  // StandardMessageCodec:默认解码器,支持基础数据类型、二进制数据、列表、字典;

void setMessageHandle(BasicMessageChannel.MessageHandler<T> handler)
// 在接收Dart端传递的消息时 需要为android端设置一个handler 消息处理器

// BasicMessageChannel.MessageHandler<T>的原型如下
public interface MessageHandler<T> {
  void onMessage(T var1, BasicMessageChannel.Reply<T> var2);
  // var1 指接收的消息内容
  // var2 是回复此消息的回调函数
}

// Android向Dart端发送消息
void send(T message)
void send(T message, BasicMessageChannel.Reply<T> callback)
  // message指传递给Dart的具体消息
  // callback是受到Dart回复的回调函数;

Android:MethodChannel

MethodChannel(BinaryMessenger messenger, String name, MethodCodec codec)

setMethodCallHander(@Nullable MethodChannel.MethodCallHandler handler)

public interface MethodCallHandler {
  void onMethodCall(MethodCall var1, MethodChannel.Result var2);
  // var1指消息内容  MethodCall有两个属性 var1.method表示方法名,call.arguments表示方法入参;
  // var2即result是回复此消息的回调函数对象,提供了 result.success、result.error、result.noImplemented方法调用;
}


Android:EventChannel

EventChannel(BinaryMessenger messenger, String name, MethodCodec codec)

void setStreamHandler(EventChannel.StreamHandler handler)

public interface StreamHandler {
  void onListen(Object args, EventChannel.EventSink eventSink);
  // args是传递的参数
  // eventSink是回调Dart的 提供 success error endOfStream三个回调方法对应事件不同状态;
  void onCancel(Object o);
  // Flutter取消监听时调用
}

Android-实践代码

与ios通信开发指南:

  • BasicMessageChannel
  • MethodChannel
  • EventChannel

iOS:BasicMessageChannel

+ (instancetype)messageChannelWithName:(NSSring*)name binaryMessenger:(NSObject<FlutterBinaryMessage>*)messenger codec:(NSObject<FlutterMessageCodec>*)codec;
// FlutterMessageCodec:
  // BinaryCodec
  // FlutterStringCodec
  // FlutterJSONMessageCodec
  // FlutterBinaryCodec 默认的

- (void)setMessageHandler:(FlutterMessageHandler _Nullable)handler;
// FlutterMessageHandler 消息处理器 处理Dart发送来的消息

// FlutterMessageHandler
typedef void (^FlutterMessageHandler)(id _Nullable message, FlutterReply callback);
  // message 是接收到的消息内容
  // reply是回复此消息的回调函数

// 向Dart端发送消息
- (void)sendMessage:(id _Nullable)message;
- (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback;


// 使用
// 接收Dart消息
- (void)initMessageChannel{
  self.messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"BasicMessageChannelPlugin" binaryMessenger:self.flutterViewController codec:[FlutterStringCodec sharedInstance]];
  MainViewController * __weak weakSelf = self;

  [self.messageChannel setMessageHandler:^(NSString* message, FlutterReply reply){
    // 向Dart回复
    reply([NSString stringWithFormat:@"BasicMessageChannel收到:%@", message]);
    // Native展示message
    [weakSelf sendShow:message];
  }];
}

// 向Dart发送消息
[self.messageChannel sendMessage:mesage reply:^(id _Nullable reply) {
  if (reply != nil) {
    [self sendShow:reply];
  }
}]

iOS:MethodChannel

+ (instancetype)methodChannelWithName:(NSString *)name binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
+ (instancetype)methodChannelWithName:(NSString *)name binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger codec:(NSObject<FlutterMethodCode>*)codec;

// Dart端调用iOS端的方法
- (void)setMethodCallHandler:(FlutterMethodCallHanlder _Nullable)handler;

// 使用
- (void)initMethodChannel {
  self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"MethodChannemPlugin" binaryMessenger:self.flutterViewController];
  MainViewController * __weak weakself = self;
  [self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result){
    if([@"send" isEqualToString:call.method]) {
      result([NSString stringWithFormat:@"MethodChannemPlugin收到:%@", call.arguments]);

      [weakSelf sendShow: call.arguments];
    }
  }]
}

iOS:EventChannel

+ (instancetype)eventChannelWithName:(NSString*)name binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
+ (instancetype)eventChannelWithName:(NSString*)name binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger codec:(NSObject<FlutterMethodCodec>*)codec;

- (void)setStreamHandler:(NSObject<FlutterStreamHandler>* _Nullable)handler;


@protocol FlutterStreamHandler
- (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink *)eventSink;
- (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments ;
// eventSink提供了 success error endOfStream回调方法对应事件的不同状态

// 使用

#import <Foundation/Foundation.h>
#import "MainViewController.h"
@interface MainViewController ()<FlutterStreamHandler>

@property (nonatomic) FlutterViewController * flutterViewController;
@property (nonatomic) FlutterEventChannel * eventChannel;
@property (nonatomic) FlutterEventSink eventSink;
@end

@implementation MainViewController

#pragma mark - sendMessage
- (void)sendMessage:(NSNotification*)notification {
  NSString * message = [notification.object valueForKey:@"message"];
  // 用EventChannel传递数据
  if (self.eventSink != nil) {
    self.eventSink(message);
  }
}

#pragma mark -navigator
- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender {
  if ([segue.destinationViewController isKindOfClass:[FlutterViewController class]]){
    self.flutterViewController = segue.destinationViewController;
    [self.flutterViewController setInitialRoute:self.inputParams];
    [self initChannel];
  }
}

#pragma mark - init Channel
- (void)initChannel {
  [self initEventChannel];
}

- (void)initEventChannel {
  self.eventChannel = [FlutterEventChannel eventChannelWithName:@"EventChannelPlugin" binaryMessenger:self.flutterViewController];
  // 设置消息处理器 处理来自Dart的消息
  [self.eventChannel setStreamHandler:self];
}

#pragma mark - <FlutterStreamHandler>
// 这个onListen是Flutter端开始监听这个channel时的回调,第二个参数EventSink是用来传输局的载体
- (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink *)eventSink{
  self.eventSink = eventSink;
  return nil;
}

// flutter不再接收
- (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments {
  self.eventSink = nil;
  return nil;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值