完整示例:路由 + 登录页面 + Token持久化(基于前面这套 Flutter+Provider 公交运维项目架构)
依赖:
provider: ^6.1.1
shared_preferences: ^2.2.3 // 本地持久化存储token
沿用项目目录结构:
pages/login、provider/auth_provider.dart、routes/app_routes.dart
1. pubspec.yaml 确认依赖
dependencies:
flutter:
sdk: flutter
http: ^1.2.1
provider: ^6.1.1
web_socket_channel: ^2.4.0
shared_preferences: ^2.2.3
flutter pub get
2. DTO:dto/login_dto.dart
class LoginReqDto {
final String username;
final String password;
LoginReqDto({required this.username, required this.password});
Map<String, dynamic> toJson() {
return {
"username": username,
"password": password,
};
}
}
class LoginRespDto {
final String token;
final String userName;
LoginRespDto({required this.token, required this.userName});
factory LoginRespDto.fromJson(Map<String, dynamic> json) {
return LoginRespDto(
token: json["token"],
userName: json["userName"],
);
}
}
3. Repository:repository/auth_repository.dart
import 'dart:convert';
import '../common/http_client.dart';
import '../dto/login_dto.dart';
class AuthRepository {
final HttpClient _http = HttpClient();
// 请求SpringBoot登录接口
Future<LoginRespDto> login(LoginReqDto req) async {
final res = await _http.post("/api/auth/login", body: req.toJson());
return LoginRespDto.fromJson(res["data"]);
}
}
4. Provider:provider/auth_provider.dart(登录状态 + token读写本地存储)
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../dto/login_dto.dart';
import '../repository/auth_repository.dart';
class AuthProvider extends ChangeNotifier {
final AuthRepository _repo = AuthRepository();
String? _token;
String? _userName;
String? get token => _token;
String? get userName => _userName;
bool get isLogin => _token != null && _token!.isNotEmpty;
// 初始化:APP启动从本地SP读取token
Future<void> loadTokenFromLocal() async {
final sp = await SharedPreferences.getInstance();
_token = sp.getString("token");
_userName = sp.getString("userName");
notifyListeners();
}
// 登录
Future<bool> login(String username, String password) async {
try {
LoginRespDto resp = await _repo.login(LoginReqDto(username: username, password: password));
_token = resp.token;
_userName = resp.userName;
// 持久化保存到本地
final sp = await SharedPreferences.getInstance();
await sp.setString("token", _token!);
await sp.setString("userName", _userName!);
notifyListeners();
return true;
} catch (e) {
debugPrint("登录失败:$e");
return false;
}
}
// 退出登录,清除本地token
Future<void> logout() async {
_token = null;
_userName = null;
final sp = await SharedPreferences.getInstance();
await sp.clear();
notifyListeners();
}
}
5. 路由管理:routes/app_routes.dart
import 'package:flutter/material.dart';
import '../pages/login/login_page.dart';
import '../pages/home/home_page.dart';
import '../pages/alarm/alarm_list_page.dart';
class AppRoutes {
// 路由常量
static const String login = "/login";
static const String home = "/home";
static const String alarmList = "/alarmList";
// 路由表
static Map<String, WidgetBuilder> routes = {
login: (context) => const LoginPage(),
home: (context) => const HomePage(),
alarmList: (context) => const AlarmListPage(),
};
// 全局路由跳转工具方法
static pushNamed(BuildContext context, String routeName) {
Navigator.pushNamed(context, routeName);
}
// 跳转并清空栈(登录成功进入首页,不能返回登录页)
static pushReplacementNamed(BuildContext context, String routeName) {
Navigator.pushReplacementNamed(context, routeName);
}
}
6. 登录页面 pages/login/login_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../provider/auth_provider.dart';
import '../../routes/app_routes.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController _usernameCtrl = TextEditingController();
final TextEditingController _passwordCtrl = TextEditingController();
bool loading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("公交运维系统登录")),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
TextField(
controller: _usernameCtrl,
decoration: const InputDecoration(labelText: "用户名"),
),
TextField(
controller: _passwordCtrl,
obscureText: true,
decoration: const InputDecoration(labelText: "密码"),
),
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: loading
? null
: () async {
setState(() => loading = true);
final authProvider = Provider.of<AuthProvider>(context, listen: false);
bool ok = await authProvider.login(
_usernameCtrl.text.trim(),
_passwordCtrl.text.trim(),
);
setState(() => loading = false);
if (ok) {
// 登录成功,跳转首页,销毁登录页面,无法返回
AppRoutes.pushReplacementNamed(context, AppRoutes.home);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("账号或密码错误!")),
);
}
}
},
child: loading ? const Text("登录中...") : const Text("登录"),
),
)
],
),
),
);
}
}
7. 首页 pages/home/home_page.dart(首页,包含跳转到告警列表 + 退出登录)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../provider/auth_provider.dart';
import '../../routes/app_routes.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text("欢迎,${auth.userName}"),
actions: [
TextButton(
onPressed: () async {
await auth.logout();
// 退出登录,跳回登录页
AppRoutes.pushReplacementNamed(context, AppRoutes.login);
},
child: const Text("退出登录", style: TextStyle(color: Colors.white)),
)
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("公交运维首页"),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
AppRoutes.pushNamed(context, AppRoutes.alarmList);
},
child: const Text("查看告警列表"),
),
],
),
),
);
}
}
8. 改造 common/http_client.dart 自动带上token(重点!每次请求自动携带token)
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import '../constants.dart';
import '../provider/auth_provider.dart';
class HttpClient {
final String baseUrl = Constants.baseUrl;
// GET请求
Future<Map<String, dynamic>> get(String path, BuildContext context) async {
final auth = Provider.of<AuthProvider>(context, listen: false);
final headers = <String, String>{
"Content-Type": "application/json",
if (auth.token != null) "token": auth.token!,
};
final uri = Uri.parse("$baseUrl$path");
final resp = await http.get(uri, headers: headers);
return _handleResponse(resp);
}
// POST请求
Future<Map<String, dynamic>> post(String path, {required Map<String, dynamic> body, required BuildContext context}) async {
final auth = Provider.of<AuthProvider>(context, listen: false);
final headers = <String, String>{
"Content-Type": "application/json",
if (auth.token != null) "token": auth.token!,
};
final uri = Uri.parse("$baseUrl$path");
final resp = await http.post(uri, headers: headers, body: jsonEncode(body));
return _handleResponse(resp);
}
Map<String, dynamic> _handleResponse(http.Response resp) {
if (resp.statusCode == 200) {
return jsonDecode(resp.body);
} else {
throw Exception("接口异常 code:${resp.statusCode}");
}
}
}
9. main.dart(入口,初始化Auth、路由守卫:自动判断是否登录,跳转页面)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider/auth_provider.dart';
import 'provider/alarm_provider.dart';
import 'routes/app_routes.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化Auth,读取本地保存的token
final authProvider = AuthProvider();
await authProvider.loadTokenFromLocal();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: authProvider),
ChangeNotifierProvider(create: (_) => AlarmProvider()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '公交运维',
theme: ThemeData(primarySwatch: Colors.blue),
initialRoute: AppRoutes.login,
routes: AppRoutes.routes,
// 路由守卫:判断登录状态
onGenerateInitialRoutes: (initialRouteName) {
final auth = Provider.of<AuthProvider>(context, listen: false);
if (auth.isLogin) {
return [MaterialPageRoute(builder: (_) => const HomePage())];
} else {
return [MaterialPageRoute(builder: (_) => const LoginPage())];
}
},
);
}
}
业务逻辑说明
- APP冷启动,main函数自动读取SP里的token;
- 有token → 直接进入首页;无token → 跳转登录页;
- 登录成功保存token到shared_preferences,后续http请求自动带上token;
- 路由跳转:pushNamed 入栈(可返回),pushReplacementNamed替换页面(不可返回,适合登录/退出);
- 退出登录清空本地token,回到登录页。
注意事项 & 联调SpringBoot
-
SpringBoot后端登录接口
POST /api/auth/login返回格式示例{
"code":200,
"data":{
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"userName":"admin"
}
} -
后端接口校验Header中的
token字段;token过期逻辑可以在http拦截器捕获401,自动跳转登录页(可后续加上) -
shared_preferences是明文存储token;生产环境如果安全要求高,可以改用flutter_secure_storage加密存储。