Skip to content

跳转页面

git checkout chapter-9

简单的页面跳转

要完成页面之间的跳转,需要先构建一个新的页面,叫 "注册" 页面。
在 pages 目录下创建一个新的 dart 文件,命名为 register.dart,然后在这个页面中,搭建一个最简单的页面框架,并为这个页面添加最简单的页面内容:

lib/pages/register.dart

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

class RegisterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('注册页面'),
      ),
    );
  }
}

接下来,让 "登录" 页面跳转到这个页面。
为了做到这一点,我们需要修改 "登录" 页面中注册按钮的事件处理函数,这里我们写了一段最简单的页面跳转代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// login.dart
child: Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text('没有账号? '),
        InkWell(
        child: Text('立即注册'),
        onTap: () => Navigator.of(context).push(
            MaterialPageRoute(
                builder: (_) => RegisterPage())),
        ),
    ],
),

这段代码很简单,保存更改以后,单击 "立即注册" 按钮,就可以看到原有的 "登录" 页面消失了,取而代之的是刚刚构建的最简单的 "注册" 页面。
那么,这一小段代码是如何起作用的呢?

在 Flutter 中,如果对过渡效果没有特殊需求,就可以使用这个 MaterialPageRoute 来完成页面之间的跳转。
MaterialPageRoute 为我们封装了一些默认的跳转行为: Android 系统中的新页面会从页面下方出现,而 iOS 系统中的新页面会从页面右侧出现

如果想从 "注册" 页面返回 "登录" 页面,实现也非常简单,只要利用 Navigator 的 pop 方法就可以了,具体如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// register.dart
class RegisterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: Text('注册页面'),
          onTap: () => Navigator.of(context).pop(),
        ),
      ),
    );
  }
}

Navigator 在内部维护了一个名为 History 的栈,这个栈保存着我们用到的 Route 对象。
很自然地,除了 push 方法,Navigator 还为我们提供了很多方法来操作跳转到新页面的形式

前面这种直接向 Navigator 中传入一个路由的方式虽然简单,但在实际的应用开发过程中,如果页面的数量非常多,这种方式并不利于他人直观地了解我们的应用到底包含多少个页面。
因此,在实际的开发过程中,我们一般会使用一种叫做命名路由的方式来实现页面之间的跳转

所谓命名路由,就是在应用的入口处直接将所有的页面和一个个 URL 字符串分别对应起来。
我们只需要在 main.dart 文件中提前设置好所有的路由表即可,就像这样:

 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
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:todo/config/colors.dart';
import 'package:todo/pages/login.dart';
import 'package:todo/pages/register.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: PRIMARY_COLOR,
        indicatorColor: ACCENT_COLOR,
        accentColor: PRIMARY_COLOR,
      ),
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en'),
        const Locale('zh', 'CN'),
      ],
      home: LoginPage(),
      initialRoute: '/',
      routes: {
        '/register': (context) => RegisterPage(),
      },
    );
  }
}

这样一来,需要执行页面跳转的时候,直接向 Navigator 传入跳转后页面对应的 URL,就可以完成这次页面跳转了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// login.dart
child: Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text('没有账号? '),
        InkWell(
            child: Text('立即注册'),
            onTap: () => Navigator.of(context).pushNamed('/register'),
        ),
    ],
),

为了减少实际编程中手动输入 URL 的麻烦,我们还可以为这些 URL 定义一些常量,这样方便我们在使用时输入这些 URL:

1
2
3
// lib/pages/route_url.dart
const LOGIN_PAGE_URL = '/';
const REGISTER_PAGE_URL = '/register';
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// main.dart
class MyApp extends StatelessWidget {
...
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      home: LoginPage(),
      initialRoute: LOGIN_PAGE_URL,
      routes: {
        REGISTER_PAGE_URL: (context) => RegisterPage(),
      },
    );
  }
}
1
2
// main.dart
onTap: () => Navigator.of(context).pushNamed(REGISTER_PAGE_URL),

