Flutter了解之入门篇4(包的使用、开发)

目录

  1. 使用包(共享的模块化代码)
  2. 开发Dart包
  3. 开发Flutter插件包
  4. Texture和PlatformView
一些公共库会被很多项目使用,将这些代码单独抽到一个独立模块,需要时直接集成,会大大提高开发效率。很多编程语言都支持这种“模块共享”机制:Java的jar包,Android的aar包,Web的npm包。

一个APP通常会依赖很多包,而这些包通常都有交叉依赖关系、版本依赖等,如果由开发者手动来管理依赖包会非常麻烦。因此,各编程语言官方都会提供一些包管理工具:Android用Gradle来管理依赖,iOS用Cocoapods来管理依赖,Node用npm。

Flutter包分为两类

  1. Dart包(不包含原生代码)
Dart代码编写(使用到了Flutter框架的特定功能,仅用于Flutter)。

如:fluro包。
/*
虽然Flutter的Dart运行时和Dart VM运行时不是完全相同,但是如果包中没有涉及这些存在差异的部分,那就可以同时支持Flutter和Dart VM。
如:dio包。
*/
  1. 插件包(包含原生代码)
包含了
  1. Dart代码。
  2. Android(使用Java或Kotlin)、iOS(使用OC或Swift)特定平台的原生代码。

如:
  battery插件包(获取电量)。
  image_picker插件包(访问相册和摄像头)。
插件简介

Flutter支持iOS、Android、Web、macOS、Windows、Linux等众多平台,但Flutter本质只是一个UI框架,本身无法提供一些系统功能(如:使用蓝牙、相机、GPS等),因此要在Flutter APP中调用特定平台的这些功能就必须依靠插件和原生平台进行通信。

Flutter官方和社区提供了一系列常用的插件
  // 官方提供,如:访问相机/相册、本地存储、播放视频等
  https://github.com/flutter/plugins/tree/master/packages。
  // 社区提供。
  https://pub.dev/ 

最小的Flutter包

  1. 一个pubspec.yaml文件。
声明了package的名称、版本、作者等的元数据文件。
注意:项目根目录下的pubspec.yaml文件是Flutter项目的默认配置文件,用来管理第三方依赖包。
示例:

name: hello
description: First Flutter application.
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
dev_dependencies:
  flutter_test:
    sdk: flutter
flutter:
  uses-material-design: true

说明:
    name:应用名/包名。
    description: 应用/包的描述、简介。
    version:应用/包的版本号。
      1. 忽略时为最新版本
      2. 指定范围:>=0.1.2 <0.2.0
      3.  0.1.x(x大于2):^0.1.2
      4. 指定版本:0.1.1
    dependencies:应用/包依赖的其它包或插件。
    dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
    flutter:flutter相关的配置选项。
  1. 一个lib文件夹。
需要公开的代码,最少应有一个<package-name>.dart文件

1. 使用包(包的3种依赖方式:Pub仓库、本地包、Git)

  1. 依赖Pub仓库 Pub

Pub仓库是Google官方的Dart Packages仓库,类似于node中的npm仓库,android中的jcenter。
在Pub上可以查找/发布 包。

示例(一个显示随机字符串的widget)

首先在pub上找到english_words包(包含数千个常用的英文单词以及一些实用功能),确定其最新的版本号和是否支持Flutter。

1. pubspec.yaml文件将“english_words”(3.1.5版本)添加到依赖项列表,如下:
  dependencies:
    flutter:
      sdk: flutter
    cupertino_icons: ^0.1.0
    # 新添加的依赖
    english_words: ^3.1.5

2. 下载包。
  1. 在Android Studio的编辑器视图中查看pubspec.yaml时,单击右上角的 Get dependencies 。
  2. 或在命令行执行:flutter packages get
这会将依赖包安装到项目,包的版本会保存在pubspec.lock。
/*
更新包:flutter packages upgrade

下载的包可以在项目的External Libraries目录下查看包的源码。
*/

3. 引入english_words包中的头文件。
import 'package:english_words/english_words.dart';

