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

Flutter中PageView与GetX结合的索引及数据展示异常问题

解决Flutter中Stateful Widget结合GetX与带动画PageView的问题

问题背景

我习惯在Stateless Widget中通过GetX控制器结合ListView展示基于Model类的数据,流程为:创建Model类→GetX控制器实现数据获取→GetView构建ListView。但尝试实现带动画的PageView时,无法与自己的Model类结合,且不喜欢使用Stateful Widget。目前在Stateful Widget中实现时遇到以下问题:

  1. 前景图片无法随PageView索引变化更新
  2. 出现IntegerDivisionByZeroException异常
  3. 不清楚Stateful Widget结合GetX控制器与Model类实现带动画PageView的正确方式

以下是我的代码基础及尝试过程:

1. FoodModel类实现

class FoodModel {
  String? foodId;
  String? foodNameEn;
  String? foodNameAr;
  String? kcal;
  String? fats;
  String? carbs;
  String? protein;
  String? createTime;
  String? foodTypeId;
  String? foodImage;

  FoodModel(
      {this.foodId,
      this.foodNameEn,
      this.foodNameAr,
      this.kcal,
      this.fats,
      this.carbs,
      this.protein,
      this.createTime,
      this.foodTypeId,
      this.foodImage});

  FoodModel.fromJson(Map<String, dynamic> json) {
    foodId = json['food_id'];
    foodNameEn = json['food_name_en'];
    foodNameAr = json['food_name_ar'];
    kcal = json['kcal'];
    fats = json['fats'];
    carbs = json['carbs'];
    protein = json['protein'];
    createTime = json['create_time'];
    foodTypeId = json['food_type_id'];
    foodImage = json['food_image'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = <String, dynamic>{};
    data['food_id'] = foodId;
    data['food_name_en'] = foodNameEn;
    data['food_name_ar'] = foodNameAr;
    data['kcal'] = kcal;
    data['fats'] = fats;
    data['carbs'] = carbs;
    data['protein'] = protein;
    data['create_time'] = createTime;
    data['food_type_id'] = foodTypeId;
    data['food_image'] = foodImage;
    return data;
  }
}

2. 尝试的PageView实现(更新后)

我已注入GetX控制器,但前景图片固定在初始索引,背景图片正常变化,且出现除零异常:

// 注入控制器
FoodControllerImp controller = Get.put(FoodControllerImp());

// 获取索引方式
int get _currentIndex=>_currentOffest.round() % controller.food.length;

// 背景图片调用
BgImage(
  currentIndex: _currentIndex,
  food: FoodModel.fromJson(controller.food[_currentIndex]),
  pageOffset: _currentOffest,
),

// 前景图片调用(固定初始索引)
late final FoodModel foodlist = FoodModel.fromJson(controller.food[_currentIndex]);
Image.network(
  "${ApiLinks.foodImg}/${foodlist.foodImage}",
  fit: BoxFit.scaleDown,
),

解决方案

1. 修复前景图片不更新问题

问题出在foodlist的初始化方式:late final只会初始化一次,导致数据固定在初始索引。需每次直接从控制器列表中获取对应索引的Model数据:

  • 移除late final FoodModel foodlist = ...代码
  • 在需要使用数据的位置,直接通过索引获取:
// 在PageView.builder的itemBuilder中
final FoodModel food = FoodModel.fromJson(controller.food[index]);
Image.network(
  "${ApiLinks.foodImg}/${food.foodImage}",
  fit: BoxFit.scaleDown,
)
  • 同时将PageView的itemCount设置为controller.food.length(而非固定的3),确保遍历所有数据。

2. 解决IntegerDivisionByZeroException异常

异常原因是数据未加载完成时,controller.food.length为0,导致取模运算除以0。需在计算索引前判断数据状态:

int get _currentIndex {
  // 数据为空时返回0,避免除零
  if (controller.food.isEmpty) return 0;
  return _currentOffest.round() % controller.food.length;
}

同时在页面构建时,先监听控制器的加载状态,确保数据就绪后再渲染PageView:

@override
Widget build(BuildContext context) {
  return GetBuilder<FoodControllerImp>(
    builder: (controller) {
      if (controller.statusRequest == StatusRequest.loading) {
        return const Center(child: CircularProgressIndicator());
      } else if (controller.food.isEmpty) {
        return const Center(child: Text("暂无数据"));
      }
      return AnimatedBuilder(
        animation: _pageController,
        builder: (context, child) {
          return stackBuild();
        },
      );
    },
  );
}

3. Stateful Widget结合GetX与Model类的正确实现方式

核心思路:用GetX管理数据状态,Stateful Widget仅负责PageView的动画与滚动状态,两者解耦。完整步骤如下:

步骤1:优化控制器数据存储

修改FoodControllerImp的getData方法,直接将接口返回的Map转换为FoodModel列表存储,避免页面中重复调用fromJson:

@override
void getData() async {
  statusRequest = StatusRequest.loading;
  update();
  var response = await categoriesData.getData();
  statusRequest = handlingData(response);
  if(StatusRequest.success == statusRequest){
    if(response['status'] == "success")
    {
      // 直接转换为Model列表
      food = (response['exercise_category'] as List)
          .map((e) => FoodModel.fromJson(e))
          .toList();
    }
    else
    {
      statusRequest = StatusRequest.taskFailure;
    }
  }
  update();
}

步骤2:完整的PageView实现代码

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../data/model/food_model.dart';
import '../../../controller/food_controller.dart';
import '../../../links/api_links.dart';

class FoodSliderBuilder extends StatefulWidget {
  const FoodSliderBuilder({super.key});