页面之间除了互相跳转,还会出现的一种情况是当页面 A 跳转到页面 B 的时候,需要给 B 传入一些参数,我们把这种情况称为路由传参。
我们可以为目前这个简单版本的 "注册" 页面增加一个新的功能: 展示前一个页面的类名称以及前一个页面的 URL

Flutter 为我们提供的参数传递 API 允许我们传递 Object 类型的对象作为参数。
因此为了顺利地从这个 API 中获取我们所传递的参数,同时也为了维护代码,我们首先需要为这个参数创建一个模型类:

1
2
3
4
5
6
7
// register.dart
class RegisterPageArgument {
  final String className;
  final String url;

  RegisterPageArgument(this.className, this.url);
}

接下来,我们需要在页面跳转的时候,将这个参数传递给 Navigator:

 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
// login.dart
class _LoginPageState extends State<LoginPage> {
  ...
  void _gotoRegister(BuildContext context) {
    Navigator.of(context).pushNamed(REGISTER_PAGE_URL,
        arguments: RegisterPageArgument('LoginPage', LOGIN_PAGE_URL));
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        ...
        Padding(
            padding: const EdgeInsets.only(
                left: 24, right: 24, bottom: 12),
            child: Container(
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('没有账号? '),
                        InkWell(
                        child: Text('立即注册'),
                        onTap: () => _gotoRegister(context),
                        ),
                    ],
                ),
            ),
        ),
        ...
    )
  }  
}

然后在 "注册" 页面,我们可以从 context 中利用 ModalRoute 获得传递过来的页面参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// register.dart
class RegisterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final RegisterPageArgument argument =
        ModalRoute.of(context)?.settings.arguments as RegisterPageArgument;
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: Text('注册页面, 从${argument.className}-${argument.url}跳转而来'),
          onTap: () => Navigator.of(context).pop(),
        ),
      ),
    );
  }
}

为页面跳转添加自定义的过渡效果

我们已经对从一个页面跳转到另一个页面的过程非常熟悉了,也基本了解了命名路由带给我们的各种便利,接下来我们把目光转向路由的另一个重要特性: 过渡效果。

我们会为待办事项应用增加一个自定义的过渡效果,让我们的应用从 "登录" 页面跳转到 "注册" 页面的时候,能够不以默认的 MaterialPageRoute 的效果展示,而是以渐变的形式完成页面过渡

实现渐变的页面过渡

创建自定义的页面过渡效果实际上非常简单,我们只需要改动少许的代码就可以完成这个功能:

 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
// main.dart
final Map<String, WidgetBuilder> routes = {
  LOGIN_PAGE_URL: (context) => LoginPage(),
  REGISTER_PAGE_URL: (context) => RegisterPage(),
};

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      home: routes[LOGIN_PAGE_URL]!(context),
      initialRoute: LOGIN_PAGE_URL,
      routes: {
        REGISTER_PAGE_URL: (context) => RegisterPage(),
      },
      onGenerateRoute: (RouteSettings settings) {
        if ([REGISTER_PAGE_URL].contains(settings.name)) {
          return PageRouteBuilder(
            settings: settings,
            pageBuilder: (context, _, __) => routes[settings.name]!(context),
            transitionsBuilder:
                (context, animation, secondaryAnimation, child) {
              return FadeTransition(
                opacity: animation,
                child: child,
              );
            },
          );
        }
        return MaterialPageRoute(
          builder: routes[settings.name] as Widget Function(BuildContext),
          settings: settings
        );
      },
    );
  }
}

代码虽然很短,但是瞬间出现了很多看起来非常陌生的元素。

了解页面过渡的原理

首先出现的是 PageRouteBuilder。
在 Flutter 中,Route 这个类负责构建页面 Widget 和实现页面过渡效果等。
自然地,当我们想要自定义页面的过渡效果时,最直接的想法就是继承 Route 类,创建一个它的子类。
但是这并不是一个好办法,因为需要我们自己重写的内容会非常多。
为了减少自定义过渡效果的工作量,Flutter 提供了 PageRouteBuilder 这个子类,让我们能够非常简单地创建一个自定义的页面过渡效果

