0%

Flutter 实践小结

好久没更新了,最近发生了太多的事儿,疫情还没有过去,目前还在武汉,相比较2个月前现在武汉的情况好太多了,虽然我居住小区还是封锁中,进出没那么自由,但人们心逐渐的平静下来,不再恐慌和担忧。我也静下心来写点东西,最近一个月写了一些Flutter相关的项目,遇到的问题不少,索性来个小总结吧,那就废话不多说,直接上干货。

ListView 撑开剩余空间

很多情况下,我们需要ListView撑开剩余空间,这个时候在外面套一个Expanded就可以做到了。如下面这个张图里显示的:

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// ...
topicTitle(),
Expanded(
child: tListView.separated(
shrinkWrap: true,
itemCount: 10,//lenght,
itemBuilder: (BuildContext context, int position) {
// ....
},
separatorBuilder: (BuildContext context, int position) {
return Padding(padding: EdgeInsets.only(top: 12),);
},
),
),
commitBtn(),
],
),
);

底部弹框的使用

showModalBottomSheet 底部弹框的使用以及给底部弹框切圆角,可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
showModalBottomSheet(
context: context,
// 点击背景蒙版是否需要关闭弹框。
isDismissible: false,
// 是否可以拖拽弹框。
enableDrag: false,
// 给底部弹框左上方和右上方切圆角。
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24)),
),
builder: (BuildContext context) {
// 这里是自定义的Widget,用来显示弹框里面的内容。
return RadioPicker(
start: () {},
stop: () {},
cancel: () {},
title: recordDuration(),
data: vd,
dataArr: [],
);
});

再来看看效果:

点击背景关闭键盘

一般用于页面有文本输入框要输入内容时,键盘弹起挡住页面其它内容,加上一下这一句就OK了。

1
2
3
4
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => FocusScope.of(context).requestFocus(FocusNode()),
child: Container());

输入框的光标问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TextField(
maxLines: 3,
scrollPadding: EdgeInsets.fromLTRB(0, 0, 0, 0),
expands: false,
onChanged: (text) {
_inputText = text
},
controller: TextEditingController.fromValue(
TextEditingValue(
text: _inputText,
selection: TextSelection.fromPosition(
TextPosition(
affinity: TextAffinity.downstream,
offset: _inputText.length
)
)
)
), // 这里要更新光标的偏移位置
decoration: InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 0, 0, 0),
hintText: "请输入作业项",
hintStyle: TextStyle(fontSize: 15, color: Color(0xFF808080)),
border: InputBorder.none),
)

父组件动态更新子组件显示的内容

我这里是更新底部弹框的内容,使用的是 ValueNotifier,实现代码如下:

1
2
3
class ValueNotifierData extends ValueNotifier<String> {
ValueNotifierData(value) : super(value);
}

子组件中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class RadioPicker extends StatefulWidget {

ValueNotifierData data;

@override
_RadioPickerState createState() => _RadioPickerState();

RadioPicker({Key key, this.data}) : super(key: key);
}

class _RadioPickerState extends State<RadioPicker> {
String title;

@override
void initState() {
super.initState();
widget.data.addListener(_handleValueChanged);
}

void _handleValueChanged() {
setState(() {
title = widget.data.value;
});
}
}

父组件中动态去修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class _ListenTaskPageState extends State<ListenTaskPage> {
ValueNotifierData vd = ValueNotifierData('00:00:00');
}

class _ListenTaskPageState extends State<ListenTaskPage> {
@override
void initState() {
super.initState();
}

_start() async {
vd.value = "xxxxxx";
}

void recordMp3() {
showModalBottomSheet(
context: context,
isDismissible: false,
builder: (BuildContext context) {
return RadioPicker(
data: vd,
);
});
}
}

全局通知事件

全局通知事件会使用到ChangeNotifier。这里使用场景是当收到推送的消息后更新学生任务角标数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/***
* 用来更新学生数据,目前用在了班级详情更新学生的角标(任务的个数)
* 获取数据:Provider.of<NotificationProvider>(context).studentID
* 更新数据:Provider.of<NotificationProvider>(context).updateStudentData("学生ID", 1);
*/
class NotificationProvider with ChangeNotifier {
String studentID;
int dotCount;

/// 更新学生的角标(任务的个数)
void updateStudentData(String studentID, int dotCount) async {
this.studentID = studentID;
this.dotCount = dotCount;

// 通知监听者更新界面
notifyListeners();
}
}

当然别忘了在 main() 函数里面加一下, ChangeNotifierProvider 套上:

1
2
3
4
5
6
7
8
9
10
void main() async {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (_){
return NotificationProvider();
}),
],
child: MyApp(),
));
}

和原生进行通信

在flutter代码里打开 iOS 原生Apple Music 为例:

dart 代码实现:

