第2节:Flutter控件Widget的自定义与封装

[TOC]

前言

如果你不是没有任何开发经验,那么你一定知道任何app里都有可能有重用性比较高的控件。所以对于那么重用性比较高的,或者需要你自定义的控件的,我们需要将它们给封装起来,以便下次或者其他app中继续使用。这也正式本节想要说的内容Flutter中如何封装Widget。

下面我从自己实现一个满意的封装,分别介绍你可能用到的三种封装方式

  • 1、函数式封装
  • 2、以继承 StatefulWidget 的方式封装
  • 3、继承父类式封装(推荐)

下面我们以登录页的文本框的自定义来谈封装。

用户名登录的UI图

一、函数式封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// 蓝色背景按钮(常用于:登录按钮)
/// 方法1:以函数的方法实现
FlatButton blueButton(String text, bool enable, VoidCallback enableOnPressed) {
return FlatButton(
child: Text(text),
splashColor: Colors.transparent,
color: Color(0xff01adfe),
textColor: Colors.white,
highlightColor: Color(0xff1393d7),
disabledColor: Color(0xffd3d3d5),
disabledTextColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0)
),
//onPressed: enable ? enableOnPressed : null,
onPressed: enable ? () {
enableOnPressed();
} : null,
);
}

乍看没什么问题,好像很简洁。但当你也用这种方式来实现文本框的时候,其代码如下:

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
34
35
36
37
38
39
40
41
/// 文本框(常用于:登录用户名、密码文本框)
/// 方法1:以函数的方法实现
TextField loginTextField(String placeholder, String prefixIconImageName, ValueChanged<String> onSubmitted) {
return TextField(
//autofocus: shouldAutofocusUserNameTextField,
style: TextStyle(color: Colors.black, fontSize: 17.0),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
//labelText: "用户名",
hintText: placeholder,
//prefixIcon: Icon(Icons.person),
prefixIcon: new Image.asset(
prefixIconImageName,
width: 14.0,
height: 15.0,
),
enabledBorder: loginTextFieldDecorationBorder(),
focusedBorder: loginTextFieldDecorationBorder(),
),
// keyboardType: TextInputType.text,
// controller: _usernameController,
// textInputAction: TextInputAction.next,
// focusNode: usernameFocusNode,
// onSubmitted: (text) {
// print("current userName:" + text);
// if (null == currentFocusNode) {
// currentFocusNode = FocusScope.of(context);
// }
// currentFocusNode.requestFocus(passwordFocusNode);
// }
onSubmitted: onSubmitted,
);
}

// 文本框border
InputBorder loginTextFieldDecorationBorder() {
return new OutlineInputBorder(
borderSide: new BorderSide(color: Color(0xffd2d2d2), width: 0.6),
borderRadius: new BorderRadius.circular(6.0)
);
}

可见这种函数的方式,没办法处理过多属性的自定义。因为它并不像我们iOS中的UIView,可以对得到的控件在后续再定制。所以,在Flutter中这种函数式的封装不适合,因为它无法满足使用。

附:以下是iOS中的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (CJTextField *)userNameTextField {
if (_userNameTextField == nil) {
UIImage *normalImage = [UIImage imageNamed:@"login_username_gray"];
UIImage *selectedImage = [UIImage imageNamed:@"login_username_blue"];
_userNameTextField = [CJDemoTextFieldFactory textFieldWithNormalImage:normalImage selectedImage:selectedImage];
_userNameTextField.placeholder = NSLocalizedString(@"用户名", nil);
_userNameTextField.returnKeyType = UIReturnKeyNext;
_userNameTextField.clearButtonMode = UITextFieldViewModeWhileEditing;
_userNameTextField.delegate = self;
}
return _userNameTextField;
}

- (UIButton *)loginButton {
if (_loginButton == nil) {
_loginButton = [CJDemoButtonFactory blueButton];
[_loginButton setTitle:NSLocalizedString(@"登录", nil) forState:UIControlStateNormal];
_loginButton.enabled = NO;
[_loginButton addTarget:self action:@selector(loginButtonAction) forControlEvents:UIControlEventTouchUpInside];
}
return _loginButton;
}