在使用 PageRouteBuilder 的过程中,我们需要给其 pageBuildr 属性传入 RoutePageBuilder 类型的参数,这个类型的完整定义是这样的:

1
2
3
4
5
typedef RoutePageBuilder = Widget Function(
    BuildContext context, 
    Animation<double> animation,
    Animation<double> secondaryAnimation
);

虽然 RoutePageBuilder 类型除了传入 context,还传入了两个 Animation 参数,
但由于这里的代码中并不需要使用这两个参数,因此分别用 ___ 代替它们。
在 pageBuilder 属性传入的函数中,我们和之前一样,返回了 RegisterPage 的实例

接下来我们需要看一下 PageRouteBuilder 的另一个重要参数: transitionsBuilder。
对于 PageRouteBuilder 来说,pageBuilder 参数只负责创建它所要跳转的页面,transitionsBuilder 才是真正定义动画效果的出现位置的

先来看看在 transitionsBuilder 中,我们做了什么:

1
2
3
4
5
6
transitionsBuilder: (context, animation, secoondaryAnimation, child) {
    return FadeTransition(
        opacity: animation,
        child: child,
    );
}

在 transitionsBuilder 中,我们返回了一个 FadeTransition 的实例,这个实例接收传入的 animation 和 child 为参数。
child 其实就是 pageBuilder 参数的返回值。
看起来很简单,但为什么 FadeTransition 可以完成这种淡入淡出的动画效果呢?
原因在于,从我们单击注册按钮一直到注册页面完全出现的这段时间内,transitionBuilder 会受到持续的调用,
第一次被调用的时候,animation 的值为 0; 而当最后一次被调用的时候,animation 的值为 1.

FadeTransition 的作用则在于,根据我们传入的 opacity 参数的不同,调整 child 的透明度。
看到这里,我们不难猜出,自定义过渡效果的实质,就是在执行页面跳转的过程中,不停地调用 transitionsBuilder,然后创建出新的具有不同透明度的 RegisterPage。
而当从 "注册" 页面返回 "登录" 页面时,这个过程正好相反。

完善我们的 "注册" 页面

到目前为止,我们已经基本了解了如何从一个页面跳转到另一个页面,现在是时候把目光移回 "注册" 页面了。
"注册" 页面的内容和 "登录" 页面基本相似,因此这里我们不会像构建 "登录" 页面那样展示详细的构建过程,而是挑选其中略有不同的地方做重点说明。

"注册" 页面和 "登录" 页面的主要区别在于,"注册" 页面中顶部的图片是需要用户进行选择的。
另外,我们还需要微调一下 "登录" 页面和 "注册" 页面之间的跳转逻辑。

现在的注册页面:

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

class RegisterPageArgument {
  final String className;
  final String url;

  RegisterPageArgument(this.className, this.url);
}

class RegisterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final RegisterPageArgument argument =
        ModalRoute.of(context)?.settings.arguments as RegisterPageArgument;
    return Scaffold(
      body: Center(
        child: GestureDetector(
          child: Text('注册页面, 从${argument.className}-${argument.url}跳转而来'),
          onTap: () => Navigator.of(context).pop(),
        ),
      ),
    );
  }
}

和登录页面基本相似的注册页面:

  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
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import 'package:flutter/material.dart';

class RegisterPageArgument {
  final String className;
  final String url;

  RegisterPageArgument(this.className, this.url);
}

class RegisterPage extends StatefulWidget {
  @override
  State<RegisterPage> createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  FocusNode emailFocusNode = FocusNode();
  FocusNode passowrdFocusNode = FocusNode();
  FocusNode confirmPassowrdFocusNode = FocusNode();
  bool canRegister = false;

  TextEditingController _emailController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();
  TextEditingController _confirmPasswordController = TextEditingController();