4. 使用english_words包来生成随机字符串。
class RandomWordsWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
   // 生成随机字符串
    final wordPair = new WordPair.random();
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: new Text(wordPair.toString()),
    );
  }
}
_MyHomePageState.build 的Column中
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    ... //省略无关代码
    RandomWordsWidget(),
  ],
)
Pub搜索english_words

pubspec.yaml添加依赖包

下载包

引入包

运行,每次保存字符串都会变化
css_colors包
  提供颜色相关功能,如:CSSColors.orange

url_launcher包
  打开浏览器,launch('https://wwww.baidu.com')

date_format包
  日期格式
  1. 依赖本地包
dependencies:
    pkg1:
        path: ../../code/pkg1
路径可以是相对的,也可以是绝对的。
  1. 依赖Git
包位于Git存储库的根目录中
dependencies:
  pkg1:
    git:
      url: git://github.com/xxx/pkg1.git

不在根目录中
dependencies:
  package1:
    git:
      url: git://github.com/flutter/packages.git
      path: packages/package1

发生冲突

// 都声明了Person类时会冲突
import ‘lib/person1.dart’;
import ‘lib/person2.dart’  as lib;

main(){
  Person person1=new Person();
  lib.Person person2=new lib.Person();
}

导入部分

方式一:导入需要的部分(使用show)
import ‘lib/person1.dart’ show Person;

方式二:隐藏不需要的部分(使用hide)
import ‘lib/person1.dart’ hide run;

延迟加载

需要的时候才去加载(可用来减少app启动时间)
import ‘lib/person1.dart’ deferred as hello;

func() async{
  // 加载库
  await hello.loadLibrary();

  hello.run();
}

分片

part 'personPart1.dart'
part 'personPart2.dart'

2. 开发Dart包(创建共享的模块化代码)

  1. 实现步骤

第一步:创建包

Android Studio:File>New>New Flutter Project->Flutter Package
或
终端命令使用 flutter create --template=package hello

包含 :
    lib/hello.dart:Package的Dart代码
    test/hello_test.dart:Package的单元测试代码。

第二步:实现包

对于纯Dart包,只需在lib/<package name>.dart文件内或lib目录中的文件中添加功能即可 。
要测试软件包,需要在test目录中添加unit tests。
例(看一下 shelf包的目录结构)

Package中主要的功能的源码都在src目录下。shelf Package也导出了一个迷你库: shelf_io,它主要是处理HttpRequest的。
在lib根目录下的“shelf.dart”中,导出了多个“lib/src”目录下的dart文件:
export 'src/cascade.dart';
export 'src/handler.dart';
export 'src/handlers/logger.dart';
export 'src/hijack_exception.dart';
export 'src/middleware.dart';
export 'src/pipeline.dart';
export 'src/request.dart';
export 'src/response.dart';
export 'src/server.dart';
export 'src/server_handler.dart';
shelf包的目录结构

第三步:生成文档

1. README.md
  介绍包
2. CHANGELOG.md 
  记录每个版本中的更改
3. LICENSE 
  许可条款
4. 所有公共API的API文档
  在发布软件包时会自动生成并发布到dartdocs.org

可使用 dartdoc 工具来为Package生成文档。
  开发者只需要遵守文档注释语法,在代码中添加文档注释,最后使用dartdoc生成API文档(一个静态网站)。
  文档注释是使用三斜线"///"开始,如:
    /// The event handler responsible for updating the badge in the UI.
    void updateBadge() {
    }

第四步:发布Package(到Pub上)

1. 检查pubspec.yaml、README.md以及CHANGELOG.md文件,以确保其内容的完整性和正确性。
2. 运行 dry-run 命令(查看是否都准备OK):
  flutter packages pub publish --dry-run
3. 验证无误后,发布
  flutter packages pub publish
如果遇到包发布失败的情况,先检查是否因为众所周知的网络原因,如果是网络问题,可以使用VPN,这里需要注意的是一些代理只会代理部分APP的网络请求,如浏览器的,它们可能并不能代理dart的网络请求,所以在这种情况下,即使开了代理也依然无法连接到Pub,因此,在发布Pub包时使用全局代理或全局VPN会保险些。如果网络没有问题,以管理员权限(sudo)运行发布命令重试。

