Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。这篇文章主要讲述了使用flutter开发的大致流程及可能遇到的问题,关于flutter开发的详细文档,可以参考 flutter中文网。
UI构建
传统的android开发使用 xml
布局文件构建UI,而flutter使用树形结构的多个widget完成UI构造。大致过程为
- 构建页面框架,一般继承
Stateless Widget
,并使用Scaffold
构建AppBar
与Drawer
- 构建页面的内容
- 不会更新的控件继承
Stateless Widget
- 会更新的控件继承
Stateful Widget
- 不会更新的控件继承
- 编写页面其它逻辑,并在需要的地方调用
setState
方法
下面是一个简单的例子(实际效果以运行后的结果为准):
/// 构建一个页面,继承 Stateless Widget
class NotesInSheetPage extends StatelessWidget {
final Sheet sheet;
const NotesInSheetPage(this.sheet, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
NoteInSheetBody body = NoteInSheetBody();
return Scaffold(
appBar: AppBar( // AppBar
title: Text(sheet.title),
titleSpacing: 0,
leading: UtilUI.backButton(() => Navigator.pop(context)), // IconButton with Icons.arrow_back
actions: <Widget>[UtilUI.syncWidget(body.doSync)]), // an IconButton has Icons.sync icon
body: body,
floatingActionButton: Builder(builder: (BuildContext context) { // floating add button
return UtilUI.floatAddButton(() {
UtilUI.showSnackBar(context, Text(ResString.pleaseExpect));
});
}));
}
}
class NoteInSheetBody extends StatefulWidget {
final _NoteInSheetBodyState state = _NoteInSheetBodyState();
@override
_NoteInSheetBodyState createState() => state;
void doSync() => state.syncNotes();
}
class _NoteInSheetBodyState extends State<NoteInSheetBody> {
List<Note> noteList;
bool syncing = false;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(ResPadMargin.common1),
child: ListView.builder(
itemCount: noteList != null ? noteList.length : 0,
itemBuilder: noteListItemBuilder,
));
}
/// builder of note list item
Widget noteListItemBuilder(BuildContext context, int index) {
Note tmpNote = noteList[index];
return GestureDetector(
child: NotesListItem(
tmpNote.title,
Util.formatTime(tmpNote.lastModTime),
tmpNote.content,
),
onTap: () => Navigator.push(
context, MaterialPageRoute(builder: (context) => NoteDetailPage(tmpNote))),
onLongPressStart: (LongPressStartDetails details) =>
_showDeleteMenu(details.globalPosition));
}
/// show popup menu
void _showDeleteMenu(Offset pos) {}
/// sync all notes
void syncNotes() async {
// ... fetch data from server
setState(() => noteList = tmpNoteList);
}
}
可以看到,flutter中使用Widget来构建UI, Scaffold
, AppBar
, ListView
等都是常用的控件。
Stateless Widget与Stateful Widget
Stateless Widget
即无状态的控件,它可以用来构建UI中永远不会改变的部分,如下面的 Text
文本显示控件:
new Text('I like Flutter!', style: new TextStyle(fontWeight: FontWeight.bold));
而 Stateful Widget
则是有状态的,对于与用户交互或者从网络获取数据的控件,应当使用它来构建。对于 Stateful Widget
来说,状态 State
是对其能够变化部分的抽象。当一个 Stateful Widget
需要更新(状态发生改变)的时候,可以通过 setState(void Function())
更新它的状态,如上面的 NoteInSheetBody
。
值得一提的是,flutter建议尽量将状态下移,即只让真正需要更新的控件是Stateful
的,因为在setState
的时候会重绘这个控件,若顶层控件为Stateful
的,那么整个页面都会重新绘制。
遇到的一些问题
构建UI固然简单,但与逻辑结合起来的时候则可能遇到许多问题,如下是我遇到的一些问题即解决方法:
- Scaffold.of() called with a context that does not contain a Scaffold
这个问题一般是因为调用Scaffold.of(context)
的时候其中的context
是一个Scaffold
的BuildContext
,如下所示
其中浮动按钮Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(sheet.title)), body: Container(), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () => Scaffold.of(context).showSnackBar(content: Text('test')), )); }
floatingActionButton
的onPressed
方法中使用了此Scaffold
的context
,因此会产生错误。解决方法有两个,一种是将调用Scaffold.of()
的控件分离到另一个类中,另一种是使用一个builder
,如下所示:Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(sheet.title)), body: Container(), floatingActionButton: Builder(builder: (BuildContext context) { return FloatingActionButton( child: Icon(Icons.add), onPressed: () => Scaffold.of(context).showSnackBar(content: Text('test')), ); })); }
- 长按弹出菜单
GestureDetector
提供的onLongPress
方法并未包含触摸位置信息,因此需要使用onLongPressStart
方法,其函数签名为void Function(LongPressStartDetails details)
,其中的detail参数的globalPosition
属性包含了触摸屏幕的坐标信息。如下为简化后的代码:
如代码所示,showMenu方法可以在给定的/// builder of note list item Widget noteListItemBuilder(BuildContext context, int index) { return GestureDetector( // ... other content onLongPressStart: (LongPressStartDetails details) => showMenu( context: context, // generate a PopupMenuEntry with given texts and values items: UtilUI.popUpMenuEntry([ResString.delete], [1]), position: RelativeRect.fromLTRB( details.globalPosition.dx, details.globalPosition.dy, details.globalPosition.dx, details.globalPosition.dy, )).then((value) => print('pressed: $value')); }
position
处显示一个弹出菜单,并在菜单点击或取消时调用then
中的方法。 - flutter build apk的注意事项
- 一定要确定manifest文件中的权限是否声明
- 可以使用参数
--target-platform android-arm
生成特定版本的apk