二、以继承 StatefulWidget 的方式封装

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// 文本框(常用于:登录用户名、密码文本框)
/// 方法2:以继承 StatefulWidget 的方式实现
class LoginTextField extends StatefulWidget {
final String placeholder;
final String prefixIconImageName;
final bool autofocus;
final TextEditingController controller;
final TextInputAction textInputAction;
final FocusNode focusNode;
final ValueChanged<String> onSubmitted;
final TextInputType keyboardType;

LoginTextField({
Key key,
this.placeholder,
this.prefixIconImageName,
this.autofocus,
this.keyboardType,
this.controller,
this.textInputAction,
this.focusNode,
this.onSubmitted

}) : super(key: key);

@override
_LoginTextFieldState createState() => _LoginTextFieldState();
}

class _LoginTextFieldState extends State<LoginTextField> {
@override
Widget build(BuildContext context) {
return Container(
child: TextField(
autofocus: widget.autofocus,
style: TextStyle(color: Colors.black, fontSize: 17.0),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
//labelText: "用户名",
hintText: widget.placeholder,
//prefixIcon: Icon(Icons.person),
prefixIcon: new Image.asset(
widget.prefixIconImageName,
width: 14.0,
height: 15.0,
),
enabledBorder: loginTextFieldDecorationBorder(),
focusedBorder: loginTextFieldDecorationBorder(),
),
keyboardType: widget.keyboardType,
controller: widget.controller,
textInputAction: widget.textInputAction,
focusNode: widget.focusNode,
onSubmitted: widget.onSubmitted
),
);
}
}

// 文本框border
InputBorder loginTextFieldDecorationBorder() {
return new OutlineInputBorder(
borderSide: new BorderSide(color: Color(0xffd2d2d2), width: 0.6),
borderRadius: new BorderRadius.circular(6.0)
);
}

使用的时候:

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
TextField userNameTextField() {
return TextField(
autofocus: shouldAutofocusUserNameTextField,
style: TextStyle(color: Colors.black, fontSize: 17.0),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
//labelText: "用户名",
hintText: "用户名",
//prefixIcon: Icon(Icons.person),
prefixIcon: new Image.asset(
userNameValid ? 'lib/Resources/login/login_username_blue.png' : 'lib/Resources/login/login_username_gray.png',
width: 14.0,
height: 15.0,
),
enabledBorder: loginTextFieldDecorationBorder(),
focusedBorder: loginTextFieldDecorationBorder(),
),
keyboardType: TextInputType.text,
controller: _usernameController,
textInputAction: TextInputAction.next,
focusNode: usernameFocusNode, //usernameFocusNode
onSubmitted: (text) {
print("current userName:" + text);
if (null == currentFocusNode) {
currentFocusNode = FocusScope.of(context);
}
currentFocusNode.requestFocus(passwordFocusNode);
}
);
}

虽然使用上看似没什么问题,但是整个TextField的继承代码难道你不觉得有更简洁的写法吗?

所以下面将讲解直接继承TextFiled的方法。

三、继承父类式封装(推荐)

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/// 文本框(常用于:登录用户名、密码文本框)
/// 方法3:以继承 TextField 的方式实现
class LoginTextField extends TextField {
LoginTextField({
Key key,
String text,

String placeholder,

/// prefix icon
bool prefixIconSelected,
String prefixIconNormalImageName,
String prefixIconSelectedImageName,

bool autofocus = false,
bool obscureText = false,
TextInputType keyboardType,
TextEditingController controller,
bool showClear = false,
TextInputAction textInputAction,
FocusNode focusNode,
ValueChanged<String> onSubmitted,
}) : super(
key: key,
autofocus: autofocus,
obscureText: obscureText,
style: TextStyle(color: Colors.black, fontSize: 17.0),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
//labelText: "用户名",
hintText: placeholder,
//prefixIcon: Icon(Icons.person),
prefixIcon: new Image.asset(
!prefixIconSelected ? prefixIconNormalImageName :prefixIconSelectedImageName,
width: 14.0,
height: 15.0,
),
suffixIcon: !showClear ? null : clearButtonWithOnPressed(controller.clear),
enabledBorder: loginTextFieldDecorationBorder(),
focusedBorder: loginTextFieldDecorationBorder(),
),
keyboardType: keyboardType,
controller: controller,
textInputAction: textInputAction,
focusNode: focusNode,
onSubmitted: onSubmitted
);
}