很多时候开启全局代理也不会让terminal中的流量打代理服务器走,以socks5为例,应该在终端下输入以下指令:
    export all_proxy=socks5://127.0.0.1:1080
此时终端中的http和https流量会打代理服务器走,可以通过curl -i https://ip.cn指令查看代理设置是否成功。

最后一步:使用包

1. 添加依赖包
  dependencies: 
    hello:^1.1.0

2. 通过"package:"指令来指定包的入口文件:
  import 'package:hello/hello.dart';
  1. 依赖处理

处理包的相互依赖

如果正在开发一个hello包,它依赖于另一个包,则需要在hello/pubspec.yaml的dependencies中添加该依赖包:
dependencies: 
  url_launcher: ^0.4.2

现在可以在hello中import 'package:url_launcher/url_launcher.dart' 然后调用 launch()方法了。
但如果hello是一个插件包,其平台特定的代码需要访问url_launcher公开的特定于平台的API,那么还需要为特定于平台的构建文件添加合适的依赖声明:

1. Android
在hello/android/build.gradle配置文件中:
android {
    // lines skipped
    dependencies {
        provided rootProject.findProject(":url_launcher")
    }
}
现在可以在hello/android/src源码中import io.flutter.plugins.urllauncher.UrlLauncherPlugin访问UrlLauncherPlugin类。

2. iOS
在hello/ios/hello.podspec:
Pod::Spec.new do |s|
  # lines skipped
  s.dependency 'url_launcher'
现在可以在hello/ios/Classes源码中 #import "UrlLauncherPlugin.h" 然后访问 UrlLauncherPlugin类。

解决依赖冲突

假设在hello包中使用some_package和other_package,并且这两个包都依赖url_launcher,但是依赖的是url_launcher的不同的版本,这就有潜在的冲突了。

方法1(最好)
在指定依赖关系时,使用版本范围而不是特定版本,pub将能够自动解决问题。
dependencies:
  url_launcher: ^0.4.2    # 较好, 任何0.4.x(x >= 2)都可.
  image_picker: '0.1.1'   # 不好,只有0.1.1版本.

方法2
添加依赖覆盖声明,从而强制使用特定版本
dependencies:
  some_package:
dependency_overrides:
  url_launcher: '0.4.3'
但如果冲突的依赖不是一个包,而是一个特定于Android的库,比如guava,那么必须将依赖重写声明添加到Gradle构建逻辑中。
强制使用23.0版本的guava库,在hello/android/build.gradle中:
configurations.all {
    resolutionStrategy {
        force 'com.google.guava:guava:23.0-android'
    }
}

Cocoapods目前不提供依赖覆盖功能。

3. 开发Flutter插件包

  1. 创建插件包
flutter create --org com.baidu.www --template=plugin -i objc -a java flutter_package_app

说明:
  1. --org选项指定组织(反向域名),Android和iOS的标识符
  2. -a指定android语言 java、kotlin(默认)
  3. -i指定iOS语言 objc、swift(默认)
创建

生成的插件包中会包含以下文件及内容

================================
flutter_package_app.dart文件

import 'dart:async';
import 'package:flutter/services.dart';
class FlutterPackageApp {
  static const MethodChannel _channel =
      const MethodChannel('flutter_package_app');
  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

================================
FlutterPackageAppPlugin.h文件

#import <Flutter/Flutter.h>
@interface FlutterPackageAppPlugin : NSObject<FlutterPlugin>
@end

================================
FlutterPackageAppPlugin.m文件

#import "FlutterPackageAppPlugin.h"
@implementation FlutterPackageAppPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  FlutterMethodChannel* channel = [FlutterMethodChannel
      methodChannelWithName:@"flutter_package_app"
            binaryMessenger:[registrar messenger]];
  FlutterPackageAppPlugin* instance = [[FlutterPackageAppPlugin 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

================================
FlutterPackageAppPlugin文件

package com.baidu.www.flutter_package_app;
import ...
public class FlutterPackageAppPlugin implements FlutterPlugin, MethodCallHandler {
  private MethodChannel channel;
  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_package_app");
    channel.setMethodCallHandler(this);
  }
  public static void registerWith(Registrar registrar) {
    final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_package_app");
    channel.setMethodCallHandler(new FlutterPackageAppPlugin());
  }
  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getPlatformVersion")) {
      result.success("Android " + android.os.Build.VERSION.RELEASE);
    } else {
      result.notImplemented();
    }
  }
  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }
}
  1. 插件实现原理

