原文:Flutter學習之旅——實用入坑指南
以上原文中沒有目錄索引,不太好找,因此把原文整理了下。html
一如前端深似海,今後節操是路人,今後再無安寧日,今後紅塵是路人。要說技術更迭速度,還有比前端更快的麼😂根本停不下來。這不,Google剛發佈Flutter不到一年時間,1.0正式版發佈不到兩個月。阿里系的閒魚老大哥,已經率先用Flutter重構了閒魚,雖然沒徹底重構,但高頻的重度頁面都是Flutter的了。這一幕似曾相識,當初RN出來的時候不也是閒魚團隊先吃的螃蟹嗎,在這裏向閒魚團隊的老哥們致敬🐣。前端
既然老大哥都出動了,也側面驗證了這項技術的可行性。當小弟的也不能落後嘛,天天抽時間斷斷續續的學了兩週時間,仿部分知乎的客戶端,擼了一套客戶端出來。前一週主要是熟悉Dart語言和常規的客戶端佈局方式,後一週主要是掌握使用HTTP的請求、下拉上拉、左滑右滑、長按等經常使用手勢、相機調用、video播放等進階用法。 兩週下來,基本上能夠開發80%以上常見的客戶端需求。html5
前期一直在用simulator開發,略有卡頓,心中不免有些疑惑。結果最後release打包到手機後,居然如絲般順滑!!!簡直喜出望外,徹底能夠睥睨原生開發,在這一點上的確要優於目前的RN。最重要的是做爲Materail Design極簡又有質感風格的鴨狗血粉絲,Flutter造出來的界面簡直倍爽。至此正式入坑Flutter開發。Google萬歲!android
Flutter是谷歌的移動UI框架,能夠快速在iOS和Android上構建高質量的原生用戶界面。 Flutter能夠與現有的代碼一塊兒工做。在全世界,Flutter正在被愈來愈多的開發者和組織使用,而且Flutter是徹底免費、開源的。Beta1版本於2018年2月27日在2018 世界移動大會公佈。
Beta2版本2018年3月6日發佈。
1.0版本於2018年12月5日(北京時間)發佈ios
這裏把學習過程當中一些經常使用高頻的東西總結出來,基本能知足大多數狀況下的開發需求。git
完整的代碼: https://github.com/flute/zhih...github
歡迎加入Flutter開拓交流,羣聊號碼:236379502json
TabController controller; @override void initState() { super.initState(); // initialize the tab controller // vsync ?? controller = new TabController(length: 5, vsync: this); } @override void dispose() { // dispose of tab controller controller.dispose(); super.dispose(); } ... body: new TabBarView( children: <Widget>[new HomeTab(), new IdeaTab(), new ColleagueTab(), new MessageTab(), new MeTab()], controller: controller, ), bottomNavigationBar: new Material( // background color of bottom navigation bar color: Colors.white, textStyle: new TextStyle( color: Colors.black45 ), child: new TabBar( unselectedLabelColor: Colors.black45, labelColor: Colors.blue, controller: controller, tabs: <Tab>[ new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.home, size: 25,), Text('首頁', style: TextStyle(fontSize: 10),) ], ), ), ), new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.access_alarm, size: 25,), Text('想法', style: TextStyle(fontSize: 10),) ], ), ), ), new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.access_time, size: 25,), Text('大學', style: TextStyle(fontSize: 10),) ], ), ), ), new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.account_balance_wallet, size: 25,), Text('消息', style: TextStyle(fontSize: 10),) ], ), ), ), new Tab( child: new Container( padding: EdgeInsets.only(top: 5), child: new Column( children: <Widget>[ Icon(Icons.adb, size: 25,), Text('個人', style: TextStyle(fontSize: 10),) ], ), ), ), ], ), ),
效果:數組
@override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: searchBar(), backgroundColor: Colors.white, bottom: new Text('bottom'), ), body: new Container() ); } /** * 頂部搜索欄 */ Widget searchBar() { return new Container( child: new Row( children: <Widget>[ new Expanded( child: new FlatButton.icon( color:Color.fromRGBO(229, 229, 229, 1.0), onPressed: (){ Navigator.of(context).push(new MaterialPageRoute(builder: (context){ return new SearchPage(); })); }, icon: new Icon( Icons.search, color: Colors.black38, size: 16.0, ), label: new Text( "諾獎得主爲上課推遲發佈會", style: new TextStyle(color: Colors.black38) ), ), ), new Container( child: new FlatButton.icon( onPressed: (){ Navigator.of(context).push(new MaterialPageRoute(builder: (context){ return new AskPage(); })); }, icon: new Icon( Icons.border_color, color: Colors.blue, size: 14.0 ), label: new Text( '提問', style: new TextStyle(color: Colors.blue), ), ), ) ], ), ); }
Container( margin: EdgeInsets.only(right: 5), decoration: new BoxDecoration( shape: BoxShape.circle, image: new DecorationImage( image: new NetworkImage(avatarUrl), ) ), width: 30, height: 30, ),
https://github.com/flutter/fl...app
bool notNull(Object o) => o != null; Widget build() { return new Column( children: <Widget>[ new Title(), new Body(), shouldShowFooter ? new Footer() : null ].where(notNull).toList(), ); }
Text( content, maxLines: 3, overflow: TextOverflow.ellipsis, style: new TextStyle(fontSize: 14, color: Colors.black54), ),
https://stackoverflow.com/que...
return Container( width: 40, height:40, // flutter中的margin沒有負值的說法 // https://stackoverflow.com/questions/42257668/the-equivalent-of-wrap-content-and-match-parent-in-flutter transform: Matrix4.translationValues(-20.0, 0.0, 0.0), decoration: new BoxDecoration( border: Border.all(width: 3, color: Colors.white), color: Colors.black, shape: BoxShape.circle, image: new DecorationImage( image: new NetworkImage('https://pic3.zhimg.com/50/d2af1b6b1_s.jpg') ) ), );
https://stackoverflow.com/que...
new Container( height: 200, decoration: new BoxDecoration( image: new DecorationImage( image: NetworkImage('https://pic3.zhimg.com/50/v2-f9fd4b13a46f2800a7049a5724e5969f_400x224.jpg'), fit: BoxFit.fill ) ), ),
justify-content: mainAxisAlignment
align-items: crossAxisAlignment
column 設置crossAxisAlignment: stretch後子元素寬度爲100%,若是想讓子元素寬度不爲100%, 將其包裹在Row元素中便可。
flutter row and column
https://medium.com/jlouage/fl...
使用GestureDetector包裹widget便可。
child: new GestureDetector( onTap: click, child: Text( name, style: TextStyle(color: Colors.black87), ), ),
https://codeburst.io/top-10-a...
https://stackoverflow.com/que...
class DetailPage extends StatefulWidget { @override DetailPageState createState() => DetailPageState(); } class DetailPageState extends State<DetailPage> { final GlobalKey _menuKey = new GlobalKey(); .... .... .... child: new Row( children: <Widget>[ new Container( child: new GestureDetector( onTap: () { dynamic state = _menuKey.currentState; state.showButtonMenu(); }, child: new Container( child: new Text('默認排序'), ), ), ), new PopupMenuButton( icon: Icon(Icons.keyboard_arrow_down), offset: Offset(0, 50), key: _menuKey, itemBuilder: (_) => <PopupMenuItem<String>>[ new PopupMenuItem<String>( child: const Text('默認排序'), value: 'default'), new PopupMenuItem<String>( child: const Text('按時間排序'), value: 'timeline'), ], onSelected: (_) {} ) ], ),
水平分割線 Divider
垂直分割線 VerticalDivider (無效???)
https://pub.dartlang.org/pack...
import 'package:flutter_swiper/flutter_swiper.dart'; ... var images = [ 'https://pic3.zhimg.com/v2-5806d9e33e36fa772c8da56c931bb416_b.jpg', 'https://pic1.zhimg.com/50/v2-f355ca177e011626938b479f0e2e3e03_hd.jpg', 'https://pic2.zhimg.com/v2-d8e47ed961b93b875ad814104016bdfd_b.jpg' ]; child: new Swiper( itemBuilder: (BuildContext context,int index){ return new Image.network(images[index], fit: BoxFit.cover,); }, itemCount: 3, pagination: new SwiperPagination(), //control: new SwiperControl(), ),
https://proandroiddev.com/a-d...
floatingActionButton 配合 Scaffold 使用最佳
Scaffold( floatingActionButton: new FloatingActionButton( onPressed: (){}, child: Icon(Icons.edit), //mini: true, ), // 默認右下角,可設置位置。 floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, )
SingleChildScrollView
水平方向滑動 scrollDirection: Axis.horizontal
https://stackoverflow.com/que...
import 'dart:ui'; new BackdropFilter( filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), child: Text(desc, style: TextStyle(color: Colors.white),), ),
https://docs.flutter.io/flutt...
AlertDialog
void _showDialog(BuildContext context) { // flutter defined function showDialog( context: context, builder: (BuildContext context) { // return object of type Dialog return AlertDialog( title: Text('Rewind and remember'), content: SingleChildScrollView( child: ListBody( children: <Widget>[ Text('You will never be satisfied.'), Text('You\’re like me. I’m never satisfied.'), ], ), ), actions: <Widget>[ // usually buttons at the bottom of the dialog new FlatButton( child: new Text("Close"), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } // 調用 .... onPressed: (){ _showDialog(context); }, ....
https://flutterchina.club/net...
// 加載庫 import 'dart:convert'; import 'dart:io'; // 請求 try { var request = await httpClient.getUrl(Uri.parse(url)); var response = await request.close(); if (response.statusCode == HttpStatus.OK) { var json = await response.transform(UTF8.decoder).join(); var data = JSON.decode(json); result = data['origin']; } else { result = 'Error getting IP address:\nHttp status ${response.statusCode}'; } } catch (exception) { result = 'Failed getting IP address'; } // 保存返回的數據 // error: setState() called after dispose() // If the widget was removed from the tree while the message was in flight, // we want to discard the reply rather than calling setState to update our // non-existent appearance. if (!mounted) return; setState(() { _ipAddress = result; });
import 'dart:async'; Future<Null> _onRefresh() { Completer<Null> completer = new Completer<Null>(); new Timer(new Duration(seconds: 3), () { print("timer complete"); completer.complete(); }); return completer.future; }
new RefreshIndicator( onRefresh: _onRefresh, child: new SingleChildScrollView( child: new Container( padding: EdgeInsets.all(10), child: new Text(_jsonData), ), ), )
https://juejin.im/post/5b3abf...
ScrollController _controller = new ScrollController(); @override void initState() { super.initState(); _controller.addListener((){ if(_controller.position.pixels == _controller.position.maxScrollExtent) { print('下拉加載'); _getMoreData(); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } ... scroll controller: _controller ...
https://www.jianshu.com/p/4db...
➜ zh git:(master) ✗ flutter clean Deleting 'build/'. ➜ zh git:(master) ✗ rm -rf ios/Flutter/App.framework ios/Flutter/Flutter.framework ➜ zh git:(master) ✗ rm -rf /Users/ludis/Library/Developer/Xcode/DerivedData/Runner-
一、在安卓真機release後 ios simulator沒法編譯
Launching lib/main.dart on iPhone X in debug mode... Xcode build done. 1.0s Failed to build iOS app Error output from Xcode build: ↳ ** BUILD FAILED ** Xcode's output: ↳ === BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Debug === diff: /Users/ludis/Desktop/opt/flutter/zh/ios/Pods/Manifest.lock: No such file or directory error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation. Could not build the application for the simulator. Error launching application on iPhone X. Exited (sigterm)
解決
cd ios pod install
https://medium.com/flutter-co...
在scrollView的滾動佈局中,若是使用column組件,併爲其添加Expanded擴展子組件的話,這二者會存在衝突。
若是堅持要使用此佈局,在column設置mainAxisSize: MainAxisSize.min,同時子組件由Expanded改成Flexible便可。
https://www.cnblogs.com/pengs...
new TextFormField( maxLength: 32, onSaved: (val)=> this._config = val, validator: (v)=>(v == null || v.isEmpty)?"請選擇配置": null, decoration: new InputDecoration( labelText: '配置', ), ),
new TextField( keyboardType: TextInputType.multiline, maxLines: 3, maxLength: 100, ),
new Radio( groupValue: this.radio, activeColor: Colors.blue, value: 'aaa', onChanged: (String val) { // val 與 value 的類型對應 this.setState(() { this.radio = val; // aaa }); }, ),
new Checkbox( value: flutter, activeColor: Colors.blue, onChanged: (val) { setState(() { flutter = val; }); }, ),
new Switch( activeColor: Colors.green, value: flutter, onChanged: (val) { setState(() { flutter = val; }); }, ),
new Slider( value: _slider, min: 0.0, max: 100.0, onChanged: (val) { setState(() { _slider = val; }); }, ),
// 設置存儲日期的變量 DateTime _dateTime = new DateTime.now(); // 顯示文字Text,設置點擊事件,點擊後打開日期選擇器 new GestureDetector( onTap: (){ _showDatePicker(); }, child: new Container( child: new Text(_dateTime.toLocal().toString()), ), ), // 打開日期選擇器 void _showDatePicker() { _selectDate(context); } Future<Null> _selectDate(BuildContext context) async { final DateTime _picked = await showDatePicker( context: context, initialDate: _dateTime, firstDate: new DateTime(2016), lastDate: new DateTime(2050) ); if(_picked != null) { print(_picked); setState(() { _dateTime = _picked; }); } }
TimeOfDay _time = new TimeOfDay.now(); // text顯示當前時間 new GestureDetector( onTap: _showTimePicker, child: new Text(_time.format(context)), ), // 顯示timpicker void _showTimePicker(){ _selectTime(context); } Future<Null> _selectTime(BuildContext context) async { final TimeOfDay _picker = await showTimePicker( context: context, initialTime: _time, ); if(_picker != null) { print(_picker); setState(() { _time = _picker; }); } }
https://material.io/design/co...
void _showToast(BuildContext context) { final scaffold = Scaffold.of(context); scaffold.showSnackBar( SnackBar( content: const Text('Added to favorite'), action: SnackBarAction( label: 'UNDO', onPressed: scaffold.hideCurrentSnackBar ), ), ); }
https://github.com/PonnamKart...
void _showToast(String title) { Fluttertoast.showToast( msg: title, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIos: 1, backgroundColor: Color.fromRGBO(0, 0, 0, 0.85), textColor: Colors.white ); }
http://flatteredwithflutter.c...
import 'package:flutter/cupertino.dart'; new MaterialButton( onPressed: () { _showActionSheet(); }, child: new Text('show ActionSheet', style: TextStyle(color: Colors.white),), color: Colors.greenAccent, ), void _showActionSheet() { showCupertinoModalPopup( context: context, builder: (BuildContext context) => actionSheet(), ).then((value) { Scaffold.of(context).showSnackBar(new SnackBar( content: new Text('You clicked $value'), )); }); } Widget actionSheet(){ return new CupertinoActionSheet( title: new Text('title'), message: const Text('your options are'), actions: <Widget>[ CupertinoActionSheetAction( child: const Text('yes'), onPressed: (){ Navigator.pop(context, 'yes'); }, ), CupertinoActionSheetAction( child: const Text('no'), onPressed: (){ Navigator.pop(context, 'no'); }, ) ], cancelButton: CupertinoActionSheetAction( child: new Text('cancel'), onPressed: () { Navigator.pop(context, 'Cancel'); }, ), ); }
https://flutter-es.io/widgets...
https://flutter.io/docs/cookb...
new Dismissible( // Each Dismissible must contain a Key. Keys allow Flutter to // uniquely identify Widgets. key: Key(item), onDismissed: (direction) { setState(() { items.removeAt(index); }); // Then show a snackbar! Scaffold.of(context) .showSnackBar(SnackBar(content: Text("$item dismissed"))); }, // Show a red background as the item is swiped away background: Container(color: Colors.red), child: ListTile(title: Text('$item')), );
https://github.com/letsar/flu...
Widget _swipe(int i, String title, String desc) { return new Slidable( delegate: new SlidableDrawerDelegate(), actionExtentRatio: 0.25, child: new Container( color: Colors.white, child: new GestureDetector( onTap: (){}, onDoubleTap: (){}, onLongPress: (){}, child: new ListTile( leading: new CircleAvatar( backgroundColor: Colors.grey[200], child: new Text( '$i', style: TextStyle(color: Colors.orange), ), foregroundColor: Colors.white, ), title: new Text( '$title', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: Colors.black87, fontSize: 16), ), subtitle: new Text( '$desc', style: TextStyle(color: Colors.blue[300]), ), ), ) ), actions: <Widget>[ new IconSlideAction( caption: 'Archive', color: Colors.blue, icon: Icons.archive, onTap: () => _showSnackBar('Archive'), ), new IconSlideAction( caption: 'Share', color: Colors.indigo, icon: Icons.share, onTap: () => _showSnackBar('Share'), ), ], secondaryActions: <Widget>[ new IconSlideAction( caption: 'More', color: Colors.black45, icon: Icons.more_horiz, onTap: () => _showSnackBar('More'), ), new IconSlideAction( caption: 'Delete', color: Colors.red, icon: Icons.delete, onTap: () => _showSnackBar('Delete'), ), ], ); }
new GestureDetector( onTap: (){_showToast('點擊: $i');}, onDoubleTap: (){_showToast('連點: $i');}, onLongPress: (){_showToast('長按: $i');}, )
https://github.com/flutter/pl...
https://github.com/flutter/plugins/tree/master/packages/image_picker
dynamic _picture; dynamic _gallery; new FlatButton.icon( icon: Icon(Icons.camera), label: Text('選擇頭像'), onPressed: (){ _optionsDialogBox(); }, ), Future<void> _optionsDialogBox() { return showDialog(context: context, builder: (BuildContext context) { return AlertDialog( content: new SingleChildScrollView( child: new ListBody( children: <Widget>[ GestureDetector( child: new Text('Take a picture'), onTap: openCamera, ), Padding( padding: EdgeInsets.all(8.0), ), GestureDetector( child: new Text('Select from gallery'), onTap: openGallery, ), ], ), ), ); }); } void openCamera() async { Navigator.of(context).pop(); var picture = await ImagePicker.pickImage( source: ImageSource.camera, ); setState(() { _picture = picture; }); } void openGallery() async { Navigator.of(context).pop(); var gallery = await ImagePicker.pickImage( source: ImageSource.gallery, ); setState(() { _gallery = gallery; }); }
https://pub.dartlang.org/pack...
camera: ^0.2.9 import 'package:camera/camera.dart'; class _CameraState extends State<CameraWidget> { List<CameraDescription> cameras; CameraController controller; bool _isReady = false; @override void initState() { super.initState(); _setupCameras(); } Future<void> _setupCameras() async { try { // initialize cameras. cameras = await availableCameras(); // initialize camera controllers. controller = new CameraController(cameras[0], ResolutionPreset.medium); await controller.initialize(); } on CameraException catch (_) { // do something on error. } if (!isMounted) return; setState(() { _isReady = true; }); } Widget build(BuildContext context) { if (!_isReady) return new Container(); return new Container( height: 200, child: AspectRatio( aspectRatio: controller.value.aspectRatio, child: CameraPreview(controller), ), ) } }
https://github.com/flutter/pl...
video_player: ^0.8.0 import 'package:video_player/video_player.dart'; VideoPlayerController _controller; bool _isPlaying = false; @override void initState() { super.initState(); _controller = VideoPlayerController.network( 'https://www.quirksmode.org/html5/videos/big_buck_bunny.mp4', ) ..addListener(() { final bool isPlaying = _controller.value.isPlaying; if (isPlaying != _isPlaying) { setState(() { _isPlaying = isPlaying; }); } }) ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. setState(() {}); }); } @override void dispose() { _controller?.dispose(); super.dispose(); } // 顯示、控制 _controller.value.initialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, child: new Container( padding: EdgeInsets.all(10), color: Colors.black, child: VideoPlayer(_controller), ), ) : Container( child: new Text('視頻加載中~'), ), new FlatButton.icon( label: Text('播放/暫停'), icon: Icon( _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, ), onPressed: _controller.value.isPlaying ? _controller.pause : _controller.play, )
https://github.com/rxlabz/aud...