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

Flutter GetX控制器首次加载接收null而非数据问题排查

Flutter GetX 首次加载API数据为null,刷新后正常的问题排查

问题现象

使用GetX Controller发起GET API请求并跳转新页面后,首次加载时模型接收的是null值,但刷新页面后数据能正确获取并展示,调试日志提示Null check operator used on a null value。


Controller代码

import 'package:app/modules/community/models/post_details_model.dart' as PostDetailsModel;
import 'package:get/get.dart';

import '../../../services/dio/api_service.dart';

class PostDetailsController extends GetxController {
  PostDetailsModel.Data postData = PostDetailsModel.Data();
  final postID = Get.arguments;
  @override
  void onInit() {
    fetchPostDetails(postID);
    super.onInit();
  }

  fetchPostDetails(postID) async {
    try {
      postData = (await APIManager.getPostDetails(postID.toString()))!;
    } catch (error) {
      print('List fetch error => $error');
    }
  }
}

View代码

import 'package:app/helpers/component/custom_elevated_fill_button.dart';
import 'package:app/modules/community/controllers/post_details_controller.dart';
import 'package:app/utils/alfnr_icons.dart';
import 'package:app/utils/app_colors.dart';
import 'package:app/utils/text_style_util.dart';
import 'package:another_carousel_pro/another_carousel_pro.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';

class PostDetailsView extends StatefulWidget {
  const PostDetailsView({Key? key}) : super(key: key);

  @override
  State<PostDetailsView> createState() => _PostDetailsViewSate();
}

class _PostDetailsViewSate extends State<PostDetailsView> {
  @override
  Widget build(BuildContext context) {
    PostDetailsController controller = Get.put(PostDetailsController());
    return Scaffold(
      backgroundColor: ColorUtil.appColorWhite,
      body: SafeArea(
        child: GestureDetector(
          onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
          child: Stack(
            children: [
              SizedBox(
                height: 300.0,
                width: double.infinity,
                child: AnotherCarousel(
                  images: [
                    if (controller.postData.thumbnails != null)
                      for (var image in controller.postData.thumbnails!) NetworkImage(image)
                  ],
                ),
              ),
              CustomScrollView(
                shrinkWrap: true,
                slivers: <Widget>[
                  SliverAppBar(
                    pinned: true,
                    snap: false,
                    floating: false,
                    expandedHeight: 230.h,
                    backgroundColor: Colors.transparent,
                    flexibleSpace: FlexibleSpaceBar(
                      background: AnotherCarousel(
                        images: [
                          if (controller.postData.thumbnails != null)
                            for (var image in controller.postData.thumbnails!) NetworkImage(image)
                        ],
                      ),
                    ),
                  ),
                  SliverFillRemaining(
                    child: SingleChildScrollView(
                      physics: const NeverScrollableScrollPhysics(),
                      child: Container(
                        height: 1200,
                        color: Colors.white,
                        padding: EdgeInsets.all(15.h),
                        alignment: Alignment.center,
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(controller.postData.categoryTitle!,
                                style: TextStyle(
                                    fontSize: 20.sp,
                                    fontWeight: FontWeight.w500,
                                    color: const Color(0xff0A496B),
                                    fontFamily: AppFont.sfProDisplayFontFamily)),
                            const SizedBox(height: 10),
                            Text('Location: ${controller.postData.location}'),
                          ],
                        ),
                      ),
                    ),
                  ),
                ],
              ),
              Align(
                alignment: Alignment.bottomCenter,
                child: Container(
                  color: Colors.white,
                  padding: EdgeInsets.symmetric(vertical: 20.h, horizontal: 20.w),
                  child: CustomElevatedFillButton(
                    buttonText: 'View 25 Subscriptions',
                    onTap: () => {},
                  ),
                ),
              ),
              Container(
                  padding: EdgeInsets.only(top: kToolbarHeight),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: [
                      Column(
                        children: [
                          Container(
                            decoration: BoxDecoration(
                                color: const Color(0xffE85175),
                                borderRadius: BorderRadius.circular(6)),
                            margin: EdgeInsets.only(bottom: 10, right: 10),
                            child: Material(
                              color: Colors.transparent,
                              child: InkWell(
                                onTap: () {},
                                child: const Padding(
                                  padding: EdgeInsets.all(10),
                                  child: Icon(Alfnr.delete, color: Colors.white, size: 18),
                                ),
                              ),
                            ),
                          ),
                          Container(
                            decoration: BoxDecoration(
                                color: const Color(0xff16A9FA),
                                borderRadius: BorderRadius.circular(6)),
                            margin: EdgeInsets.only(bottom: 10, right: 10),
                            child: Material(
                              color: Colors.transparent,
                              child: InkWell(
                                onTap: () {
                                  // Get.toNamed(Routes.EDIT_SERVICES,
                                  //     arguments: {"serviceId": postModel.postID});
                                },
                                child: const Padding(
                                  padding: EdgeInsets.all(10),
                                  child: Icon(Alfnr.edit, color: Colors.white, size: 18),
                                ),
                              ),
                            ),
                          )
                        ],
                      )
                    ],
                  ))
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    Get.delete<PostDetailsController>();
    super.dispose();
  }
}

Binding代码

import 'package:app/modules/community/controllers/post_details_controller.dart';
import 'package:get/get.dart';

class PostDetailsBindings extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => PostDetailsController());
  }
}

API Service代码

