You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter服务端校验错误展示及try-catch合理使用咨询

Flutter服务端校验错误展示问题

我在Flutter中展示服务端返回的校验错误时遇到两个问题:

  1. API、Service层、View层都加了try-catch,不确定这种方式是否正确;
  2. 点击按钮弹出错误对话框时,提示:An error occurred: NoSuchMethodError: The Method '[]' was called on null. Receiver: null. Tried calling: []("message")

相关代码

API代码(PHP)

public function register(Request $request)
{
    try {
        // 校验客户端传入的数据
        $request->validate([
            'name' => 'required|string|max:255',
            'username' => 'required|string|max:255|unique:users',
            'email' => 'required|email|unique:users',
            'password' => 'required|string|min:6',
            'password_confirmation' => 'required|string|same:password',
        ]);

        // 创建User实例
        $user = new User();
        $user->name = $request->input('name');
        $user->username = $request->input('username');
        $user->email = $request->input('email');
        $user->password = bcrypt($request->input('password'));
        $user->save();

        // 返回注册成功响应
        return response()->json(['message' => 'Registrasi berhasil!', 'data' => $user], 201);
    } catch (ValidationException $e) {
        // 处理校验错误,返回400状态码
        return response()->json(['message' => 'Validasi gagal', 'errors' => $e->errors()], 400);
    } catch (\Exception $e) {
        // 处理其他服务端异常
        return response()->json(['message' => 'Registrasi gagal', 'error' => $e->getMessage()], 500);
    }
}

Flutter Service层代码

Future register(
      {required String name,
      required String username,
      required String email,
      required String password,
      required String password_confirmation}) async {
    final Map jsonData = {
      'name': name,
      'username': username,
      'email': email,
      'password': password,
      'password_confirmation': password_confirmation,
    };

    try {
      final Map jsonData = {
        'name': name,
        'username': username,
        'email': email,
        'password': password,
        'password_confirmation': password_confirmation,
      };
      Response response = await dio.post('$baseUrl/register', data: jsonData);
      print(response.data);

      if (response.statusCode == 201) {
        final Map<String, dynamic> resultData = response.data;
        return resultData;
      } else if (response.statusCode == 400) {
        final Map<String, dynamic> resultData = response.data;
        return resultData;
      }
    } on DioException catch (e) {
      print(e);
    }
  }

Flutter View层按钮代码

ElevatedButton(
                  onPressed: () async {
                    if (key.currentState!.validate()) {
                      final navigator = Navigator.of(context);

                      showDialog(
                        context: context,
                        barrierDismissible: false,
                        builder: (context) {
                          return const Center(
                              child: CircularProgressIndicator());
                        },
                      );

                      try {
                        final data = await AuthService().register(
                          name: nameC.text,
                          username: usernameC.text,
                          email: emailC.text,
                          password: passwordC.text,
                          password_confirmation: password_confirmationC.text,
                        );
                        print(data);
                        if (data["message"] == "Registrasi berhasil!") {
                          navigator.pushAndRemoveUntil(
                            MaterialPageRoute(
                              builder: (ctx) => const LoginPage(),
                            ),
                            (route) => false,
                          );
                        } else {
                          throw Error();
                        }
                      } catch (e) {
                        navigator.pop();
                        if (e is Map<String, dynamic> &&
                            e.containsKey("validationError")) {
                          // 处理API返回的校验错误
                          showDialog(
                            context: context,
                            builder: (ctx) {
                              final validationErrors = e["errors"];
                              final errorMessages = validationErrors.values
                                  .expand((value) => value)
                                  .toList();
                              return AlertDialog(
                                shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(12),
                                ),
                                title: Text('Registrasi Gagal'),
                                content: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  mainAxisSize: MainAxisSize.min,
                                  children: errorMessages.map((message) {
                                    return Text(message.toString());
                                  }).toList(),
                                ),
                                actions: [
                                  TextButton(
                                    onPressed: () {
                                      Navigator.pop(context);
                                    },
                                    child: Text(
                                      "Ok",
                                      style: TextStyle(color: Colors.green),
                                    ),
                                  ),
                                ],
                              );
                            },
                          );
                        } else {
                          // 处理非校验类错误
                          showDialog(
                            context: context,
                            builder: (ctx) {
                              return AlertDialog(
                                shape: RoundedRectangleBorder(
                                  borderRadius: BorderRadius.circular(12),
                                ),
                                title: Text('Registrasi Gagal'),
                                content: Text('Terjadi kesalahan: $e'),
                                actions: [
                                  TextButton(
                                    onPressed: () {
                                      Navigator.pop(context);
                                    },
                                    child: Text(
                                      "Ok",
                                      style: TextStyle(color: Colors.green),
                                    ),
                                  ),
                                ],
                              );
                            },
                          );
                        }
                      }
                    }
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(20.0),
                    ),
                    minimumSize: Size(double.infinity, 50.0),
                  ),
                  child: Text('Register'),
                ),

请求解答

  1. try-catch应该合理放置在什么位置?
  2. 如何实现在Flutter中正确展示服务端返回的校验错误?

解答

