路由 + 登录页面 + Token 持久化

完整示例:路由 + 登录页面 + 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())];
        }
      },
    );
  }
}

业务逻辑说明

  1. APP冷启动,main函数自动读取SP里的token;
  2. 有token → 直接进入首页;无token → 跳转登录页;
  3. 登录成功保存token到shared_preferences,后续http请求自动带上token;
  4. 路由跳转:pushNamed 入栈(可返回),pushReplacementNamed替换页面(不可返回,适合登录/退出);
  5. 退出登录清空本地token,回到登录页。

注意事项 & 联调SpringBoot

  1. SpringBoot后端登录接口 POST /api/auth/login 返回格式示例

    {
    "code":200,
    "data":{
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "userName":"admin"
    }
    }

  2. 后端接口校验Header中的token字段;token过期逻辑可以在http拦截器捕获401,自动跳转登录页(可后续加上)

  3. shared_preferences是明文存储token;生产环境如果安全要求高,可以改用flutter_secure_storage加密存储。

相关推荐
古法安卓3 天前
Android-SystemUI ANR协程相关问题排查
android·java·android studio
我命由我123455 天前
Android 广播 - 组件通信
android·java·开发语言·android studio·android jetpack·android-studio·android runtime
hai_android6 天前
Android 嵌套滑动冲突:内部拦截 vs 外部拦截实战
android·android studio·android jetpack
浪潮IT馆8 天前
Android Studio 非最新版本下载安装教程
android·ide·android studio
hai_android10 天前
InputStage 责任链核心执行流程
android·android studio·android jetpack
执明wa15 天前
Android RecyclerView 实战:点击频道栏切换对应内容
android·开发语言·windows·microsoft·android studio
kv11017 天前
Android studio国内开发有关
java·数据库·android studio
烽火聊员18 天前
Android屏幕卡顿traces.txt拉取查看
android·java·android studio
古法安卓18 天前
Android-Ext4文件系统问题排查
android·java·android studio