  void _checkInputValid(String _) {
    // 这里的参数写成 _ 表示在这里我们没有使用这个参数,这是一种比较约定俗成的写法
    bool isInputValid = _emailController.text.contains('@') &&
        _passwordController.text.length >= 6 &&
        _passwordController.text == _confirmPasswordController.text;
    if (isInputValid == canRegister) {
      return;
    }
    setState(() {
      canRegister = isInputValid;
    });
  }

  @override
  void initState() {
    super.initState();
    canRegister = false;
    emailFocusNode = FocusNode();
    passowrdFocusNode = FocusNode();
    confirmPassowrdFocusNode = FocusNode();
  }

  @override
  void dispose() {
    super.dispose();
    emailFocusNode.dispose();
    passowrdFocusNode.dispose();
    confirmPassowrdFocusNode.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        emailFocusNode.unfocus();
        passowrdFocusNode.unfocus();
        confirmPassowrdFocusNode.unfocus();
        FocusManager.instance.primaryFocus?.unfocus();
      },
      child: Scaffold(
        body: SingleChildScrollView(
          child: ConstrainedBox(
            // 利用 MediaQuery 来获取屏幕的高度
            constraints: BoxConstraints(
              maxHeight: MediaQuery.of(context).size.height,
            ),
            child: Center(
              child: Column(
                children: <Widget>[
                  // 利用 Expanded 组件让两个子组件都能占满整个屏幕
                  Expanded(
                    child: Container(
                      color: Colors.white,
                      child: Center(
                        child: FractionallySizedBox(
                          child: Image.asset('assets/images/mark.png'),
                          widthFactor: 0.4,
                          heightFactor: 0.4,
                        ),
                      ),
                    ),
                  ),
                  Expanded(
                    child: Center(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          Padding(
                            padding: const EdgeInsets.only(
                                left: 24, right: 24, bottom: 12),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.stretch,
                              children: <Widget>[
                                TextField(
                                  decoration: InputDecoration(
                                    hintText: '请输入邮箱',
                                    labelText: '邮箱',
                                  ),
                                  focusNode: emailFocusNode,
                                  textInputAction: TextInputAction.next,
                                  onChanged: _checkInputValid,
                                  controller: _emailController,
                                ),
                                TextField(
                                  decoration: InputDecoration(
                                    hintText: '请输入六位以上的密码',
                                    labelText: '密码',
                                  ),
                                  focusNode: passowrdFocusNode,
                                  textInputAction: TextInputAction.next,
                                  obscureText: true,
                                  onChanged: _checkInputValid,
                                  controller: _passwordController,
                                ),
                                TextField(
                                  decoration: InputDecoration(
                                    hintText: '请输入六位以上的密码',
                                    labelText: '确认密码',
                                  ),
                                  focusNode: confirmPassowrdFocusNode,
                                  obscureText: true,
                                  onChanged: _checkInputValid,
                                  controller: _confirmPasswordController,
                                ),
                              ],
                            ),
                          ),
                          Padding(
                            padding: const EdgeInsets.only(
                                left: 24, right: 24, bottom: 12),
                            child: TextButton(
                              child: Text('注册',
                                  style: TextStyle(color: Colors.white)),
                              onPressed: canRegister ? () {} : null,
                              style: TextButton.styleFrom(
                                backgroundColor:
                                    Color.fromRGBO(69, 202, 181, 1),
                                disabledBackgroundColor:
                                    Color.fromRGBO(69, 202, 160, 0.5),
                              ),
                            ),
                          ),
                          Padding(
                            padding: const EdgeInsets.only(
                                left: 24, right: 24, bottom: 12),
                            child: Container(
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: <Widget>[
                                  Text('已有账号? '),
                                  InkWell(
                                    child: Text('立即登录'),
                                    onTap: () => Navigator.of(context).pop(),
                                  ),
                                ],
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

处理 "注册" 页面中的用户头像

"注册" 页面中的用户头像部分,在 assets/images/default_avatar.png

在 pubspce.yaml 文件中增加照片的引用:

1
2
3
assets:
    - assets/images/mark.png
    - assets/images/default_avatar.png

考虑到这里的用户头像是圆形的,我们可以直接使用CircleAvatar 这个 Widget 来实现默认的用户头像:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
child: FractionallySizedBox(
    child: CircleAvatar(
        backgroundColor: Colors.transparent,
        radius: 48,
        backgroundImage:
            AssetImage('assets/images/default_avatar.png'),
    ),
    widthFactor: 0.4,
    heightFactor: 0.4,
),

还可以注意到,用户头像的右上角有一个加号图标,我们可以直接使用 Flutter 内置的 Icon Widget 中的 add 方法啊来实现这个加号图标。
同时,我们可以为这个 Icon Widget 增加一个装饰器来实现其外部的绿色背景。
外部这种嵌套层级的布局,我们可以使用栈来实现:

 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
child: FractionallySizedBox(
    child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
            CircleAvatar(
                backgroundColor: Colors.transparent,
                radius: 48,
                backgroundImage: AssetImage(
                    'assets/images/default_avatar.png'),
            ),
            Positioned(
                right: 20,
                top: 5,
                child: Container(
                    decoration: BoxDecoration(
                        borderRadius:
                            BorderRadius.all(Radius.circular(17)),
                        color: Color.fromARGB(255, 80, 210, 194),
                    ),
                    child: Icon(
                        Icons.add,
                        size: 34,
                        color: Colors.white,
                    ),
                ),
            ),
        ],
    ),
    widthFactor: 0.4,
    heightFactor: 0.4,
)

这里我们将栈的 fit 属性值设置为 StackFitexpand, 表示把 avatar 这种没有指定位置的组件拉伸到和栈一样大。

这样一来,就实现了设计稿上的效果。
接下来我们需要为用户头像增加事件处理机制,实现当单击用户头像的时候,从相册中选择图片并替换这里默认的用户头像:

为了能够获取用户相册中的图片,需要引入一个第三方的 dart 包: image_picker

https://pub.dev/packages/image_picker/install

1
2
3
4
5
6
7
8
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  flutter_cupertino_localizations: ^1.0.1
  cupertino_icons: ^1.0.2
  image_picker: ^0.8.2

接下来我们需要创建一个 File 类型的变量,来存储用户选择的图片。
然后使用 image_picker 提供的 getImage 方法就可以从相册中选择图片了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import 'dart:io';
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart';

class _RegisterPageState extends State<RegisterPage> {
  ...
  final picker = ImagePicker();
  File? image;

  Future<void> _getImage() async {
    XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
    setState(() {
      image = File(pickedFile!.path);
    });
  }
  ...
}

这里我们为了简单起见,给 getImage 方法传入的 source 参数为 ImageSource.gallery,表示将从用户的本地相册中选择头像

接下来,在 build 方法中使用从相册里获取的图片即可:

 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
child: GestureDetector(
    onTap: _getImage,
    child: FractionallySizedBox(
        child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
                CircleAvatar(
                    backgroundColor: Colors.transparent,
                    radius: 48,
                    backgroundImage: image == null
                        ? AssetImage(
                            'assets/images/default_avatar.png')
                        : FileImage(image!)
                            as ImageProvider<Object>,
                ),
                Positioned(
                    right: 20,
                    top: 5,
                    child: Container(
                        decoration: BoxDecoration(
                            borderRadius:
                                BorderRadius.all(Radius.circular(17)),
                            color: Color.fromARGB(255, 80, 210, 194),
                        ),
                        child: Icon(
                            Icons.add,
                            size: 34,
                            color: Colors.white,
                        ),
                    ),
                ),
            ],
        ),
        widthFactor: 0.4,
        heightFactor: 0.4,
    ),
)

现在,我们就可以从本地相册中选择图片作为用户头像,并更新在 "注册" 页面上了

处理 "登录" 页面与 "注册" 页面之间的跳转逻辑

当构建好 "注册" 页面的时候,我们意识到 "登录" 页面和 "注册" 页面在业务逻辑上需要相互跳转,这时就不能简单在 "注册" 页面通过 pop 方法返回 "登录" 页面了,
而需要通过 pushReplacementNamed 方法来让两个页面进行跳转:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// login.dart
void _gotoRegister(BuildContext context) {
    Navigator.of(context).pushReplacementNamed(REGISTER_PAGE_URL,
        arguments: RegisterPageArgument('LoginPage', LOGIN_PAGE_URL));
}

// register.dart
child: Text('立即登录'),
onTap: () => Navigator.of(context)
    .pushReplacementNamed(LOGIN_PAGE_URL),

为了保证相互跳转的效果保持一致,我们也需要将跳转 "登录" 页面的过渡效果更改为渐变。
这里直接在 main.dart 文件中修改之前的路由跳转判断规则即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
home: routes[LOGIN_PAGE_URL]!(context),
initialRoute: LOGIN_PAGE_URL,
routes: {
    REGISTER_PAGE_URL: (context) => RegisterPage(),
},
onGenerateRoute: (RouteSettings settings) {
    if ([REGISTER_PAGE_URL, LOGIN_PAGE_URL].contains(settings.name)) {
        return PageRouteBuilder(
            settings: settings,
            pageBuilder: (context, _, __) => routes[settings.name]!(context),
            transitionsBuilder:
                (context, animation, secondaryAnimation, child) {
                return FadeTransition(
                opacity: animation,
                child: child,
                );
            },
        );
    }
    return MaterialPageRoute(
        builder: routes[settings.name] as Widget Function(BuildContext),
        settings: settings);
},

小结

 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
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:todo/config/colors.dart';
import 'package:todo/pages/login.dart';
import 'package:todo/pages/register.dart';
import 'package:todo/pages/route_url.dart';

void main() {
  runApp(const MyApp());
}

final Map<String, WidgetBuilder> routes = {
  LOGIN_PAGE_URL: (context) => LoginPage(),
  REGISTER_PAGE_URL: (context) => RegisterPage(),
};

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: PRIMARY_COLOR,
        indicatorColor: ACCENT_COLOR,
        accentColor: PRIMARY_COLOR,
      ),
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en'),
        const Locale('zh', 'CN'),
      ],
      home: routes[LOGIN_PAGE_URL]!(context),
      initialRoute: LOGIN_PAGE_URL,
      routes: {
        REGISTER_PAGE_URL: (context) => RegisterPage(),
      },
      onGenerateRoute: (RouteSettings settings) {
        if ([REGISTER_PAGE_URL, LOGIN_PAGE_URL].contains(settings.name)) {
          return PageRouteBuilder(
            settings: settings,
            pageBuilder: (context, _, __) => routes[settings.name]!(context),
            transitionsBuilder:
                (context, animation, secondaryAnimation, child) {
              return FadeTransition(
                opacity: animation,
                child: child,
              );
            },
          );
        }
        return MaterialPageRoute(
            builder: routes[settings.name] as Widget Function(BuildContext),
            settings: settings);
      },
    );
  }
}
  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
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// login.dart
import 'package:flutter/material.dart';
import 'package:todo/pages/register.dart';
import 'package:todo/pages/route_url.dart';

class LoginPage extends StatefulWidget {
  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  FocusNode emailFocusNode = FocusNode();
  FocusNode passowrdFocusNode = FocusNode();
  bool canLogin = false;