1. try-catch的合理位置

  • API层:保留try-catch是合理的,这里需要捕获服务端内部异常(如校验失败、数据库操作错误),返回标准化的JSON错误响应给客户端,避免未处理异常导致服务崩溃。
  • Service层(Flutter):不能只捕获异常却不向上传递。当前代码捕获DioException后仅打印,没有返回或抛出错误,导致View层拿到null,这是触发NoSuchMethodError的核心原因之一。正确做法是:捕获异常后解析服务端错误数据并抛出,或直接将异常传递给View层处理。
  • View层:必须保留try-catch,用来接收Service层传递的错误,然后展示对应的UI提示(如对话框)。

总结各层职责:

  • API层:处理服务端内部异常,返回标准化错误响应
  • Service层:捕获网络异常,解析错误信息后向上抛出
  • View层:捕获Service层的错误,展示用户友好的错误提示

2. 正确展示服务端校验错误的修复方案

错误原因分析

  1. Service层捕获DioException后未抛出错误,导致View层的data变量为null,执行data["message"]时触发空指针错误。
  2. View层判断校验错误的条件是e.containsKey("validationError"),但API返回的校验错误中并没有该字段,而是message为Validasi gagal且包含errors字段,导致条件不匹配,进入通用错误分支。

修正后的代码

修正Flutter Service层代码

Future<Map<String, dynamic>?> register(
      {required String name,
      required String username,
      required String email,
      required String password,
      required String password_confirmation}) async {
    final Map jsonData = {
      'name': name,
      'username': username,
      'email': email,
      'password': password,
      'password_confirmation': password_confirmation,
    };

    try {
      Response response = await dio.post('$baseUrl/register', data: jsonData);
      print(response.data);

      if (response.statusCode == 201 || response.statusCode == 400) {
        return response.data as Map<String, dynamic>;
      }
    } on DioException catch (e) {
      // 解析服务端返回的错误数据
      if (e.response != null) {
        throw e.response!.data as Map<String, dynamic>;
      }
      // 处理无响应的网络错误
      throw {'message': '网络连接失败,请检查网络状态'};
    }
    return null;
  }

修正Flutter View层按钮代码

ElevatedButton(
  onPressed: () async {
    if (key.currentState!.validate()) {
      final navigator = Navigator.of(context);

      showDialog(
        context: context,
        barrierDismissible: false,
        builder: (context) {
          return const Center(child: CircularProgressIndicator());
        },
      );

      try {
        final data = await AuthService().register(
          name: nameC.text,
          username: usernameC.text,
          email: emailC.text,
          password: passwordC.text,
          password_confirmation: password_confirmationC.text,
        );
        print(data);
        if (data?["message"] == "Registrasi berhasil!") {
          navigator.pushAndRemoveUntil(
            MaterialPageRoute(builder: (ctx) => const LoginPage()),
            (route) => false,
          );
        } else if (data?["message"] == "Validasi gagal") {
          // 触发校验错误处理逻辑
          throw data!;
        } else {
          throw {'message': '未知错误,请重试'};
        }
      } catch (e) {
        navigator.pop();
        if (e is Map<String, dynamic> && e.containsKey("errors")) {
          // 展示服务端校验错误列表
          showDialog(
            context: context,
            builder: (ctx) {
              final validationErrors = e["errors"] as Map<String, dynamic>;
              final errorMessages = validationErrors.values
                  .expand((value) => value)
                  .toList();
              return AlertDialog(
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                title: const Text('Registrasi Gagal'),
                content: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: errorMessages.map((message) {
                    return Text(message.toString());
                  }).toList(),
                ),
                actions: [
                  TextButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: const Text(
                      "Ok",
                      style: TextStyle(color: Colors.green),
                    ),
                  ),
                ],
              );
            },
          );
        } else {
          // 展示其他类型错误
          String errorMsg = 'Terjadi kesalahan';
          if (e is Map<String, dynamic>) {
            errorMsg = e["message"] ?? errorMsg;
          } else {
            errorMsg = e.toString();
          }
          showDialog(
            context: context,
            builder: (ctx) {
              return AlertDialog(
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                title: const Text('Registrasi Gagal'),
                content: Text(errorMsg),
                actions: [
                  TextButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: const Text(
                      "Ok",
                      style: TextStyle(color: Colors.green),
                    ),
                  ),
                ],
              );
            },
          );
        }
      }
    }
  },
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.green,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(20.0),
    ),
    minimumSize: const Size(double.infinity, 50.0),
  ),
  child: const Text('Register'),
),

关键修正点

  1. Service层:明确返回类型为Future<Map<String, dynamic>?>,捕获DioException后抛出服务端错误数据或自定义网络错误;
  2. View层:
    • 使用空安全操作符?访问data["message"],避免null报错;
    • 将校验错误判断条件改为e is Map<String, dynamic> && e.containsKey("errors"),匹配API返回结构;
    • 处理data为null的情况,抛出明确错误信息;
    • 优化错误信息提取逻辑,确保不同类型错误都能展示合理提示。

内容的提问来源于stack exchange,提问作者M. Abdul Rahman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 14:22:03