  @override
  State<FoodSliderBuilder> createState() => _FoodSliderBuilderState();
}

class _FoodSliderBuilderState extends State<FoodSliderBuilder> {
  late PageController _pageController;
  // 获取全局注入的控制器(若未全局注入,可在initState中用Get.put())
  final FoodControllerImp controller = Get.find<FoodControllerImp>();

  double get _currentOffset {
    bool inited = _pageController.hasClients && 
                  _pageController.position.hasContentDimensions;
    return inited ? _pageController.page! : _pageController.initialPage * 1.0;
  }

  int get _currentIndex {
    if (controller.food.isEmpty) return 0;
    return _currentOffset.round() % controller.food.length;
  }

  @override
  void initState() {
    _pageController = PageController(initialPage: 0);
    super.initState();
  }
  
  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GetBuilder<FoodControllerImp>(
      builder: (controller) {
        if (controller.statusRequest == StatusRequest.loading) {
          return const Center(child: CircularProgressIndicator());
        } else if (controller.food.isEmpty) {
          return const Center(child: Text("暂无食物数据"));
        }
        return AnimatedBuilder(
          animation: _pageController,
          builder: (context, child) {
            return stackBuild();
          },
        );
      },
    );
  }

  Stack stackBuild() {
    final FoodModel _currFood = controller.food[_currentIndex];
    return Stack(
      alignment: Alignment.center,
      children: [
        Positioned(
          top: -Get.width * 0.7,
          child: BgImage(
            currentIndex: _currentIndex,
            food: _currFood,
            pageOffset: _currentOffset,
          ),
        ),
        SafeArea(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Text(
                _currFood.foodNameEn!,
                textAlign: TextAlign.center,
                style: const TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
              ),
            ],
          ),
        ),
        Center(
          child: SizedBox(
            height: Get.width * (450 / 375.0),
            child: PageView.builder(
              controller: _pageController,
              itemCount: controller.food.length,
              itemBuilder: (context, index) {
                double _value = 0.0;
                double vp = 1;
                double scale = max(vp, (_currentOffset - index).abs());
                if (_pageController.position.haveDimensions) {
                  _value = index.toDouble() - (_pageController.page ?? 0);
                  _value = (_value * 0.7).clamp(-1, 1);
                }

                final FoodModel food = controller.food[index];
                return Transform.rotate(
                  angle: pi * _value,
                  child: Padding(
                    padding: EdgeInsets.only(bottom: 200 - scale * 5),
                    child: Image.network(
                      "${ApiLinks.foodImg}/${food.foodImage}",
                      fit: BoxFit.scaleDown,
                    ),
                  ),
                );
              },
            ),
          ),
        ),
      ],
    );
  }
}

class BgImage extends StatelessWidget {
  const BgImage({super.key, required this.currentIndex, required this.food, required this.pageOffset});

  final int currentIndex;
  final FoodModel food;
  final double pageOffset;

  @override
  Widget build(BuildContext context) {
    double _value = 0.0;
    _value = (pageOffset - currentIndex + 1).abs();
    
    return Opacity(
      opacity: 0.6,
      child: Transform.rotate(
        angle: pi + _value + pi / 180,
        child: SizedBox(
          height: Get.height,
          width: Get.width * (450 / 375.0),
          child: Image.network(
            "${ApiLinks.foodImg}/${food.foodImage}",
            fit: BoxFit.fitWidth,
          ),
        ),
      ),
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 20:27:01