1
2
3
4
5
6
7
8
9
10
11
if (Platform.isIOS) {
const platform = const MethodChannel('joewang');
var result;
try {
result = await platform.invokeMethod('openMedia');
print(result);
return Future.value(result);
} on PlatformException catch (e) {
return Future.error(e.toString());
}
}

iOS 原生代码(OC)实现:

.h 文件:

1
2
3
4
5
6
7
8
9
10
11
12
#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>

NS_ASSUME_NONNULL_BEGIN

@interface FlutterNativePlugin : NSObject<FlutterPlugin>

@end

NS_ASSUME_NONNULL_END

.m 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import "FlutterNativePlugin.h"
@interface FlutterNativePlugin() <UIImagePickerControllerDelegate, MPMediaPickerControllerDelegate>
@property (nonatomic) UIViewController *viewController;
@property (nonatomic) MPMediaPickerController *audioPickerController;
@end

@implementation FlutterNativePlugin

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"joewang" binaryMessenger:[registrar messenger]];
UIViewController *viewController = [UIApplication sharedApplication].delegate.window.rootViewController;
FlutterNativePlugin *instance = [[FlutterNativePlugin alloc]initWithViewController:viewController];
[registrar addMethodCallDelegate:instance channel:channel];
}

- (instancetype)initWithViewController:(UIViewController *)viewController {
self = [super init];
if(self) {
self.viewController = viewController;
}
return self;
}

- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([call.method isEqualToString:@"openMedia"]) {
self.audioPickerController = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
self.audioPickerController.delegate = self;
self.audioPickerController.showsCloudItems = NO;
self.audioPickerController.allowsPickingMultipleItems = NO;
self.audioPickerController.modalPresentationStyle = UIModalPresentationCurrentContext;
[self.viewController presentViewController:self.audioPickerController animated:YES completion:nil];
}
}

测试的时候记得iPhone要安装 Apple Music App哦,本人曾遇到过,还花了半天的时间找原因。

网络请求loading的显示问题。

通常我们在发出请求之前,显示loading,请求完成后再 dismiss 掉这个loading。类似于这样的代码:

1
2
3
4
5
6
7
8
9
Future uploadMp3() async {
Loading(context).show();
HttpUtil.post("url", "params", (value) {
Loading(context).dismiss();
// ...
}, (value) {
Loading(context).dismiss();
});
}

一般来讲,这样 showdismiss loading不会有什么问题,但是当网络请求在某一个异步回调函数中发出时,此时我们的loading调用 dismiss 好像不管用了。如:选择音频文件上传,在拿到音频文件完成后,我需要先异步来获取音频文件的时长,再将音频文件通过接口上传,那么此时会发现一直在loading。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
await FilePicker.getFilePath(type: FileType.audio).then((value) {
print("value = $value");
if (value == null) {
return;
}
_getDuration(value).then((duration) => {
print(duration)
}).whenComplete(() {
uploadMp3(value, context);
});
});
} catch (e) {
ZhituoxinToast.showToast(msg: e.toString());
}

解决办法:在 Loading 找到 showDialog<void>()。在调用 showDialog 时传入 GlobalKey ,通过 GlobalKey 去获取正确的 context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Loading {
_Body _dialog;
VoidCallback timeOutHandler;
int timeOut;
String msg;

static GlobalKey loadingGlobalKey = GlobalKey();
}

void show() {
showDialog<dynamic>(
context: _context,
barrierDismissible: false,
builder: (BuildContext context) {
return WillPopScope(
/// 这里使用 GlobalKey
key: loadingGlobalKey,
onWillPop: () {
return Future.value(_barrierDismissible);
},
child: Dialog(child: Container()),
);
},
);
}

网络请求里面调用的时候这样写:

1
2
3
4
5
6
7
8
9
10
11
12
Future uploadMp3() async {
Loading(context).show();
HttpUtil.post("url", "params", (value) {
Loading(context).dismiss();
if (Loading.loadingGlobalKey.currentContext != null) {
Navigator.of(Loading.loadingGlobalKey.currentContext)?.pop;
}
// ...
}, (value) {

});
}

Text设置ellipsis不起作用?

TextOverflow.ellipsis 不起作用?可以这么做:最外层用Row或者Flex包裹,然后再用Expanded包一层就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.asset(
'assets/images/add_student_header_icon.png',
width: 19,
height: 19
),
Padding( padding: EdgeInsets.only(right: 10),),
Expanded(
child: Text(
"听闻远方有你,动身跋涉千里,
我吹过你吹过的风,这算不算相拥,
我踏过你走过的路,这算不算相逢,
我还是喜欢你,认真且怂,从一而终。",
textAlign: TextAlign.left,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14, color: Color(0xFF808080))),
)
],
)

欢迎关注公众号:flutter_todo,有更多技术干货和学习资源教程分享。

qrcode_for_gh_a1ca9094adfb_430