Flutter应用包括原生代码和Flutter代码两部分。

Flutter中提供了平台通道,用于Flutter和原生平台的通信。

Flutter与原生之间的通信本质上是一个远程调用(RPC),通过消息传递实现:
    1. 应用的Flutter部分通过平台通道将消息发送到宿主应用(iOS或Android)。
    2. 宿主监听平台通道,并接收该消息,然后调用该平台的API,并将响应发送回Flutter。
消息传递是异步的,确保用户界面在消息传递时不会被挂起(卡顿)。
/*
平台特定/特定平台/原生平台中的平台指的就是Flutter应用程序运行的平台,如Android或IOS。

在Flutter中,MethodChannel API 可以发送与方法调用相对应的消息。 在原生平台上,MethodChannel Android API 和 FlutterMethodChannel iOS API可以接收方法调用并返回结果。这些类可以用很少的代码就能开发平台插件。
方法调用(消息传递)可以是反向的,即原生平台调用Dart中实现的API。如: quick_actions插件。

除了上面提到的MethodChannel,还可以使用BasicMessageChannel,它支持使用自定义消息编解码器进行基本的异步消息传递。 此外,可以使用专门的BinaryCodec、StringCodec和 JSONMessageCodec类,或创建自己的编解码器。
*/
使用平台通道在Flutter(client)和原生(host)之间传递消息

平台通道数据类型支持

平台通道使用标准消息编/解码器对消息进行编解码,它可以高效的对消息进行二进制序列化与反序列化。
当在发送和接收值时,这些值在消息中的序列化和反序列化会自动进行。
平台通道数据类型支持

获取平台信息

如果想根据宿主平台添加一些差异化的功能,则需要获取当前平台信息。

Flutter中提供了一个全局变量defaultTargetPlatform来获取当前应用的平台信息,defaultTargetPlatform定义在"platform.dart"中,它的类型是TargetPlatform,这是一个枚举类,定义如下:
enum TargetPlatform {
  android,
  fuchsia,
  iOS,
}
目前Flutter只支持这三个平台。

判断平台:if(defaultTargetPlatform==TargetPlatform.android){}
由于不同平台有它们各自的交互规范,Flutter Material库中的一些组件都针对相应的平台做了一些适配,比如路由组件MaterialPageRoute,它在android和ios中会应用各自平台规范的切换动画。
那如果想让APP在所有平台都表现一致,比如希望在所有平台路由切换动画都按照ios平台一致的左右滑动切换风格该怎么做?Flutter中提供了一种覆盖默认平台的机制,可以通过显式指定debugDefaultTargetPlatformOverride全局变量的值来指定应用平台。
比如:通过显示指定平台debugDefaultTargetPlatformOverride=TargetPlatform.iOS;
print(defaultTargetPlatform); // 会输出TargetPlatform.iOS
上面代码即在Android中运行后,Flutter APP就会认为是当前系统是iOS,Material组件库中所有组件交互方式都会和iOS平台对齐,defaultTargetPlatform的值也会变为TargetPlatform.iOS。

示例(通过平台通道 获取电池电量)

在Dart中通过getBatteryLevel 调用Android BatteryManager API和iOS device.batteryLevel API

第一步:创建一个新的应用程序项目

// 默认情况下,模板支持使用Java编写Android代码,或使用Objective-C编写iOS代码。
// 要使用Kotlin或Swift,请使用-i和/或-a标志:  flutter create -i swift -a kotlin batterylevel
在终端中运行:flutter create batterylevel

第二步:创建Flutter平台客户端

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

