近期在学习flutter,顺手写点东西记录一下学习心得以及踩过的坑。本文只讲解了简单的页面之间的跳转,路由传参等知识点如需了解,自行网上查找。
1.路由的原理
大多数应用程序具有多个页面或视图,并且希望将用户从页面平滑过渡到另一个页面。Flutter的路由和导航功能可帮助我们管理应用中屏幕之间的命名和过渡,
管理多个页面时有两个核心概念和类:Route和 Navigator。 一个route是一个屏幕或页面的抽象,Navigator是管理route的Widget。Navigator可以通过route入栈和出栈来实现页面之间的跳转。
2.Android、IOS、Flutter页面跳转比较
Android实现页面之间的跳转是通过context.startActivity,iOS中使用的是viewController。而在Flutter实现页面之间的跳转与数据传递使用的是Navigator.push和Navigator.pop以及Router,最终效果图如下。
3.新建页面
本文主要讲解页面直接的跳转,简单写两个页面就可以了。所以这里我就只贴登录页面和主页面的代码。
- 登录页面:login_page
- 主页面:welcome_page
4.登录页面
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginPage> {
TextEditingController _pwdEditController;
TextEditingController _userNameEditController;
final FocusNode _userNameFocusNode = FocusNode();
final FocusNode _pwdFocusNode = FocusNode();
@override
void initState() {
super.initState();
_pwdEditController = TextEditingController();
_userNameEditController = TextEditingController();
_pwdEditController.addListener(() => setState(() => {}));
_userNameEditController.addListener(() => setState(() => {}));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.blue[400],
elevation: 0,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
buildTopWidget(context),
SizedBox(height: 80),
buildEditWidget(context),
buildLoginButton()
],
),
),
);
}
/// 头部
Widget buildTopWidget(BuildContext context) {
double height = 200.0;
double width = MediaQuery.of(context).size.width;
return Container(
width: width,
height: height,
color: Colors.blue[400],
child: Stack(
overflow: Overflow.visible, // 超出部分显示
children: <Widget>[
Positioned(
left: (width - 90) / 2.0,
top: height - 45,
child: Container(
width: 90.0,
height: 90.0,
decoration: BoxDecoration(
///阴影
boxShadow: [
BoxShadow(color: Theme.of(context).cardColor, blurRadius: 4.0)
],
///形状
shape: BoxShape.circle,
///图片
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(
'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1569298229946&di=ea4ffb2b140ef40035772bbcee7bbdd5&imgtype=0&src=http%3A%2F%2Fcimg2.163.com%2Fcatchimg%2F20090909%2F8112139_3.jpg'),
),
),
),
)
],
),
);
}
/// 输入框
Widget buildEditWidget(BuildContext context) {
return Container(
margin: EdgeInsets.only(left: 15, right: 15),
child: Column(
children: <Widget>[
buildLoginNameTextField(),
SizedBox(height: 20.0),
buildPwdTextField(),
],
),
);
}
/// 用户名
Widget buildLoginNameTextField() {
return Container(
height: 40,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
child: Stack(
children: <Widget>[
Positioned(
left: 16,
top: 11,
width: 18,
height: 18,
child: Image.asset('images/login_user.png'),
),
Positioned(
left: 45,
top: 10,
bottom: 10,
width: 1,
child: Container(
color: Colors.black,
),
),
Positioned(
left: 55,
right: 10,
top: 10,
height: 30,
child: TextField(
controller: _userNameEditController,
focusNode: _userNameFocusNode,
decoration: InputDecoration(
hintText: "请输入用户名",
border: InputBorder.none,
),
style: TextStyle(fontSize: 14),
),
)
],
),
);
}
/// 密码
Widget buildPwdTextField() {
return Container(
height: 40,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
child: Stack(
children: <Widget>[
Positioned(
left: 16,
top: 11,
width: 18,
height: 18,
child: Image.asset('images/login_pwd.png'),
),
Positioned(
left: 45,
top: 10,
bottom: 10,
width: 1,
child: Container(
color: Colors.black,
),
),
Positioned(
left: 55,
right: 10,
top: 10,
height: 30,
child: TextField(
controller: _pwdEditController,
focusNode: _pwdFocusNode,
decoration: InputDecoration(
hintText: "请输入密码",
border: InputBorder.none,
),
style: TextStyle(fontSize: 14),
obscureText: true,
/// 设置密码
),
)
],
));
}
/// 登录按钮
Widget buildLoginButton() {
return Container(
margin: EdgeInsets.only(top: 40, left: 10, right: 10),
padding: EdgeInsets.all(0),
width: MediaQuery.of(context).size.width - 20,
height: 40,
child: RaisedButton(
onPressed: () {
if (checkInput()) {
// Fluttertoast.showToast(
// msg: "登录成功",
// gravity: ToastGravity.CENTER,
// timeInSecForIos: 2,
// textColor: Colors.white,
// fontSize: 14.0
// );
// Navigator.push(
// context,
// new MaterialPageRoute(builder: (context) => new WelcomePage()),
// );
Navigator.pushNamed(context, "/welcome");
}
},
child: Text("登录"),
color: Colors.blue[400],
textColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
),
);
}
bool checkInput() {
if (_userNameEditController.text.length == 0) {
Fluttertoast.showToast(
msg: "请输入用户名",
gravity: ToastGravity.CENTER,
timeInSecForIos: 2,
textColor: Colors.white,
fontSize: 14.0);
return false;
} else if (_pwdEditController.text.length == 0) {
Fluttertoast.showToast(
msg: "请输入密码",
gravity: ToastGravity.CENTER,
timeInSecForIos: 2,
textColor: Colors.white,
fontSize: 14.0);
return false;
}
return true;
}
}
5.主页面
import 'package:flutter/material.dart';
class WelcomePage extends StatefulWidget {
@override
_WelcomePageState createState() {
return _WelcomePageState();
}
}
class _WelcomePageState extends State<WelcomePage> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '',
home: Scaffold(
appBar: AppBar(
title: Text('data input'),
),
body: Column(
children: <Widget>[
Image.asset('images/shiyanshi.jpg'),
titleSelection,
textSelection
],
),
floatingActionButton: FancyButton(
onPressed: () {
_saveDataFun();
},
), // This trailing comma tells the Dart formatter to use
// a style that looks nicer for build methods.
),
);
}
void _saveDataFun() {
// Fluttertoast.showToast(
// msg: "保存成功!",
// toastLength: Toast.LENGTH_SHORT,
// gravity: ToastGravity.CENTER,
// timeInSecForIos: 1);
// Navigator.push(
// context, new MaterialPageRoute(builder: (context) => new InputPage()));
Navigator.pushNamed(context, "/input");
}
}
Widget titleSelection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
/**
* 将Column放置在Expanded中,由于Expanded会默认占据所有当前Row的空闲可用空间,所以这个Column也会自然被拉伸的占据完所有当前Row可用的空闲空间。
*/
Expanded(
child: Column(
/**
* 将Column的crossAxisAlignment属性设置为CrossAxisAlignment.start以保证这个列中的元素(即children属性中的Widget)在水平方向上排列在当前Column的起始位置
*/
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
// 第一列上方文本
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Amount of submited data',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Text(
'for viewing only',
style: TextStyle(),
),
])),
Icon(
Icons.star,
color: Colors.red[500],
),
Text(
'41',
),
],
));
Widget textSelection = Container(
padding: const EdgeInsets.all(32),
child: Text(
'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
'Alps. Situated 1,578 meters above sea level, it is one of the '
'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
'half-hour walk through pastures and pine forest, leads you to the '
'lake, which warms to 20 degrees Celsius in the summer. Activities '
'enjoyed here include rowing, and riding the summer toboggan run.',
// 保证文字可以在单词分界的地方换行而不是在单词中间换行。
softWrap: true),
);
class FancyButton extends StatelessWidget {
final GestureTapCallback onPressed;
FancyButton({@required this.onPressed});
@override
Widget build(BuildContext context) {
return RawMaterialButton(
fillColor: Colors.blue,
splashColor: Colors.orange,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 20.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const <Widget>[
RotatedBox(
//将icon进行了旋转
quarterTurns: 1,
child: Icon(
Icons.save,
color: Colors.white,
),
),
SizedBox(
width: 8.0,
),
PulseAnimator(
//动画效果
child: Text(
'Save',
style: TextStyle(color: Colors.white),
),
),
],
),
),
onPressed: onPressed, //点击事件
shape: const StadiumBorder(), //添加圆角
);
}
}
class PulseAnimator extends StatefulWidget {
final Widget child;
const PulseAnimator({Key key, this.child}) : super(key: key);
@override
_PulseAnimatorState createState() => _PulseAnimatorState();
}
class _PulseAnimatorState extends State<PulseAnimator>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1200))
..repeat();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: Tween(begin: 0.5, end: 1.0).animate(_controller),
child: widget.child,
);
}
}
6.使用Navigator.push跳转页面
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new WelcomePage()),
);
Navigator.push(BuildContext context, Route<T> route)可以将当前页面转换成Router,压入由Navigator管理的路由堆栈(the stack of routes)中。
用此种方式就可以实现页面之间的跳转,但是,我们会发现一个问题,单页面之间的跳转使用这种方式可能还好。如果一个页面包含多个跳转点,那么我们首先需要把相应得dart文件import,然后再重复引用上述代码,这样过于冗余。所以我们需要对路由进行统一管理。
7.基于已命名路由调整
除了上述跳转方式以外,Flutetr还支持将路由与页面对应起来,进行已命名的路由跳转。
7.1更改main.dart
路由名称通常使用路径结构:“/a/b/c”,主页默认为 “/”。
import 'package:flutter/material.dart';
import 'package:idata/home_page/welcome_page.dart';
import './home_page/login_page.dart';
import './home_page/input_page.dart';
void main() {
runApp(NamedRouter.inintApp());
}
/** 直接调用NamedRouter.inintApp()返回MaterialApp,代码更加简洁、清晰 */
// class MyApp extends StatelessWidget {
// // This widget is the root of your application.
// @override
// Widget build(BuildContext context) {
// return NamedRouter.inintApp();
// }
// }
class NamedRouter {
static Map<String, WidgetBuilder> routes;
// 初始化app
static Widget inintApp() {
return MaterialApp(
title: 'idata',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 初始路由
initialRoute: '/',
routes: NamedRouter.initRoutes(),
);
}
// 初始化路由
static initRoutes() {
routes = {
'/': (context) => LoginPage(),
'/welcome': (context) => WelcomePage(),
'/input': (context) => InputPage(),
};
return routes;
}
}
具体应用如下
/// 登录按钮
Widget buildLoginButton() {
return Container(
margin: EdgeInsets.only(top: 40, left: 10, right: 10),
padding: EdgeInsets.all(0),
width: MediaQuery.of(context).size.width - 20,
height: 40,
child: RaisedButton(
onPressed: () {
if (checkInput()) {
// Fluttertoast.showToast(
// msg: "登录成功",
// gravity: ToastGravity.CENTER,
// timeInSecForIos: 2,
// textColor: Colors.white,
// fontSize: 14.0
// );
// Navigator.push(
// context,
// new MaterialPageRoute(builder: (context) => new WelcomePage()),
// );
Navigator.pushNamed(context, "/welcome");
}
},
child: Text("登录"),
color: Colors.blue[400],
textColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
),
);
}
这样所有路由均在main.dart中定义好,其他地方只需在Navigator.pushNamed引用定义好的name便可以了。