本文面向希望基于现有的 React Native 的知识结构使用 Flutter 开发移动端应用的开发者。如果你已经对 RN 的框架有所了解,那么你可以通过这个文档入门 Flutter 开发。
本文可以当做查询手册使用,里面涉及到的问题基本上可以满足需求。
本系列上部分:
本文结构如下:
1.针对 JavaScript 开发者的 Dart 介绍(上)
2.基本知识(上)
3.项目结构和资源(上)
4.Flutter widgets(上)
5. 视图(上)
6.布局(上)
7.风格化(上)
8.状态管理(下)
9.道具(下)
10.本地存储(下)
11.路径(下)
12.手势检测和触摸事件处理(下)
13.发起HTTP网络请求(下)
14.输入表单(下)
15.平台相关代码(下)
16.调试(下)
17.动画(下)
八、状态管理
当 widget 被创建或者在 widget 的生命周期中有信息发生改变时所产生的信息叫做状态。要在 Flutter 中管理应用程序的状态,使用 StatefulWidget 和 State 对象。
8.1 The StatelessWidget
StatelessWidget 在 Flutter 中是一个不需要状态改变的 widget,它没有内部的状态。
当你展现给用户的界面并不依赖其它任何配置信息并且使用 BuildContext 来解析 widget,则需要使用无状态 widget。
AboutDialog、 CircleAvatar 和 Text 是 StatelessWidget 的子类,并且是很典型的无状态 widget。
// Flutter
import 'package:flutter/material.dart';
void main() => runApp(MyStatelessWidget(text: 'StatelessWidget Example to show immutable data'));
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,
),
);
}
}
在上面的例子中,你用到了 MyStatelessWidget 类的构造函数来传递 text。并且它被标记为 final。该类继承了 StatelessWidget,它包含不可数的数据。
无状态 widget 的 build 方法通常只有在三种情况下会被调用:
当 widget 被插入到 widget 树中
当 widget 的父 widget 改变了配置
当所依赖的 InheritedWidget 发生了改变
8.2 StatefulWidget widget
StatefulWidget 是携带状态变化的 widget。通过调用 setState 方法可以管理 StatefulWidget 的状态。当调用 setState 的时候,程序会通知 Flutter 框架有状态发生了改变,然后会重新运行 build 方法来更新应用的状态。
状态是在 widget 被创建期间可以被同步读取的信息,并且在 widget 的生命周期中会发生改变。实现该 widget 的时候要注意保证党状态发生改变的时候程序能够获得相应的提醒。当 widget 能够动态改变的时候,请使用 StatefulWidget。比如,某个 widget 会随着用户填写表单或者移动滑块的时候发生改变。亦或者随着数据源更新的时候发生改变。
Checkbox、 Radio、 Slider、 InkWell、 Form、 和 TextField 都是有状态的 widget,是 StatefulWidget 的子类。
下面的示例代码声明了一个 StatefulWidget,需要实现 createState() 方法。该方法创建一个对象来管理 widget 的状态,也就是 _MyStatefulWidgetState。
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key, this.title}) : super(key: key);
final String title;
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
下面的状态类,_MyStatefulWidgetState,实现了 build() 方法。当状态发生改变的时候,比如说用户点击了开关按钮,这时 setState 就会被调用,并且将新的开关状态传进来。这就会使整体框架重构这个 widget。
class _MyStatefulWidgetState extends State {
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 build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
(showtext
?(Text('This execution will be done before you can blink.'))
:(Container())
),
Padding(
padding: EdgeInsets.only(top: 70.0),
child: RaisedButton(
onPressed: toggleBlinkState,
child: (toggleState
?( Text('Blink'))
:(Text('Stop Blinking'))
)
)
)
],
),
),
);
}
}}
8.3 StatefulWidget 和 StatelessWidget 的最佳实践是什么?
下面有一些设计原则供大家参考。
1. 确定一个 widget 应该是 StatefulWidget 还是 StatelessWidget
在 Flutter 中, widget 要么是有状态的,要么是无状态的。这取决于 widget 是否依赖状态的改变。
如果一个 widget 发生了改变,而它所处的用户界面或者数据中断了 UI,那么该 widget 就是有状态的。
如果一个 widget 是 final 类型或者 immutable 类型的,那么该 widget 是无状态的。
2. 确定哪个对象来控制 widget 的状态(针对 StatefulWidget)。
在 Flutter 中,有三种途径来管理状态:
widget 管理它的自身状态
由其父 widget 管理 widget 状态
通过混搭的方式
当决定了使用哪个途径后,要考虑下述的几个原则:
如果状态信息是用户数据,比如 checkbox 是被勾选还是未被勾选,或者滑块的位置,那么父 widget 会很好的处理当前 widget 的状态。
如果状态是和外观效果相关的,比如动画,那么 widget 自己会处理状态的变化。
如果无法确定,那么父 widget 会处理子 widget 的状态。
3. 继承 StatefulWidget 和 状态
MyStatefulWidget 类管理它自身的状态 - 它继承自 StatefulWidget,重写了 createState() 方法。该方法创建了 State 对象,同时框架会调用 createState() 方法来构建 widget。在这个例子中,createState() 方法创建了一个 _MyStatefulWidgetState实例。下面的最佳实践中也实现了类似的方法。
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key, this.title}) : super(key: key);
final String title;
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State {
@override
widget build(BuildContext context) {
...
}
}
4. 将 StatefulWidget 添加到 widget 树中
将你自定义的 StatefulWidget 通过应用程序的 build 方法添加到 widget 树中。
class MyStatelessWidget extends StatelessWidget {
// This widget is the root of your application.
@override
widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyStatefulWidget(title: 'State Change Demo'),
);
}
}
Android
iOS
九、Props
在 React Native 中,大多数组件都可以在创建的时候通过不同的参数或者属性来自定义,叫做 props。这些参数可以在子组件中通过 this.props 进行调用。
// React Native
class CustomCard extends React.Component {
render() {
return (
Card {this.props.index}
);
}
}
class App extends React.Component {
onPress = index => {
console.log('Card ', index);
};
render() {
return (
(
)}
/>
);
}
}
在 Flutter 中,你可以将构造函数中的参数值赋值给标记为 final 的本地变量或者函数。
// Flutter
class CustomCard extends StatelessWidget {
CustomCard({@required this.index, @required this.onPress});
final index;
final Function onPress;
@override
widget build(BuildContext context) {
return Card(
child: Column(
children: [
Text('Card $index'),
FlatButton(
child: const Text('Press'),
onPressed: this.onPress,
),
],
));
}
}
...
//Usage
CustomCard(
index: index,
onPress: () {
print('Card $index');
},
)
如果你不需要在本地存储太多数据同时也不需要存储结构化数据,那么你可以使用 shared_preferences,通过它来读写一些原始数据类型键值对,数据类型包括 boolean, float, ints, longs 和 string。
10.1 如何存储在应用程序中全局有效的键值对?
在 React Native,可以使用 AsyncStorage 中的 setItem 和 getItem 函数来存储和读取应用程序中的全局数据。
在 Flutter 中,使用 shared_preferences 插件来存储和访问应用程序内全局有效的键值对数据。shared_preferences 插件封装了 iOS 中的 NSUserDefaults 和 Android 中的 SharedPreferences 来实现简单数据的持续存储。如果要使用该插件,可以在 pubspec.yaml 中添加依赖 shared_preferences,然后再 Dart 文件中引用包即可。
// Dart
import 'package:shared_preferences/shared_preferences.dart';
要实现持久数据存储,使用 SharedPreferences 类提供的 setter 方法即可。Setter 方法适用于多种原始类型数据,比如 setInt, setBool, 和 setString。要读取数据,使用 SharedPreferences 类中相应的 getter 方法。对于每一个 setter 方法都有对应的 getter 方法。比如,getInt, getBool, 和 getString。
SharedPreferences prefs = await SharedPreferences.getInstance();
_counter = prefs.getInt('counter');
prefs.setInt('counter', ++_counter);
setState(() {
_counter = _counter;
});
十一、路径
大多数应用都会包含多个页面来显示不同类型的数据。比如,你有一个页面展示商品列表,用户可以通过点击其中的任意一个商品在另外一个页面查看该商品的详细信息。
在 Android 中,新的页面是 Activity。在 iOS 中,新的页面是 ViewController。在 Flutter 中,页面就是 widget !如果在 Flutter 中要切换页面,使用 Navigator widget 即可。
11.1 如何在页面之间进行切换?
在 React Native,有三种主要的导航 widget :StackNavigator, TabNavigator 和 DrawerNavigator。每个都提供了配置和定义页面的方法。
// React Native
const MyApp = TabNavigator(
{ Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
{ tabBarOptions: { activeTintColor: '#e91e63' } }
);
const SimpleApp = StackNavigator({
Home: { screen: MyApp },
stackScreen: { screen: StackScreen }
});
export default (MyApp1 = DrawerNavigator({
Home: {
screen: SimpleApp
},
Screen2: {
screen: drawerScreen
}
}));
在 Flutter 中,有两种主要的 widget 实现页面之间的切换:
Route 是应用程序页面的一个抽象类。
Navigator 是管理页面路径的 widget。
Navigator 以堆栈的方式管理子 widget。它的堆栈里存储的是 Route 对象,并且提供方法管理整个堆栈,比如 Navigator.push和 Navigator.pop。路径列表需要在 MaterialApp 中指定。或者在页面切换的时候进行构建,比如 hero 动画。下面的例子在 MaterialApp widget 中指定了页面切换路径。
// Flutter
class NavigationApp extends StatelessWidget {
// This widget is the root of your application.
@override
widget build(BuildContext context) {
return MaterialApp(
...
routes: {
'/a': (BuildContext context) => usualNavscreen(),
'/b': (BuildContext context) => drawerNavscreen(),
}
...
);
}
}
要切换到一个已命名的路径,Navigator 中的 of 方法被用于指定 BuildContext ( 该对象可以定位到 widget 树中的一个具体的 widget )。路径的名称传递到 pushNamed 函数来切换至指定的路径。
Navigator.of(context).pushNamed('/a');
你可以使用 Navigator 中的 push 方法添加 route 到 navigator 的历史队列中,其中包含 context 并且可以切换到指定页面。在下面的例子中,MaterialPageRoute 是一个模式化路径,可以将整个页面通过平台自适应切换方式进行切换。它需要一个 WidgetBuilder 参数。
Navigator.push(context, MaterialPageRoute(builder: (BuildContext context)
=> UsualNavscreen()));
11.2 如何使用 tab 导航和 drawer 导航?
在 Material Design 应用程序中,Flutter 的导航形式主要有两种:tab 和 drawer。如果没有足够的 widget 可以容纳 tab,drawer 就是个不错的选择。
11.2.1 Tab 导航
在 React Native 中,createBottomTabNavigator 和 TabNavigation 用来显示 tab 和 tab 导航。
// React Native
import { createBottomTabNavigator } from 'react-navigation';
const MyApp = TabNavigator(
{ Home: { screen: HomeScreen }, Notifications: { screen: tabNavScreen } },
{ tabBarOptions: { activeTintColor: '#e91e63' } }
);
Flutter 针对 drawer 和 tab 导航提供几种专用的 widget:
TabController—将 tab 与 TabBar 和 TabBarView 结合起来使用。
TabBar—水平显示一行 tab。
Tab—创建一个 material design 风格的 TabBar 中的 tab。
TabBarView—显示目前所选 tab 所对应的 widget。
// Flutter
TabController controller=TabController(length: 2, vsync: this);
TabBar(
tabs: [
Tab(icon: Icon(Icons.person),),
Tab(icon: Icon(Icons.email),),
],
controller: controller,
),
要将 tab 选项与 TabBar 和 TabBarView 结合起来使用就需要 TabController。TabController 的构造函数中的 length 参数定义了 tab 的总数。当状态变化时,需要使用 TickerProvider 来触发通知。TickerProvider 是 vsync。当你需要创建新的 TabController 时,将 vsync: this 作为构造函数的参数即可。
TickerProvider 接口可以用于生成 Ticker 对象。当有对象被触发通知后会用到 Tickers,不过它通常都是被 AnimationController 间接调用。AnimationControllers 需要 TickerProvider 来获得对应的 Ticker。如果你通过 State 创建了一个 AnimationController,那么你就可以使用 TickerProviderStateMixin 或者 SingleTickerProviderStateMixin 来获得对应的 TickerProvider。
Scaffold 封装了一个新的 TabBar widget,其中包含两个 tab。TabBarView 作为 body 参数传递到 Scaffold 中。所有和 TabBar中的 tab 相关的页面均是 TabBarView 的子 widget,并且都对应同一个 TabController。
// Flutter
class _NavigationHomePageState extends State with SingleTickerProviderStateMixin {
TabController controller=TabController(length: 2, vsync: this);
@override
widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Material (
child: TabBar(
tabs: [
Tab(icon: Icon(Icons.person),)
Tab(icon: Icon(Icons.email),),
],
controller: controller,
),
color: Colors.blue,
),
body: TabBarView(
children: [
home.homeScreen(),
tabScreen.tabScreen()
],
controller: controller,
)
);
}
}
11.2.2 Drawer 导航
在 React Native 中,导入所需的 react-navigation 包,然后使用 createDrawerNavigator 和 DrawerNavigation 实现。
// React Native
export default (MyApp1 = DrawerNavigator({
Home: {
screen: SimpleApp
},
Screen2: {
screen: drawerScreen
}
}));
在 Flutter 中,可结合 Drawer 和 Scaffold 一起使用来实现 Material Design 风格的 drawer 布局。如果要在应用程序中添加 Drawer, 可以将它封装在 Scaffold widget 中。Scaffold widget 提供了一种一致的界面风格,它遵循 Material Design 的设计原则。同时它还支持一些特殊的 Material Design 组件,比如 Drawers,AppBars, 和 SnackBars。
Drawer 就是一个 Material Design 窗格,它可以从 Scaffold 边缘水平滑动显示应用程序的导航选项。你可以在里面添加 Button, Text。或者添加一个列表的元素作为 Drawer 的子 widget。在下面的例子中,ListTile 提供了点击导航:
// Flutter
Drawer(
child:ListTile(
leading: Icon(Icons.change_history),
title: Text('Screen2'),
onTap: () {
Navigator.of(context).pushNamed('/b');
},
),
elevation: 20.0,
),
Scaffold 还包含一个 AppBar。它会自动显示一个图标按钮来表明 Scaffold 中有一个Drawer。Scaffold 会自动处理边缘的滑动手势来显示 Drawer。
// Flutter
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: ListTile(
leading: Icon(Icons.change_history),
title: Text('Screen2'),
onTap: () {
Navigator.of(context).pushNamed('/b');
},
),
elevation: 20.0,
),
appBar: AppBar(
title: Text('Home'),
),
body: Container(),
);
}
Flutter 支持点击、拖拽和缩放手势来监听和相应手势操作。Flutter 中的手势处理有两个独立的层。第一层是指针事件,指针事件定义了指针在屏幕上的位置和动作,比如触摸、鼠标和触摸笔。第二层指手势,主要是语义层面的动作,里面包含一种或者多种指针动作。
12.1 如何为 widget 添加点击或者按压的监听器?
在 React Native 中,使用 PanResponder 或者 Touchable 组件来添加监听器。
// React Native
{
console.log('Press');
}}
onLongPress={() => {
console.log('Long Press');
}}
>
Tap or Long Press
对于更加复杂手势以及将多个触摸添加到单独的一个手势中,可以使用 PanResponder。
// React Native
class App extends Component {
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: (event, gestureState) =>
!!getDirection(gestureState),
onPanResponderMove: (event, gestureState) => true,
onPanResponderRelease: (event, gestureState) => {
const drag = getDirection(gestureState);
},
onPanResponderTerminationRequest: (event, gestureState) => true
});
}
render() {
return (
Swipe Horizontally or Vertically
);
}
}
在 Flutter 中,要为 widget 添加点击或者按压监听器,使用带有 onPress: field 的按钮或者可触摸 widget 即可。或者,用任何 widget 封装 GestureDetector,在其中添加手势检测。
// Flutter
GestureDetector(
child: Scaffold(
appBar: AppBar(
title: Text('Gestures'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Tap, Long Press, Swipe Horizontally or Vertically '),
],
)
),
),
onTap: () {
print('Tapped');
},
onLongPress: () {
print('Long Pressed');
},
onVerticalDragEnd: (DragEndDetails value) {
print('Swiped Vertically');
},
onHorizontalDragEnd: (DragEndDetails value) {
print('Swiped Horizontally');
},
);
如果想要了解更多详细内容,包括 Flutter 的 GestureDetector 回调函数的列表,请查看页面 GestureDetector class。
Android
iOS
十三、发起 HTTP 网络请求
对于大多数应用程序来说都需要从互联网上获取数据。在 Flutter 中,http 包提供了从互联网获取数据的最简单的途径。
13.1 如何通过 API 调用来获得数据呢?
React Native 提供 Fetch API 实现网络编程,你可以发起请求,然后接收响应来获得数据。
// React Native
_getIPAddress = () => {
fetch('https://httpbin.org/ip')
.then(response => response.json())
.then(responseJson => {
this.setState({ _ipAddress: responseJson.origin });
})
.catch(error => {
console.error(error);
});
};
Flutter 使用 http 包。如果要安装 http 包,将它添加到 pubspec.yaml 的 dependencies 部分。
dependencies:
flutter:
sdk: flutter
http:
Flutter 使用 dart:io 提供核心的 HTTP 客户端支持,要创建一个 HTTP 客户端,引用 dart:io。
import 'dart:io';
客户端支持如下所列的 HTTP 操作:GET, POST, PUT 和 DELETE。
// Flutter
final url = Uri.https('httpbin.org', 'ip');
final httpClient = HttpClient();
_getIPAddress() async {
var request = await httpClient.getUrl(url);
var response = await request.close();
var responseBody = await response.transform(utf8.decoder).join();
String ip = jsonDecode(responseBody)['origin'];
setState(() {
_ipAddress = ip;
});
}
TextField 用于在应用程序中输入文本,这样就可以实现创建表单、短消息应用、搜索框等等功能。Flutter 提供两个核心文本输入 widget :TextField 和 TextFormField.
14.1 如何使用文本输入 widget ?
在 React Native 里,可以使用 TextInput 组件来输入文本,它会显示一个输入框,然后通过回调函数来传递输入值。
// React Native
this.setState({ password })}
/>
在 Flutter 中,使用 TextEditingController 类来管理 TextField widget。当用户修改文本的时候,controller 会通知监听器。
监听器读取文本和选项属性来获知用户所输入的内容。你可以通过 TextField 中的 text 属性获得用户输入的文本数据。
// Flutter
final TextEditingController _controller = TextEditingController();
...
TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Type something', labelText: 'Text Field '
),
),
RaisedButton(
child: Text('Submit'),
onPressed: () {
showDialog(
context: context,
child: AlertDialog(
title: Text('Alert'),
content: Text('You typed ${_controller.text}'),
),
);
},
),
)
在这个例子中,当用户点击提交按钮的时候,会弹出窗口显示当前输入的文本内容。可以使用 alertDialog widget 显示提示信息,TextField 的文本通过 text 属性来获得,该属性属于 TextEditingController。
14.2 如何使用 Form widget 呢?
在 Flutter 中,当需要使用带有提交按钮和 TextFormField 组件的复合 widget 时,就会用到 Form。TextFormField 内含一个 onSaved 参数,它可以设置一个回调函数,当表单存储的时候会回调该函数。FormState 用于存储、重置或者验证 Form 内含的每个 FormField。你可以通过将当前表单的 context 属性赋值给 Form.of 来获得 FormState。或者在表单的构造函数里使用 GlobalKey,然后调用 GlobalKey.currentState 来获得 FormState。
final formKey = GlobalKey();
...
Form(
key:formKey,
child: Column(
children: [
TextFormField(
validator: (value) => !value.contains('@') ? 'Not a valid email.' : null,
onSaved: (val) => _email = val,
decoration: const InputDecoration(
hintText: 'Enter your email',
labelText: 'Email',
),
),
RaisedButton(
onPressed: _submit,
child: Text('Login'),
),
],
),
下面的示例代码展示了 Form.save() 和 formKey(这个实际上是 GlobalKey)如何被用于表单提交的。
void _submit() {
final form = formKey.currentState;
if (form.validate()) {
form.save();
showDialog(
context: context,
child: AlertDialog(
title: Text('Alert'),
content: Text('Email: $_email, password: $_password'),
)
);
}
}
Android
iOS
十五、平台相关代码
当构建跨平台应用程序的时候,你会尽量多地复用代码。然而,根据不同的应用场景,代码会根据平台的不同有所变化。这就需要提前声明具体的平台来进行独立的实现。
在 React Native 中,下面的实现代码会被用到:
// React Native
if (Platform.OS === 'ios') {
return 'iOS';
} else if (Platform.OS === 'android') {
return 'android';
} else {
return 'not recognised';
}
而在 Flutter 中,则是下面这样的实现:
// Flutter
if (Theme.of(context).platform == TargetPlatform.iOS) {
return 'iOS';
} else if (Theme.of(context).platform == TargetPlatform.android) {
return 'android';
} else if (Theme.of(context).platform == TargetPlatform.fuchsia) {
return 'fuchsia';
} else {
return 'not recognised ';
}
十六、调试
在运行应用程序之前,可以使用 flutter analyze 检验一下代码。Flutter analyzer (它封装了 dartanalyzer 工具)可以验证你的代码并且帮助你定位潜在的问题。如果你使用的是启用了 Flutter 的 IDE 的话,这个过程是全自动的。
16.1 如何打开程序里的开发者菜单?
在 React Native 中,开发者菜单可以通过摇动设备打开:对于 iOS 模拟器的快捷键是 ⌘D 而 Android 模拟器的快捷键是 ⌘M。
在 Flutter 中,如果你使用 IDE,那么可以直接使用 IDE 工具。如果你是通过命令行运行 flutter run 来启动应用程序的,你可以在命令行窗口通过输入 h 来打开菜单,或者参考下面的快捷键说明:
功能 | 命令行快捷键 | 调试功能和属性 |
---|---|---|
应用程序的 widget 层级 | w |
debugDumpApp() |
渲染程序的 widget 树 | t |
debugDumpRenderTree() |
层 | L |
debugDumpLayerTree() |
无障碍 | S (遍历顺序) 或者U (反转点击测试顺序) |
debugDumpSemantics() |
打开或者关闭 widget 窗口 | i |
WidgetsApp. showWidgetInspectorOverride |
显示或者隐藏框架线条 | p |
debugPaintSizeEnabled |
模拟不同的操作系统 | o |
defaultTargetPlatform |
叠加显示性能参数 | P |
WidgetsApp. showPerformanceOverlay |
将截屏保存为 flutter.png | s |
|
退出 | q |
16.2 如何进行热重载?
Flutter 的热重载特性可以帮助你快速便捷地实验、构建 UI 和各种特性以及修复 bug。每次修改代码以后,你只需直接热重载你的应用程序即可,而无需重新进行编译。应用程序会根据你的修改进行相应的更新,而程序原有的状态则会被保留。
在 React Native 中,iOS 模拟器对应的快捷键是 ⌘R ,对应 Android 模拟器的快捷键是点击两次 R 。
在 Flutter 中,如果你使用的是 IntelliJ 或者 Android Studio, 可以使用 Save All (⌘s/ctrl-s),或者可以点击工具栏上的 Hot Reload 按钮。如果你是在命令行里使用 flutter run 命令运行的程序,在窗口里输入 r 即可。也可以输入 R 进行彻底的重启。
16.3 在 Flutter 中使用什么工具可以调试应用程序呢?
关于 Flutter 应用程序的调试,有多个可选方式和工具供你选择。
除了 Flutter analyzer,还可以使用 Dart Observatory, 它可用于调试 Dart 应用程序。如果你是通过命令行运行 flutter run启动应用程序的, 可以打开 Observatory URL 所显示的地址,比如:http://127.0.0.1:8100/。
Observatory 包括程序运行状态监控,堆栈测试,运行代码监视,内存泄漏和内存分段调试。如果想了解更多详细内容,请参考Observatory documentation。
如果你在使用 IDE,那么你可以直接使用 IDE 内置的调试器进行调试。
如果你使用的是 IntelliJ 和 Android Studio,你可以使用 Flutter Inspector。Flutter Inspector 很好上手,并且可以帮你洞悉应用程序图像渲染的过程。通过它,你可以:
以 widget 树的形式查看应用程序的 UI 结构
在设备或者模拟器上选择某一个点,然后找到该位置图像对应的 widget
查看每个 widget 的属性
快速定位布局问题并找到原因
Flutter Inspector 窗口可以通过如下步骤打开:View > Tool Windows > Flutter Inspector。只有在应用程序运行的时候才会显示内容。
如果要查看特定的 widget,在工具栏中选择 Toggle inspect mode ,然后在已连接的手机或者模拟器上点击对应的 widget。该 widget 会高亮显示。你就可以在 IntelliJ 看到对应的 widget 层级和 widget 属性。
更多详细内容,请查看 Debugging Flutter Apps。
十七、动画
精美的动画效果会使得 UI 更加直观,可以提升整体视觉效果,使应用显得更加精致,从而提升用户体验。Flutter 的动画框架使得开发者能够更方便地实现简单和复杂的动画。Flutter SDK 含有很多 Material Design widget。其中已经包括了标准的动画效果,你可以很方便地自定义这些效果。
在 React Native 中,动画 API 用于创建动画。
在 Flutter 中,使用 Animation 类和 AnimationController 类实现动画。Animation 是抽象类,内含其当前的值和它的状态(已完成或者已取消)。AnimationController 类可以正向或者反向播放动画或者停止动画以及为动画设置特定值来自定义动画。
17.1 如何添加一个简单的淡入动画效果?
在下面的 React Native 示例中,有一个动画组件,也就是 FadeInView,它是使用 Animated API 创建的。定义了初始的不透明状态,最终状态和动画切换之间的时间间隔。在 Animated 中添加了动画组件,不透明状态 fadeAnim 映射到我们想要添加动画效果的文本组件上,然后在开始动画的时候调用 start()。
// React Native
class FadeInView extends React.Component {
state = {
fadeAnim: new Animated.Value(0) // Initial value for opacity: 0
};
componentDidMount() {
Animated.timing(this.state.fadeAnim, {
toValue: 1,
duration: 10000
}).start();
}
render() {
return (
{this.props.children}
);
}
}
...
Fading in
...
要在 Flutter 中实现相同的动画效果,创建一个 AnimationController 对象,叫它 controller,并且指定时间间隔。在默认配置下, AnimationController 会在给定时间间隔线性的生成从 0.0 到 1.0 的数值。当你的程序可以显示新一帧画面的时候,AnimationController 会生成一个新的值。通常,这个频率在每秒 60 个值。
当定义 AnimationController 的时候,你必须传入一个 vsync 对象。vsync 会防止屏幕显示区域之外的动画消耗不必要的资源。你可以通过添加 TickerProviderStateMixin 到类定义中来使用有状态的对象。AnimationController 需要传入一个 TickerProvider,它是通过构造函数里的 vsync 参数进行配置的。
Tween 定义了起始和结束值之间或者输入段到输出段之间的过渡。如果要在动画中使用 Tween 对象,调用 Tween 对象的 animate方法,然后把它赋给你要修改的 Animation 对象。
在这个例子中,用到了 FadeTransition widget,它的 opacity 属性映射到了 animation 对象上。
要开始动画,使用 controller.forward()。其它的操作也可以使用控制器里的方法,比如 fling() 或者 repeat()。这个例子里,FlutterLogo widget 被用于 FadeTransition widget 中。
// Flutter
import 'package:flutter/material.dart';
void main() {
runApp(Center(child: LogoFade()));
}
class LogoFade extends StatefulWidget {
_LogoFadeState createState() => _LogoFadeState();
}
class _LogoFadeState extends State with TickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 3000), vsync: this);
final CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.easeIn);
animation = Tween(begin: 0.0, end: 1.0).animate(curve);
controller.forward();
}
widget build(BuildContext context) {
return FadeTransition(
opacity: animation,
child: Container(
height: 300.0,
width: 300.0,
child: FlutterLogo(),
),
);
}
dispose() {
controller.dispose();
super.dispose();
}
}
在 React Native 中,无论 PanResponder 或者第三方库都可被用于滑动动画。
在 Flutter 中,要添加滑动动画,使用 Dismissible widget 封装其它子 widget 即可。
child: Dismissible(
key: key,
onDismissed: (DismissDirection dir) {
cards.removeLast();
},
child: Container(
...
),
),
(完)