前言
这是笔者作为一个Android
工程师入门Flutter
的学习笔记,笔者不想通过一种循规蹈矩的方式来学习:先学Dart
语言,然后学习Flutter
的基本使用,再到实践应用这样的步骤。这样的方式有点无趣且效率较低。
笔者觉得对于已经有Android
基础的来说,通过类比Android
的方式来学习Flutter
,掌握核心基础概念后,直接开发实践应用,在这个过程中去学习其中的知识比如Dart
语法、深入的知识点。这是笔者的一次学习尝试,并将其记录下来:
给Android工程师的Flutter入门手册(一)
给Android工程师的Flutter入门手册(二)
本篇是该系列的第三篇,主要内容是:
(1)布局:Android常用的布局对应Flutter的实现
(2)列表视图和适配器:Flutter中如何实现列表展示和适配
(3)主题:主题的应用
布局
LinearLayout
在 Android
中,LinearLayout
用于线性布局 widget
的——水平或者垂直。在 Flutter
中,使用 Row
或者 Column
Widget来实现相同的效果。
Widget getRowWidget() {return Row(mainAxisAlignment: MainAxisAlignment.center,children: const <Widget>[Text('Row One'),Text('Row Two'),Text('Row Three'),Text('Row Four'),],);}Widget getColumnWidget() {return Column(mainAxisAlignment: MainAxisAlignment.center,children: const <Widget>[Text('Column One'),Text('Column Two'),Text('Column Three'),Text('Column Four'),],);}Widget getRowWidget() { return Row( mainAxisAlignment: MainAxisAlignment.center, children: const <Widget>[ Text('Row One'), Text('Row Two'), Text('Row Three'), Text('Row Four'), ], ); } Widget getColumnWidget() { return Column( mainAxisAlignment: MainAxisAlignment.center, children: const <Widget>[ Text('Column One'), Text('Column Two'), Text('Column Three'), Text('Column Four'), ], ); }Widget getRowWidget() { return Row( mainAxisAlignment: MainAxisAlignment.center, children: const <Widget>[ Text('Row One'), Text('Row Two'), Text('Row Three'), Text('Row Four'), ], ); } Widget getColumnWidget() { return Column( mainAxisAlignment: MainAxisAlignment.center, children: const <Widget>[ Text('Column One'), Text('Column Two'), Text('Column Three'), Text('Column Four'), ], ); }
仔细看上面代码,会发现除了 Row
和 Column
widget 以外是一模一样的。它们的子级是一样的,这个特性可以被充分利用来开发包含有相同的子级,但是会随时间改变的复杂布局。
RelativeLayout
在Android中
,RelativeLayout
表示相对布局,通过 Widget
的相互位置对它们进行布局。
在 Flutter
中,可以通过组合使用 Column
、Row
和 Stack Widget
实现 RelativeLayout
的效果。
层叠布局 Stack
和 Web
中的绝对定位、Android
中的 Frame
布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。层叠布局允许子组件按照代码中声明的顺序堆叠起来。
Flutter
中使用Stack
和Positioned
这两个组件来配合实现绝对定位。Stack
允许子组件堆叠,而Positioned
用于根据Stack
的四个角来确定子组件的位置。
Widget getRelativeLayoutWidget() {// ConstrainedBox来确保Stack占满屏幕return ConstrainedBox(constraints: const BoxConstraints.expand(),child: Stack(alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式children: <Widget>[Container(color: Colors.red,child: const Text("Hello Flutter",style: TextStyle(color: Colors.white)),),const Positioned(left: 18.0,child: Text("Text1 On Left"),),const Positioned(top: 18.0,child: Text("Text2 On Top"),),const Positioned(right: 18.0,child: Text("Text3 On Right"),),const Positioned(bottom: 18.0,child: Text("Text3 On Right"),)],),);}Widget getRelativeLayoutWidget() { // ConstrainedBox来确保Stack占满屏幕 return ConstrainedBox( constraints: const BoxConstraints.expand(), child: Stack( alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式 children: <Widget>[ Container( color: Colors.red, child: const Text("Hello Flutter",style: TextStyle(color: Colors.white)), ), const Positioned( left: 18.0, child: Text("Text1 On Left"), ), const Positioned( top: 18.0, child: Text("Text2 On Top"), ), const Positioned( right: 18.0, child: Text("Text3 On Right"), ), const Positioned( bottom: 18.0, child: Text("Text3 On Right"), ) ], ), ); }Widget getRelativeLayoutWidget() { // ConstrainedBox来确保Stack占满屏幕 return ConstrainedBox( constraints: const BoxConstraints.expand(), child: Stack( alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式 children: <Widget>[ Container( color: Colors.red, child: const Text("Hello Flutter",style: TextStyle(color: Colors.white)), ), const Positioned( left: 18.0, child: Text("Text1 On Left"), ), const Positioned( top: 18.0, child: Text("Text2 On Top"), ), const Positioned( right: 18.0, child: Text("Text3 On Right"), ), const Positioned( bottom: 18.0, child: Text("Text3 On Right"), ) ], ), ); }
通过Stack
和Positioned
,可以指定一个或多个子元素相对于父元素各个边的精确偏移,并且可以重叠。
但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用Align
组件会更简单一些。
Align
组件可以调整子组件的位置:
Align({Key key,this.alignment = Alignment.center,this.widthFactor,this.heightFactor,Widget child,})Align({ Key key, this.alignment = Alignment.center, this.widthFactor, this.heightFactor, Widget child, })Align({ Key key, this.alignment = Alignment.center, this.widthFactor, this.heightFactor, Widget child, })
alignment
: 需要一个AlignmentGeometry
类型的值,表示子组件在父组件中的起始位widthFactor
和heightFactor
是用于确定Align
组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是Align
组件的宽高。如果值为null
,则组件的宽高将会占用尽可能多的空间。
ScrollView
在 Android
中,使用 ScrollView
布局 `widget,如果用户的设备屏幕比应用的内容区域小,用户可以滑动内容。
在 Flutter
中,实现这个功能的最简单的方法是使用 ListView
widget, Flutter 中 ListView
widget 既可以说是Android
中 ScrollView
,也是 ListView
最简单的用法就是这样:
Widget getScrollListView() {return SizedBox(width: 200,height: 100,child: Align(alignment: Alignment.center,child: ListView(scrollDirection: Axis.vertical,children: const <Widget>[Text('ListView One', ),Text('ListView Two', ),Text('ListView Three', ),Text('ListView Four', ),Text('ListView Five', ),Text('ListView Six', ),],),));}Widget getScrollListView() { return SizedBox( width: 200, height: 100, child: Align(alignment: Alignment.center, child: ListView( scrollDirection: Axis.vertical, children: const <Widget>[ Text('ListView One', ), Text('ListView Two', ), Text('ListView Three', ), Text('ListView Four', ), Text('ListView Five', ), Text('ListView Six', ), ], ), )); }Widget getScrollListView() { return SizedBox( width: 200, height: 100, child: Align(alignment: Alignment.center, child: ListView( scrollDirection: Axis.vertical, children: const <Widget>[ Text('ListView One', ), Text('ListView Two', ), Text('ListView Three', ), Text('ListView Four', ), Text('ListView Five', ), Text('ListView Six', ), ], ), )); }
但是只用List是没有滚动条,怎么快速加上呢:
Scrollbar
是一个Material风格的滚动条,如果要给可滚动组件添加滚动条,只需将Scrollbar
作为可滚动组件的任意一个父级组件即可:
Widget getScrollListView() {return Scrollbar(child: SizedBox(width: 200,height: 100,child: Align(alignment: Alignment.center,child: ListView(scrollDirection: Axis.vertical,children: const <Widget>[Text('ListView One', ),Text('ListView Two', ),Text('ListView Three', ),Text('ListView Four', ),Text('ListView Five', ),Text('ListView Six', ),],),)));}Widget getScrollListView() { return Scrollbar( child: SizedBox( width: 200, height: 100, child: Align( alignment: Alignment.center, child: ListView( scrollDirection: Axis.vertical, children: const <Widget>[ Text('ListView One', ), Text('ListView Two', ), Text('ListView Three', ), Text('ListView Four', ), Text('ListView Five', ), Text('ListView Six', ), ], ), ))); }Widget getScrollListView() { return Scrollbar( child: SizedBox( width: 200, height: 100, child: Align( alignment: Alignment.center, child: ListView( scrollDirection: Axis.vertical, children: const <Widget>[ Text('ListView One', ), Text('ListView Two', ), Text('ListView Three', ), Text('ListView Four', ), Text('ListView Five', ), Text('ListView Six', ), ], ), ))); }
列表视图和适配器
使用 Android
的 ListView
时,创建一个 adapter
并将其传给 ListView
, ListView
渲染 adapter
返回的每一行内容。然后,你需要确保回收了每一行视图,否则,你会遇到各种奇怪的界面和内存问题。
因为 Flutter widget
不可变的特点,你需要向 ListView
传入一组 widget
,Flutter
会保证滑动的快速顺畅。
添加分割线和点击事件
实现一个带分割线,并且每个Item可以响应点击的ListView:
Widget getListView() {return ListView.separated(itemBuilder: (BuildContext context, int index) {return GestureDetector(onTap: () {debugPrint('item tapped $index');},child: ListTile(title: Text("ITEM $index")),);},separatorBuilder: (BuildContext context, int index) {return const Divider(color: Colors.blue);},itemCount: 100);}Widget getListView() { return ListView.separated( itemBuilder: (BuildContext context, int index) { return GestureDetector( onTap: () { debugPrint('item tapped $index'); }, child: ListTile(title: Text("ITEM $index")), ); }, separatorBuilder: (BuildContext context, int index) { return const Divider(color: Colors.blue); }, itemCount: 100); }Widget getListView() { return ListView.separated( itemBuilder: (BuildContext context, int index) { return GestureDetector( onTap: () { debugPrint('item tapped $index'); }, child: ListTile(title: Text("ITEM $index")), ); }, separatorBuilder: (BuildContext context, int index) { return const Divider(color: Colors.blue); }, itemCount: 100); }
如何动态更新 ListView?
在 Android
中,通过adapter
调用 notifyDataSetChanged
实现列表刷新。
在 Flutter
中,如果你准备在 setState()
里更新一组 widget
,你很快会发现你的数据并没有更新到界面上。这是因为当 setState()
被调用的时候, Flutter
渲染引擎会查看Widget
树是否有任何更改。当引擎检查到 ListView
,他会执行 ==
检查,并判断两个 ListView
是一样的。没有任何更改,所以也就不需要更新。
所以,更新 ListView
的一个简单方法是,在 setState()
里创建一个新的 List
,并将数据从旧列表拷贝到新列表。虽然这个方法很简单,但是不推荐在大数据集的时候使用。
推荐的高效且有效的创建一个列表的方法是使用 ListView.Builder
。这个方法非常适用于动态列表或者拥有大量数据的列表。可以理解它就是Android
里的 RecyclerView
,会为你自动回收列表项:
对上小节代码做个改造,完整代码如下:
class _SampleAppPageState extends State<SampleAppPage> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('ListView Demo'),),body: getListView());}List<Widget> widgets = [];@overridevoid initState() {super.initState();for (int i = 0; i < 100; i++) {widgets.add(getItemView(i));}}Widget getListView() {return ListView.separated(itemBuilder: (BuildContext context, int index) {return getItemView(index);},separatorBuilder: (BuildContext context, int index) {return const Divider(color: Colors.blue);},itemCount: widgets.length);}Widget getItemView(int index) {return GestureDetector(onTap: () {debugPrint('item tapped $index');setState(() {debugPrint('item setState $index');widgets.add(getItemView(index + 1)); // tap后添加一个新数据});},child: ListTile(title: Text("ITEM $index")),);}}class _SampleAppPageState extends State<SampleAppPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('ListView Demo'), ), body: getListView()); } List<Widget> widgets = []; @override void initState() { super.initState(); for (int i = 0; i < 100; i++) { widgets.add(getItemView(i)); } } Widget getListView() { return ListView.separated( itemBuilder: (BuildContext context, int index) { return getItemView(index); }, separatorBuilder: (BuildContext context, int index) { return const Divider(color: Colors.blue); }, itemCount: widgets.length); } Widget getItemView(int index) { return GestureDetector( onTap: () { debugPrint('item tapped $index'); setState(() { debugPrint('item setState $index'); widgets.add(getItemView(index + 1)); // tap后添加一个新数据 }); }, child: ListTile(title: Text("ITEM $index")), ); } }class _SampleAppPageState extends State<SampleAppPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('ListView Demo'), ), body: getListView()); } List<Widget> widgets = []; @override void initState() { super.initState(); for (int i = 0; i < 100; i++) { widgets.add(getItemView(i)); } } Widget getListView() { return ListView.separated( itemBuilder: (BuildContext context, int index) { return getItemView(index); }, separatorBuilder: (BuildContext context, int index) { return const Divider(color: Colors.blue); }, itemCount: widgets.length); } Widget getItemView(int index) { return GestureDetector( onTap: () { debugPrint('item tapped $index'); setState(() { debugPrint('item setState $index'); widgets.add(getItemView(index + 1)); // tap后添加一个新数据 }); }, child: ListTile(title: Text("ITEM $index")), ); } }
其中ItemBuilder
方法和 Android adapter
里的 getView
方法类似;它通过位置返回你期望在这个位置渲染的列表项。
最重要的一条是, onTap()
方法不重建列表项,而是对widget
集合执行元素添加的操作,添加后就会自动动态更新ListView
的数据显示了
主题
Android
中你在 XML
文件中定义主题并在 AndroidManifest.xml
中将其赋值给你的应用。
Flutter
中是在顶层 Widget
上声明主题。为了在应用中利用好 Material
组件,可以在应用中声明一个顶层 Widget
–MaterialApp
作为入口。
如何定义主题
Flutter
提供开箱即用的优美的 Material Design
实现,可以满足你通常需要的各种样式和主题的需求。MaterialApp
是一个包装了一系列 Widget
的为你给予便利的 Widget
,而这些 Widget
通常是实现 Material Design
的应用所必须的。它基于 WidgetsApp
并添加了Material
相关的功能。
当然可以使用 WidgetApp
作为应用的 Widget
,它会提供一些相同的功能,但是不如 MaterialApp
提供的功能丰富。
如果要自定义任意子组件的颜色或者样式,给 MaterialApp
这个Widget
传入一个 ThemeData
对象即可。
例如,在下面的代码中,主色调设置为蓝色,定义一些文本主题:
Widget build(BuildContext context) {const appName = 'Custom Themes';return MaterialApp(title: appName,theme: ThemeData(primarySwatch: Colors.blue, // 主色调设置为蓝色// 文本主题textTheme: const TextTheme(displayLarge: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),titleLarge: TextStyle(fontSize: 36.0, fontStyle: FontStyle.italic),bodyMedium: TextStyle(fontSize: 14.0, fontFamily: 'Hind'),),),home: const MyHomePage(title: appName,),);}Widget build(BuildContext context) { const appName = 'Custom Themes'; return MaterialApp( title: appName, theme: ThemeData( primarySwatch: Colors.blue, // 主色调设置为蓝色 // 文本主题 textTheme: const TextTheme( displayLarge: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold), titleLarge: TextStyle(fontSize: 36.0, fontStyle: FontStyle.italic), bodyMedium: TextStyle(fontSize: 14.0, fontFamily: 'Hind'), ), ), home: const MyHomePage( title: appName, ), ); }Widget build(BuildContext context) { const appName = 'Custom Themes'; return MaterialApp( title: appName, theme: ThemeData( primarySwatch: Colors.blue, // 主色调设置为蓝色 // 文本主题 textTheme: const TextTheme( displayLarge: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold), titleLarge: TextStyle(fontSize: 36.0, fontStyle: FontStyle.italic), bodyMedium: TextStyle(fontSize: 14.0, fontFamily: 'Hind'), ), ), home: const MyHomePage( title: appName, ), ); }
应用文本主题
定义好文本主题后,就可以应用到Text
`中:
body: Center(child: Container(color: Theme.of(context).colorScheme.secondary,child: Text('This is a theme text',style: Theme.of(context).textTheme.titleLarge,),),)body: Center( child: Container( color: Theme.of(context).colorScheme.secondary, child: Text( 'This is a theme text', style: Theme.of(context).textTheme.titleLarge, ), ), )body: Center( child: Container( color: Theme.of(context).colorScheme.secondary, child: Text( 'This is a theme text', style: Theme.of(context).textTheme.titleLarge, ), ), )
参考
LinearLayout部分
How to design LinearLayout in Flutter.
BoxDecoration
主题部分