/// selected Image
class SelectedImage extends Image {
SelectedImage({
Key key,
bool selected,
String normalImageName,
String selectedImageName,
}) :super (
key: key,
image: AssetImage(!selected ? normalImageName :selectedImageName)
);
}


/// 文本框border
InputBorder loginTextFieldDecorationBorder() {
return new OutlineInputBorder(
borderSide: new BorderSide(color: Color(0xffd2d2d2), width: 0.6),
borderRadius: new BorderRadius.circular(6.0)
);
}

使用时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 用户名文本框
LoginTextField userNameTextField() {
return LoginTextField(
placeholder: "用户名",
prefixIconSelected: userNameValid,
prefixIconNormalImageName: 'assets/images/login/login_username_gray.png',
prefixIconSelectedImageName: 'assets/images/login/login_username_blue.png',
autofocus: shouldAutofocusUserNameTextField,
keyboardType: TextInputType.text,
controller: _usernameController,
textInputAction: TextInputAction.next,
focusNode: usernameFocusNode,
onSubmitted: (text) {
print("current userName:" + text);
if (null == currentFocusNode) {
currentFocusNode = FocusScope.of(context);
}
currentFocusNode.requestFocus(passwordFocusNode);
});
}

可见使用继承父类式封装这种方式,不管在封装时候,还是在使用时候,写的代码都是最简洁的。而且后期如果要直接使用系统样式,也只需要改回类名,其他结构和属性都不用动即可

四、强调自定义类的设计规范

在前面,我们已经知道使用继承父类式封装这种方式,不管在封装时候,还是在使用时候,写的代码都是最简洁的。而且后期如果要直接使用系统样式,也只需要改回类名,其他结构和属性都不用动即可

所以,即使是你所定义的类只有一个入参,也一定要遵守使用继承父类式封装的设计规范。

以下以按钮中 textStyle 的传值为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'package:flutter_baseui_kit/flutter_baseui_kit.dart';

ThemeBGButton(
//width: 300, // 不设置会根据内容自适应
//height: 80, // 不设置会根据内容自适应
bgColorType: ThemeBGType.pink,
title: '红底白字的按钮',
//titleStyle: ButtonThemeUtil.PingFang_FontSize_Bold(18.0), // bad
titleStyle: ButtonBoldTextStyle(fontSize: 18.0), // good
cornerRadius: 20,
//enable: true, // 不设置,默认true
onPressed: () {},
),

附:bad 和 good 两种实现方式的代码分别如下:

bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'package:flutter/material.dart';

// 按钮上的文本样式(按钮上的文字颜色,已通过其他属性设置;不需要 TextStyle 中设置;其他类的文本需要在 TextStyle 设置文本颜色,所以此类最多只提供给按钮使用)
class ButtonThemeUtil {
// 类命名注意:系统有 ButtonTheme 类,别取重名,否则外部取不到

static TextStyle PingFang_FontSize_Medium(double fontSize) {
return TextStyle(
fontFamily: 'PingFang SC',
fontSize: fontSize,
fontWeight: FontWeight.w500,
);
}
}

good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'package:flutter/material.dart';

// medium 的文本样式
class ButtonMediumTextStyle extends TextStyle {
final double fontSize;
// final Color color;

ButtonMediumTextStyle({
@required this.fontSize,
// this.color,
}) : assert(fontSize != null),
// assert(color != null),
super(
fontFamily: 'PingFang SC',
fontSize: fontSize,
fontWeight: FontWeight.w500,
// color: color,
);
}

所以,综上在Flutter中对于一个Widget的封装,我们采用直接继承其父类的方式来处理,且其具体的写法如上。

End