注:原课程视频是基于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
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/studio
,https://developer.android.google.cn/studio
,https://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,通过
CustomPaint
和CustomPainter
两个类帮助绘制画布;
// 画图
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 + <