static Future<PostDetailsModel.Data?> getPostDetails(postID) async {
    String token = Get.find<GetStorageService>().token;
    Response<dynamic> response = await DioClient(
      isOverlayLoader: false,
    ).get(
      Endpoints.getCommunityPostDetails(postID),
      options: Options(
        headers: {
          'Authorization': "Bearer $token",
        },
      ),
    );
    if (response.statusCode == 200) {
      return PostDetailsModel.postDetailsResponseModelFromJson(response.data.toString()).data;
    } else {
      return null;
    }
  }

调试错误日志

[GETX] GOING TO ROUTE /POSTS_DETAILS
[GETX] Instance "PostDetailsController" has been created
[GETX] Instance "PostDetailsController" has been initialized
Another exception was thrown: Null check operator used on a null value

问题根源及修复方案

核心问题

  1. 状态未响应式更新:postData不是Rx类型,API请求完成后更新数据不会通知View刷新,首次加载时View拿到的是初始空对象,后续刷新时数据已缓存所以能显示。
  2. 空值强制解包风险:代码中使用!强制解包categoryTitle和API返回值,数据未加载或API返回null时触发空指针错误。
  3. View重复创建Controller:build方法中调用Get.put()会每次重建Widget时重新创建Controller,与Binding的lazyPut冲突,应使用Get.find()获取已绑定的实例。

修复步骤

1. 修改Controller为响应式状态

import 'package:app/modules/community/models/post_details_model.dart' as PostDetailsModel;
import 'package:get/get.dart';

import '../../../services/dio/api_service.dart';

class PostDetailsController extends GetxController {
  // 使用Rxn可空响应式对象,自动通知View更新
  final postData = Rxn<PostDetailsModel.Data>();
  final postID = Get.arguments;

  @override
  void onInit() {
    fetchPostDetails(postID);
    super.onInit();
  }

  fetchPostDetails(postID) async {
    try {
      final data = await APIManager.getPostDetails(postID.toString());
      postData.value = data;
    } catch (error) {
      print('List fetch error => $error');
    }
  }
}

2. 修改View,监听状态并避免空值解包

class _PostDetailsViewSate extends State<PostDetailsView> {
  @override
  Widget build(BuildContext context) {
    // 获取Binding中已注册的Controller,避免重复创建
    PostDetailsController controller = Get.find<PostDetailsController>();
    
    return Scaffold(
      backgroundColor: ColorUtil.appColorWhite,
      body: SafeArea(
        child: GestureDetector(
          onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
          child: Obx(() {
            // 数据未加载时显示加载状态
            if (controller.postData.value == null) {
              return const Center(child: CircularProgressIndicator());
            }
            
            final post = controller.postData.value!;
            return Stack(
              children: [
                SizedBox(
                  height: 300.0,
                  width: double.infinity,
                  child: AnotherCarousel(
                    images: post.thumbnails?.map((url) => NetworkImage(url)).toList() ?? [],
                  ),
                ),
                CustomScrollView(
                  shrinkWrap: true,
                  slivers: <Widget>[
                    SliverAppBar(
                      pinned: true,
                      snap: false,
                      floating: false,
                      expandedHeight: 230.h,
                      backgroundColor: Colors.transparent,
                      flexibleSpace: FlexibleSpaceBar(
                        background: AnotherCarousel(
                          images: post.thumbnails?.map((url) => NetworkImage(url)).toList() ?? [],
                        ),
                      ),
                    ),
                    SliverFillRemaining(
                      child: SingleChildScrollView(
                        physics: const NeverScrollableScrollPhysics(),
                        child: Container(
                          height: 1200,
                          color: Colors.white,
                          padding: EdgeInsets.all(15.h),
                          alignment: Alignment.center,
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              // 空值运算符替代强制解包
                              Text(post.categoryTitle ?? '无分类',
                                  style: TextStyle(
                                      fontSize: 20.sp,
                                      fontWeight: FontWeight.w500,
                                      color: const Color(0xff0A496B),
                                      fontFamily: AppFont.sfProDisplayFontFamily)),
                              const SizedBox(height: 10),
                              Text('Location: ${post.location ?? '未填写'}'),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
                // 其余UI组件保持不变
              ],
            );
          }),
        ),
      ),
    );
  }

  @override
  void dispose() {
    // 通过Binding注册的Controller无需手动删除,GetX自动管理生命周期
    super.dispose();
  }
}

3. 确保路由绑定生效

跳转页面时使用GetX路由并绑定Binding,或在路由配置中绑定:

// 方式1:跳转时绑定
Get.toNamed('/POSTS_DETAILS', arguments: postId, binding: PostDetailsBindings());

// 方式2:路由配置中绑定
GetPage(
  name: '/POSTS_DETAILS',
  page: () => const PostDetailsView(),
  binding: PostDetailsBindings(),
),

4. 优化API Service错误处理

非200状态时抛出异常,便于统一错误捕获:

static Future<PostDetailsModel.Data> getPostDetails(postID) async {
    String token = Get.find<GetStorageService>().token;
    Response<dynamic> response = await DioClient(
      isOverlayLoader: false,
    ).get(
      Endpoints.getCommunityPostDetails(postID),
      options: Options(
        headers: {
          'Authorization': "Bearer $token",
        },
      ),
    );
    if (response.statusCode == 200) {
      return PostDetailsModel.postDetailsResponseModelFromJson(response.data.toString()).data!;
    } else {
      throw Exception('获取帖子详情失败:${response.statusCode}');
    }
  }

内容的提问来源于stack exchange,提问作者Jamal Ahmad

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 08:14:58