1. Flutter 安装
具体安装步骤需要按照中文安装教程安装,有两点非常重要:
1. 没有翻墙的情况一定要使用Flutter镜像
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
注意: 此镜像为临时镜像,并不能保证一直可用,读者可以参考详情请参考 Using Flutter in China 以获得有关镜像服务器的最新动态。
2. Flutter SDK 下载
在Mac电脑上选择了去Flutter github项目下去下载压缩包,下载完后运行 flutter doctor出现了如下报错:
Error: The Flutter directory is not a clone of the GitHub project.
The flutter tool requires Git in order to operate properly;
to set up Flutter, run the following command:
git clone -b beta https://github.com/flutter/flutter.git
通过git clone拉flutter的代码解决上述问题。 参考:Mac安装配置Flutter与踩坑
2. ListView 使用
ListView 可以说是我接触Flutter开发使用的第一个组件,使用中经常会遇到非常可怕的事情,整个页面一片空白。然后查看日志就会看到下面的报错,我把这一类报错统一叫做 hasSize报错:
════════ Exception caught by rendering library ═════════════════════════════════
'package:flutter/src/rendering/sliver_multi_box_adaptor.dart': Failed assertion: line 549 pos 12: 'child.hasSize': is not true.
The relevant error-causing widget was
ListView
lib/…/widgets/headList.dart:220
════════════════════════════════════════════════════════════════════════════════
刚开始开发不知道 debug的时候报错都可以通过调试控制台查看错误堆栈,查看堆栈也不知道多网上翻翻,通常划拉半个屏幕看到满屏的这个报错和模拟器上的一片空白就莫名紧张。后面发现划拉到最上面其实才能看到最终引起报错的地方在哪里。但是看到这个报错次数多了就总结了以下两种情况:
- 组件内非空的参数没有传,导致ListView 失去了Size;
- 避免 ListView 套在 Colume 中使用,如果这样使用了就会经常遇到hasSize报错,具体原因还没搞清楚;
3. 图片选择
// 从相册选择照片上传
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
// 从相机拍照上传
var image = await ImagePicker.pickImage(source: ImageSource.camera);
// 把图片裁剪成圆形
_image = ClipOval(
child: Image.file(
_imgFile,
width: 36,
height: 36,
fit: BoxFit.cover,
),
);
}
4. iOS 风格的 picker 组件使用
使用 showCupertinoModalPopup 展示一个弹框,当点击弹框遮罩层的时候收起弹框。 使用 CupertinoPicker 展示iOS风格的选择组件,该组件接受 children 参数,给 children 传递展示的选项内容Widgets;通过 scrollController 可以控制滑轮的位置设置组件展示的初始值,设置当前选中的item等。PickerHead 展示在滑轮顶部,是一个自定义的组件。
// demo: 弹框选择性别
showCupertinoModalPopup(
context: context,
builder: (context) {
return Container(
height: _theme.pickerHeight,
child: Column(children: <Widget>[
PickerHead(name: '选择性别',onOk: () { submit(); },), // 公共头部封装
Container(height: _theme.containerHeight, child: CupertinoPicker(
backgroundColor: Colors.white, //选择器背景色
itemExtent: _theme.itemHeight, //item的高度
scrollController: FixedExtentScrollController( // initValue
initialItem: int.parse(widget.data.gender ?? '1') - 1,
),
onSelectedItemChanged: (i) { //选中item的位置索引
index = i + 1;
},
children: <Widget>[ //所有的选择项
Text('男'),
Text('女'),
],
)),
]),
);
},
);
5. DatePicker 与本地化
iOS 风格日期选择器的实现同样是先使用 showCupertinoModalPopup 展示一个弹框,在 showCupertinoModalPopup 的 builder 方法中返回一个 CupertinoDatePicker,这个组件的使用很简单就不介绍了 :
// demo: 日期选择器
showCupertinoModalPopup(
context: context,
builder: (context) {
return Container(
height: _theme.pickerHeight,
child: Column(children: <Widget>[
PickerHead(name: '选择生日', onOk: () { submut(date); },), // 公共头部封装
Container(height: _theme.containerHeight, child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.date,
initialDateTime: date,
minimumYear: 1950,
maximumDate: DateTime.now(),
onDateTimeChanged: (DateTime val) { date = val; },
)),
],
));
}
);
在实现完上面的生日选择组件后发现一个很大的问题:picker组件的月份显示的是英文,而且是月份在前的格式。经过一番折腾(Google搜索)找到了答案:在创建 MaterialApp 的时候设置 localizationsDelegates 和 supportedLocales 参数。
MaterialApp(
localizationsDelegates: [
// ... app-specific localization delegate[s] here
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
DefaultCupertinoLocalizations.delegate,
ChinaCupertinoLocalizations.delegate, // 自定义Cupertino本地化类
],
supportedLocales: [
// const Locale('en', 'US'), // English
const Locale('zh', 'CH'), // China
],
debugShowCheckedModeBanner: false,
title: '多多商服',
home: IndexPage(),
routes: routes,
);
ChinaCupertinoLocalizations.dart 如下,并没有完全本地化,只挑自己用的到的就可以了。
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
class _CupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
const _CupertinoLocalizationsDelegate();
@override
bool isSupported(Locale locale) => locale.languageCode == 'zh';
@override
Future<CupertinoLocalizations> load(Locale locale) => ChinaCupertinoLocalizations.load(locale);
@override
bool shouldReload(_CupertinoLocalizationsDelegate old) => false;
@override
String toString() => 'DefaultCupertinoLocalizations.delegate(zh_CH)';
}
/// US English strings for the cupertino widgets.
class ChinaCupertinoLocalizations implements CupertinoLocalizations {
/// Constructs an object that defines the cupertino widgets' localized strings
/// for US English (only).
///
/// [LocalizationsDelegate] implementations typically call the static [load]
/// function, rather than constructing this class directly.
const ChinaCupertinoLocalizations();
static const List<String> _shortWeekdays = <String>[
'星期一',
'星期二',
'星期三',
'星期四',
'星期五',
'星期六',
'星期日',
];
static const List<String> _shortMonths = <String>[
'1月',
'2月',
'3月',
'4月',
'5月',
'6月',
'7月',
'8月',
'9月',
'10月',
'11月',
'12月',
];
static const List<String> _months = <String>[
'1月',
'2月',
'3月',
'4月',
'5月',
'6月',
'7月',
'8月',
'9月',
'10月',
'11月',
'12月',
];
@override
String datePickerYear(int yearIndex) => yearIndex.toString()+ '年';
@override
String datePickerMonth(int monthIndex) => _months[monthIndex - 1];
@override
String datePickerDayOfMonth(int dayIndex) => dayIndex.toString() + '日';
@override
String datePickerHour(int hour) => hour.toString();
@override
String datePickerHourSemanticsLabel(int hour) => hour.toString() + " Uhr";
@override
String datePickerMinute(int minute) => minute.toString().padLeft(2, '0');
@override
String datePickerMinuteSemanticsLabel(int minute) {
if (minute == 1)
return '1 Minute';
return minute.toString() + ' Minuten';
}
@override
String datePickerMediumDate(DateTime date) {
return '${_shortMonths[date.month - DateTime.january]} '
'${_shortWeekdays[date.weekday - DateTime.monday]} '
'${date.day.toString().padRight(2)}';
}
@override
DatePickerDateOrder get datePickerDateOrder => DatePickerDateOrder.ymd;
@override
DatePickerDateTimeOrder get datePickerDateTimeOrder => DatePickerDateTimeOrder.dayPeriod_time_date;
@override
String get anteMeridiemAbbreviation => '上午';
@override
String get postMeridiemAbbreviation => '下午';
@override
String get alertDialogLabel => 'Info';
@override
String timerPickerHour(int hour) => hour.toString();
@override
String timerPickerMinute(int minute) => minute.toString();
@override
String timerPickerSecond(int second) => second.toString();
@override
String timerPickerHourLabel(int hour) => hour == 1 ? 'Stunde' : 'Stunden';
@override
String timerPickerMinuteLabel(int minute) => 'Min';
@override
String timerPickerSecondLabel(int second) => 'Sek';
@override
String get cutButtonLabel => '剪切';
@override
String get copyButtonLabel => '复制';
@override
String get pasteButtonLabel => '粘贴';
@override
String get selectAllButtonLabel => '全选';
/// Creates an object that provides US English resource values for the
/// cupertino library widgets.
///
/// The [locale] parameter is ignored.
///
/// This method is typically used to create a [LocalizationsDelegate].
static Future<CupertinoLocalizations> load(Locale locale) {
return SynchronousFuture<CupertinoLocalizations>(const ChinaCupertinoLocalizations());
}
/// A [LocalizationsDelegate] that uses [DefaultCupertinoLocalizations.load]
/// to create an instance of this class.
static const LocalizationsDelegate<CupertinoLocalizations> delegate = _CupertinoLocalizationsDelegate();
@override
String get todayLabel => '今天';
}
6. inputDialog 实践
在修改用户名、个性签名、添加评论等相对短内容的场景下需要点击一个按钮弹出输入框,点击键盘和输入框其余的部分键盘和输入框同时收起的效果。综合比较了网友的实现方案,最终参考了Flutter 模态框dialog方式弹出键盘不遮挡输入框实现。这个方案是自己实现一个模态框,Navigator.push 进一个继承 PopupRoute 类的实例。使用 Expanded 撑起一个需要接收点击收起事件的组件。 具体实现如下:
// PopRoute.dart
import 'package:flutter/material.dart';
class PopRoute extends PopupRoute{
final Duration _duration = Duration(milliseconds: 300);
Widget child;
PopRoute({@required this.child});
@override
Color get barrierColor => null;
@override
bool get barrierDismissible => true;
@override
String get barrierLabel => null;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return child;
}
@override
Duration get transitionDuration => _duration;
}
当需要调起输入弹框时执行以下代码:
Navigator.push(context, PopRoute(child: Scaffold(
backgroundColor: Colors.transparent,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max, //有效,外层Colum高度为整个屏幕
children: <Widget>[
Expanded( // 撑开键盘弹起后的空间
child: new GestureDetector(
child: new Container(
color: Colors.black26,
),
onTap: () {
Navigator.pop(context);
},
)
),
Container( // 输入框个性化code部分
color: Colors.white,
padding: EdgeInsets.fromLTRB(16, 10, 16, 10),
constraints: BoxConstraints(
maxHeight: 300,
minHeight: 78,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(flex: 1, child: CupertinoTextField(
placeholder: "输入个性签名...",
autofocus: true,
minLines: 1,
maxLines: 3,
textInputAction: TextInputAction.newline,
onChanged: (String val) {
str = val;
},
onSubmitted: (String val) {
if (val != '' && val != null) {
_updateProfile({ 'selfSign': val });
}
Navigator.pop(context);
},
)),
Container(width: 50, margin: EdgeInsets.fromLTRB(10, 0, 0, 0), child: FlatButton(
textColor: Colors.black45,
padding: EdgeInsets.all(0),
child: Container(
height: 40,
alignment: Alignment.center,
child: Text('确定'),
),
onPressed: () {
if (str != '' && str != null) {
_updateProfile({ 'wechatNumber': str });
}
Navigator.pop(context);
},
)),
]
)
)
],
),
)));
7. 悬浮按钮
悬浮按钮使用的地方很多,一般在屏幕右下角的位置浮动展示。目前我只找到了通过 Scaffold 的floatingActionButton 这一种方式实现悬浮按钮。按钮默认在右下的位置,可以通过修改 floatingActionButtonLocation 使按钮位置变化,但是FloatingActionButtonLocation这个类提供的位置非常局限,可以通过自己实现一个CustomFloatingActionButtonLocation 来自定义悬浮按钮位置。懒得自己写,百度了一个:参考Flutter中FloatingActionButton自定义位置的简单实现,这个实现非常好,不贴代码了,大家可以自己去看。
后记:
Flutter 发展势头特别迅猛,作为一个前端工程师经过一周的开发体验,总体感觉是体验超出H5很多很多的。毕竟在H5上开发经常有各种奇葩问题,关于键盘输入 focus、blur 的问题一堆、图片视频选择的问题又一堆。使用 Flutter 可以大胆的追求极致的体验是最令人兴奋的事情。 文中很多代码粘贴的部分,还有一些可能不是最优的解决方案,请见谅,共勉~!