// 该State类拥有当前的应用状态。需要延长这一点以保持当前的电量
class _MyHomePageState extends State<MyHomePage> {
  // 首先,构建通道。使用MethodChannel,调用一个方法来返回电池电量。
  // 通道的客户端和宿主通过通道构造函数中传递的通道名称进行连接。单个应用中使用的所有通道名称必须是唯一的; 建议在通道名称前加一个唯一的“域名前缀”,例如samples.flutter.io/battery。
  static const platform = const MethodChannel('samples.flutter.io/battery');
  // Get battery level.
  String _batteryLevel = 'Unknown battery level.';
  Future<Null> _getBatteryLevel() async {
    String batteryLevel;
    try {
      // 调用通道上的方法,指定通过字符串标识符调用方法getBatteryLevel。 
      // 该调用可能失败(平台不支持平台API,例如在模拟器中运行时),所以将invokeMethod调用包装在try-catch语句中。
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }
    // 使用返回的结果,在setState中来更新用户界面状态batteryLevel。
    setState(() {
      _batteryLevel = batteryLevel;
    });
  }
  @override
  // 在build创建包含一个小字体显示电池状态和一个用于刷新值的按钮的用户界面。
  Widget build(BuildContext context) {
    return new Material(
      child: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            new RaisedButton(
              child: new Text('Get Battery Level'),
              onPressed: _getBatteryLevel,
            ),
            new Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}

Android端API实现(android/src/.../MainActivity.java文件)

import io.flutter.app.FlutterActivity;
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 android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;

public class MainActivity extends FlutterActivity {
    private static final String CHANNEL = "samples.flutter.io/battery";
    @Override
    // 在onCreate里创建MethodChannel并设置一个MethodCallHandler。确保使用和Flutter客户端中使用的通道名称相同的名称。
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
          new MethodCallHandler() {
             @Override
             public void onMethodCall(MethodCall call, Result result) {
                // 在call参数中进行检测是否为getBatteryLevel
                if (call.method.equals("getBatteryLevel")) {
                    int batteryLevel = getBatteryLevel();

                    if (batteryLevel != -1) {
                        result.success(batteryLevel);
                    } else {
                        result.error("UNAVAILABLE", "Battery level not available.", null);
                    }
                } else {
                    result.notImplemented();
                }
             }
          });
    }
    private int getBatteryLevel() {
      int batteryLevel = -1;
      if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
        BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
        batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
      } else {
        Intent intent = new ContextWrapper(getApplicationContext()).
            registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
            intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
      }
      return batteryLevel;
    }  
}

iOS端API实现(AppDelegate.m文件)

#import <Flutter/Flutter.h>
@implementation AppDelegate
// 在application didFinishLaunchingWithOptions:方法内部创建一个FlutterMethodChannel,并添加一个处理方法。 确保与在Flutter客户端使用的通道名称相同。
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
  FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
                                          methodChannelWithName:@"samples.flutter.io/battery"
                                          binaryMessenger:controller];
  [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
   // call参数中进行检测是否为getBatteryLevel
    if ([@"getBatteryLevel" isEqualToString:call.method]) {
      int batteryLevel = [self getBatteryLevel];
      if (batteryLevel == -1) {
        result([FlutterError errorWithCode:@"UNAVAILABLE"
                                   message:@"电池信息不可用"
                                   details:nil]);
      } else {
        result(@(batteryLevel));
      }
    } else {
      result(FlutterMethodNotImplemented);
    }
  }];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (int)getBatteryLevel {
  UIDevice* device = UIDevice.currentDevice;
  device.batteryMonitoringEnabled = YES;
  if (device.batteryState == UIDeviceBatteryStateUnknown) {
    return -1;
  } else {
    return (int)(device.batteryLevel * 100);
  }
}

4. Texture和PlatformView

Flutter本身只是一个UI系统,对于一些系统能力的调用可以通过消息传送机制与原生交互。
但是这种消息传送机制并不能覆盖所有的应用场景,比如想调用摄像头来拍照或录视频,但在拍照和录视频的过程中需要将预览画面显示到Flutter UI中,如果要用Flutter定义的消息通道机制来实现这个功能,就需要将摄像头采集的每一帧图片都要从原生传递到Flutter中,这样做代价将会非常大,因为将图像或视频数据通过消息通道实时传输必然会引起内存和CPU的巨大消耗!
为此,Flutter提供了一种基于Texture的图片数据共享机制。

Texture(使用摄像头)