  TextEditingController _emailController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();

  void _checkInputValid(String _) {
    // 这里的参数写成 _ 表示在这里我们没有使用这个参数,这是一种比较约定俗成的写法
    bool isInputValid = _emailController.text.contains('@') &&
        _passwordController.text.length >= 6;
    if (isInputValid == canLogin) {
      return;
    }
    setState(() {
      canLogin = isInputValid;
    });
  }

  @override
  void initState() {
    super.initState();
    canLogin = false;
    emailFocusNode = FocusNode();
    passowrdFocusNode = FocusNode();
  }

  @override
  void dispose() {
    super.dispose();
    emailFocusNode.dispose();
    passowrdFocusNode.dispose();
  }

  void _gotoRegister(BuildContext context) {
    Navigator.of(context).pushReplacementNamed(REGISTER_PAGE_URL,
        arguments: RegisterPageArgument('LoginPage', LOGIN_PAGE_URL));
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        emailFocusNode.unfocus();
        passowrdFocusNode.unfocus();
        FocusManager.instance.primaryFocus?.unfocus();
      },
      child: Scaffold(
        body: Center(
          child: Column(
            children: <Widget>[
              // 利用 Expanded 组件让两个子组件都能占满整个屏幕
              Expanded(
                child: Container(
                  color: Colors.white,
                  child: Center(
                    child: FractionallySizedBox(
                      child: Image.asset('assets/images/mark.png'),
                      widthFactor: 0.4,
                      heightFactor: 0.4,
                    ),
                  ),
                ),
              ),
              Expanded(
                child: Center(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.stretch,
                          children: <Widget>[
                            TextField(
                              decoration: InputDecoration(
                                hintText: '请输入邮箱',
                                labelText: '邮箱',
                              ),
                              focusNode: emailFocusNode,
                              textInputAction: TextInputAction.next,
                              onChanged: _checkInputValid,
                              controller: _emailController,
                            ),
                            TextField(
                              decoration: InputDecoration(
                                hintText: '请输入六位以上的密码',
                                labelText: '密码',
                              ),
                              focusNode: passowrdFocusNode,
                              obscureText: true,
                              onChanged: _checkInputValid,
                              controller: _passwordController,
                            ),
                          ],
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: TextButton(
                          child:
                              Text('登录', style: TextStyle(color: Colors.white)),
                          onPressed: canLogin ? () {} : null,
                          style: TextButton.styleFrom(
                            backgroundColor: Color.fromRGBO(69, 202, 181, 1),
                            disabledBackgroundColor:
                                Color.fromRGBO(69, 202, 160, 0.5),
                          ),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(
                            left: 24, right: 24, bottom: 12),
                        child: Container(
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: <Widget>[
                              Text('没有账号? '),
                              InkWell(
                                child: Text('立即注册'),
                                onTap: () => _gotoRegister(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
 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
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
// register.dart
import 'dart:io';
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:todo/pages/route_url.dart';

class RegisterPageArgument {
  final String className;
  final String url;

  RegisterPageArgument(this.className, this.url);
}

class RegisterPage extends StatefulWidget {
  @override
  State<RegisterPage> createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  FocusNode emailFocusNode = FocusNode();
  FocusNode passowrdFocusNode = FocusNode();
  FocusNode confirmPassowrdFocusNode = FocusNode();
  bool canRegister = false;
  final picker = ImagePicker();
  File? image;

  Future<void> _getImage() async {
    XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery);
    setState(() {
      image = File(pickedFile!.path);
    });
  }

  TextEditingController _emailController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();
  TextEditingController _confirmPasswordController = TextEditingController();

  void _checkInputValid(String _) {
    // 这里的参数写成 _ 表示在这里我们没有使用这个参数,这是一种比较约定俗成的写法
    bool isInputValid = _emailController.text.contains('@') &&
        _passwordController.text.length >= 6 &&
        _passwordController.text == _confirmPasswordController.text;
    if (isInputValid == canRegister) {
      return;
    }
    setState(() {
      canRegister = isInputValid;
    });
  }

  @override
  void initState() {
    super.initState();
    canRegister = false;
    emailFocusNode = FocusNode();
    passowrdFocusNode = FocusNode();
    confirmPassowrdFocusNode = FocusNode();
  }

  @override
  void dispose() {
    super.dispose();
    emailFocusNode.dispose();
    passowrdFocusNode.dispose();
    confirmPassowrdFocusNode.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        emailFocusNode.unfocus();
        passowrdFocusNode.unfocus();
        confirmPassowrdFocusNode.unfocus();
        FocusManager.instance.primaryFocus?.unfocus();
      },
      child: Scaffold(
        body: SingleChildScrollView(
          child: ConstrainedBox(
            // 利用 MediaQuery 来获取屏幕的高度
            constraints: BoxConstraints(
              maxHeight: MediaQuery.of(context).size.height,
            ),
            child: Center(
              child: Column(
                children: <Widget>[
                  // 利用 Expanded 组件让两个子组件都能占满整个屏幕
                  Expanded(
                    child: Container(
                      color: Colors.white,
                      child: Center(
                        child: GestureDetector(
                          onTap: _getImage,
                          child: FractionallySizedBox(
                            child: Stack(
                              fit: StackFit.expand,
                              children: <Widget>[
                                CircleAvatar(
                                  backgroundColor: Colors.transparent,
                                  radius: 48,
                                  backgroundImage: image == null
                                      ? AssetImage(
                                          'assets/images/default_avatar.png')
                                      : FileImage(image!)
                                          as ImageProvider<Object>,
                                ),
                                Positioned(
                                  right: 20,
                                  top: 5,
                                  child: Container(
                                    decoration: BoxDecoration(
                                      borderRadius:
                                          BorderRadius.all(Radius.circular(17)),
                                      color: Color.fromARGB(255, 80, 210, 194),
                                    ),
                                    child: Icon(
                                      Icons.add,
                                      size: 34,
                                      color: Colors.white,
                                    ),
                                  ),
                                ),
                              ],
                            ),
                            widthFactor: 0.4,
                            heightFactor: 0.4,
                          ),
                        ),
                      ),
                    ),
                  ),
                  Expanded(
                    child: Center(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          Padding(
                            padding: const EdgeInsets.only(
                                left: 24, right: 24, bottom: 12),
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.stretch,
                              children: <Widget>[
                                TextField(
                                  decoration: InputDecoration(
                                    hintText: '请输入邮箱',
                                    labelText: '邮箱',
                                  ),
                                  focusNode: emailFocusNode,
                                  textInputAction: TextInputAction.next,
                                  onChanged: _checkInputValid,
                                  controller: _emailController,
                                ),
                                TextField(
                                  decoration: InputDecoration(
                                    hintText: '请输入六位以上的密码',
                                    labelText: '密码',
                                  ),
                                  focusNode: passowrdFocusNode,
                                  textInputAction: TextInputAction.next,
                                  obscureText: true,
                                  onChanged: _checkInputValid,
                                  controller: _passwordController,
                                ),
                                TextField(
                                  decoration: InputDecoration(
                                    hintText: '请输入六位以上的密码',
                                    labelText: '确认密码',
                                  ),
                                  focusNode: confirmPassowrdFocusNode,
                                  obscureText: true,
                                  onChanged: _checkInputValid,
                                  controller: _confirmPasswordController,
                                ),
                              ],
                            ),
                          ),
                          Padding(
                            padding: const EdgeInsets.only(
                                left: 24, right: 24, bottom: 12),
                            child: TextButton(
                              child: Text('注册',
                                  style: TextStyle(color: Colors.white)),
                              onPressed: canRegister ? () {} : null,
                              style: TextButton.styleFrom(
                                backgroundColor:
                                    Color.fromRGBO(69, 202, 181, 1),
                                disabledBackgroundColor:
                                    Color.fromRGBO(69, 202, 160, 0.5),
                              ),
                            ),
                          ),
                          Padding(
                            padding: const EdgeInsets.only(
                                left: 24, right: 24, bottom: 12),
                            child: Container(
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: <Widget>[
                                  Text('已有账号? '),
                                  InkWell(
                                    child: Text('立即登录'),
                                    onTap: () => Navigator.of(context)
                                        .pushReplacementNamed(LOGIN_PAGE_URL),
                                  ),
                                ],
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}