Texture可以理解为GPU内保存将要绘制的图像数据的一个对象,Flutter engine会将Texture的数据在内存中直接进行映射(而无需在原生和Flutter之间再进行数据传递),Flutter会给每一个Texture分配一个id,同时Flutter中提供了一个Texture组件,Texture构造函数定义如下:
Texture({
  Key key,
  @required this.textureId,
})
Texture 组件正是通过textureId与Texture数据关联起来;在Texture组件绘制时,Flutter会自动从内存中找到相应id的Texture数据,然后进行绘制。可以总结一下整个流程:图像数据先在原生部分缓存,然后在Flutter部分再通过textureId和缓存关联起来,最后绘制由Flutter完成。

如果我们作为一个插件开发者,在原生代码中分配了textureId,那么通过MethodChannel来传递textureId在Flutter侧获取。
当原生摄像头捕获的图像发生变化时,Texture 组件会自动重绘,不需要写任何Dart 代码去控制。
如果要手动实现一个相机插件,需要分别实现原生部分和Flutter部分。

Flutter官方提供的相机(camera)插件和视频播放(video_player)插件都是使用Texture来实现的
camera包自带的一个示例,它包含如下功能:
    可以拍照,也可以拍视频,拍摄完成后可以保存;保存的视频可以播放预览。
    可以切换摄像头(前置摄像头、后置摄像头、其它)
    可以显示已经拍摄内容的预览图。

1. 首先,依赖camera插件的最新版,并下载依赖。
dependencies:
  ...  //省略无关代码
  camera: ^0.5.2+2
2. 在main方法中获取可用摄像头列表。
void main() async {
  // 获取可用摄像头列表,cameras为全局变量
  cameras = await availableCameras();
  runApp(MyApp());
}
3. 构建UI

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import '../common.dart';
import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart'; //用于播放录制的视频

/// 获取不同摄像头的图标(前置、后置、其它)
IconData getCameraLensIcon(CameraLensDirection direction) {
  switch (direction) {
    case CameraLensDirection.back:
      return Icons.camera_rear;
    case CameraLensDirection.front:
      return Icons.camera_front;
    case CameraLensDirection.external:
      return Icons.camera;
  }
  throw ArgumentError('Unknown lens direction');
}
void logError(String code, String message) =>
    print('Error: $code\nError Message: $message');
// 示例页面路由
class CameraExampleHome extends StatefulWidget {
  @override
  _CameraExampleHomeState createState() {
    return _CameraExampleHomeState();
  }
}
class _CameraExampleHomeState extends State<CameraExampleHome>
    with WidgetsBindingObserver {
  CameraController controller;
  String imagePath; // 图片保存路径
  String videoPath; //视频保存路径
  VideoPlayerController videoController;
  VoidCallback videoPlayerListener;
  bool enableAudio = true;
  @override
  void initState() {
    super.initState();
    // 监听APP状态改变,是否在前台
    WidgetsBinding.instance.addObserver(this);
  }
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // 如果APP不在在前台
    if (state == AppLifecycleState.inactive) {
      controller?.dispose();
    } else if (state == AppLifecycleState.resumed) {
      // 在前台
      if (controller != null) {
        onNewCameraSelected(controller.description);
      }
    }
  }
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: const Text('相机示例'),
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: Container(
              child: Padding(
                padding: const EdgeInsets.all(1.0),
                child: Center(
                  child: _cameraPreviewWidget(),
                ),
              ),
              decoration: BoxDecoration(
                color: Colors.black,
                border: Border.all(
                  color: controller != null && controller.value.isRecordingVideo
                      ? Colors.redAccent
                      : Colors.grey,
                  width: 3.0,
                ),
              ),
            ),
          ),
          _captureControlRowWidget(),
          _toggleAudioWidget(),
          Padding(
            padding: const EdgeInsets.all(5.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: <Widget>[
                _cameraTogglesRowWidget(),
                _thumbnailWidget(),
              ],
            ),
          ),
        ],
      ),
    );
  }
  /// 展示预览窗口
  Widget _cameraPreviewWidget() {
    if (controller == null || !controller.value.isInitialized) {
      return const Text(
        '选择一个摄像头',
        style: TextStyle(
          color: Colors.white,
          fontSize: 24.0,
          fontWeight: FontWeight.w900,
        ),
      );
    } else {
      return AspectRatio(
        aspectRatio: controller.value.aspectRatio,
        child: CameraPreview(controller),
      );
    }
  }
  /// 开启或关闭录音
  Widget _toggleAudioWidget() {
    return Padding(
      padding: const EdgeInsets.only(left: 25),
      child: Row(
        children: <Widget>[
          const Text('开启录音:'),
          Switch(
            value: enableAudio,
            onChanged: (bool value) {
              enableAudio = value;
              if (controller != null) {
                onNewCameraSelected(controller.description);
              }
            },
          ),
        ],
      ),
    );
  }
  /// 显示已拍摄的图片/视频缩略图。
  Widget _thumbnailWidget() {
    return Expanded(
      child: Align(
        alignment: Alignment.centerRight,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            videoController == null && imagePath == null
                ? Container()
                : SizedBox(
              child: (videoController == null)
                  ? Image.file(File(imagePath))
                  : Container(
                child: Center(
                  child: AspectRatio(
                      aspectRatio:
                      videoController.value.size != null
                          ? videoController.value.aspectRatio
                          : 1.0,
                      child: VideoPlayer(videoController)),
                ),
                decoration: BoxDecoration(
                    border: Border.all(color: Colors.pink)),
              ),
              width: 64.0,
              height: 64.0,
            ),
          ],
        ),
      ),
    );
  }
  /// 相机工具栏
  Widget _captureControlRowWidget() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      mainAxisSize: MainAxisSize.max,
      children: <Widget>[
        IconButton(
          icon: const Icon(Icons.camera_alt),
          color: Colors.blue,
          onPressed: controller != null &&
              controller.value.isInitialized &&
              !controller.value.isRecordingVideo
              ? onTakePictureButtonPressed
              : null,
        ),
        IconButton(
          icon: const Icon(Icons.videocam),
          color: Colors.blue,
          onPressed: controller != null &&
              controller.value.isInitialized &&
              !controller.value.isRecordingVideo
              ? onVideoRecordButtonPressed
              : null,
        ),
        IconButton(
          icon: const Icon(Icons.stop),
          color: Colors.red,
          onPressed: controller != null &&
              controller.value.isInitialized &&
              controller.value.isRecordingVideo
              ? onStopButtonPressed
              : null,
        )
      ],
    );
  }
  /// 展示所有摄像头
  Widget _cameraTogglesRowWidget() {
    final List<Widget> toggles = <Widget>[];
    if (cameras.isEmpty) {
      return const Text('没有检测到摄像头');
    } else {
      for (CameraDescription cameraDescription in cameras) {
        toggles.add(
          SizedBox(
            width: 90.0,
            child: RadioListTile<CameraDescription>(
              title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
              groupValue: controller?.description,
              value: cameraDescription,
              onChanged: controller != null && controller.value.isRecordingVideo
                  ? null
                  : onNewCameraSelected,
            ),
          ),
        );
      }
    }
    return Row(children: toggles);
  }
  String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();
  void showInSnackBar(String message) {
    _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message)));
  }
  // 摄像头选中回调
  void onNewCameraSelected(CameraDescription cameraDescription) async {
    if (controller != null) {
      await controller.dispose();
    }
    controller = CameraController(
      cameraDescription,
      ResolutionPreset.high,
      enableAudio: enableAudio,
    );
    controller.addListener(() {
      if (mounted) setState(() {});
      if (controller.value.hasError) {
        showInSnackBar('Camera error ${controller.value.errorDescription}');
      }
    });
    try {
      await controller.initialize();
    } on CameraException catch (e) {
      _showCameraException(e);
    }
    if (mounted) {
      setState(() {});
    }
  }
  // 拍照按钮点击回调
  void onTakePictureButtonPressed() {
    takePicture().then((String filePath) {
      if (mounted) {
        setState(() {
          imagePath = filePath;
          videoController?.dispose();
          videoController = null;
        });
        if (filePath != null) showInSnackBar('图片保存在 $filePath');
      }
    });
  }
  // 开始录制视频
  void onVideoRecordButtonPressed() {
    startVideoRecording().then((String filePath) {
      if (mounted) setState(() {});
      if (filePath != null) showInSnackBar('正在保存视频于 $filePath');
    });
  }
  // 终止视频录制
  void onStopButtonPressed() {
    stopVideoRecording().then((_) {
      if (mounted) setState(() {});
      showInSnackBar('视频保存在: $videoPath');
    });
  }
  Future<String> startVideoRecording() async {
    if (!controller.value.isInitialized) {
      showInSnackBar('请先选择一个摄像头');
      return null;
    }
    // 确定视频保存的路径
    final Directory extDir = await getApplicationDocumentsDirectory();
    final String dirPath = '${extDir.path}/Movies/flutter_test';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${timestamp()}.mp4';
    if (controller.value.isRecordingVideo) {
      // 如果正在录制,则直接返回
      return null;
    }
    try {
      videoPath = filePath;
      await controller.startVideoRecording(filePath);
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
    return filePath;
  }
  Future<void> stopVideoRecording() async {
    if (!controller.value.isRecordingVideo) {
      return null;
    }
    try {
      await controller.stopVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
    await _startVideoPlayer();
  }
  Future<void> _startVideoPlayer() async {
    final VideoPlayerController vcontroller =
    VideoPlayerController.file(File(videoPath));
    videoPlayerListener = () {
      if (videoController != null && videoController.value.size != null) {
        // Refreshing the state to update video player with the correct ratio.
        if (mounted) setState(() {});
        videoController.removeListener(videoPlayerListener);
      }
    };
    vcontroller.addListener(videoPlayerListener);
    await vcontroller.setLooping(true);
    await vcontroller.initialize();
    await videoController?.dispose();
    if (mounted) {
      setState(() {
        imagePath = null;
        videoController = vcontroller;
      });
    }
    await vcontroller.play();
  }
  Future<String> takePicture() async {
    if (!controller.value.isInitialized) {
      showInSnackBar('错误: 请先选择一个相机');
      return null;
    }
    final Directory extDir = await getApplicationDocumentsDirectory();
    final String dirPath = '${extDir.path}/Pictures/flutter_test';
    await Directory(dirPath).create(recursive: true);
    final String filePath = '$dirPath/${timestamp()}.jpg';
    if (controller.value.isTakingPicture) {
      // A capture is already pending, do nothing.
      return null;
    }
    try {
      await controller.takePicture(filePath);
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
    return filePath;
  }
  void _showCameraException(CameraException e) {
    logError(e.code, e.description);
    showInSnackBar('Error: ${e.code}\n${e.description}');
  }
}

PlatformView

如果在开发过程中需要使用一个原生组件,但这个原生组件在Flutter中很难实现时怎么办(如webview)?这时一个简单的方法就是将需要使用原生组件的页面全部用原生实现,在flutter中需要打开该页面时通过消息通道打开这个原生的页面。

缺点:
  1. 原生组件很难和Flutter组件进行组合。
  2. 开销是非常大的。
如果一个原生组件用Flutter实现的难度不大时,应该首选Flutter实现。

在 Flutter 1.0版本中,Flutter SDK中新增了AndroidView和UIKitView 两个组件,这两个组件的主要功能就是将原生的Android组件和iOS组件嵌入到Flutter的组件树中,这个功能是非常重要的,尤其是对一些实现非常复杂的组件,比如webview,这些组件原生已经有了,如果Flutter中要用,重新实现的话成本将非常高,所以如果有一种机制能让Flutter共享原生组件,这将会非常有用,也正因如此,Flutter才提供了这两个组件。

由于AndroidView和UIKitView 是和具体平台相关的,所以称它们为PlatformView。需要说明的是将来Flutter支持的平台可能会增多,则相应的PlatformView也将会变多。
1. 原生代码中注册要被Flutter嵌入的组件工厂,如webview_flutter插件中Android端注册webview插件代码:
public static void registerWith(Registrar registrar) {
   registrar.platformViewRegistry().registerViewFactory("webview", 
   WebViewFactory(registrar.messenger()));
}
WebViewFactory的具体实现请参考webview_flutter插件的实现源码。

2. 在Flutter中使用;打开Flutter中文社区首页。
class PlatformViewRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return WebView(
      initialUrl: "https://flutterchina.club",
      javascriptMode: JavascriptMode.unrestricted,
    );
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345