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

如何用服务器获取的AssetImage路径填充Flutter卡片轮播

Flutter 卡片轮播加载用户专属图片问题修复

问题背景

原本使用预定义本地图片路径实现的卡片轮播功能正常,现在需要从Node.js接口获取当前用户的专属卡片图片路径,改用PageView实现后功能失效。

Node.js 服务代码

static async getCarddata(userId) {
    try {
        const cardData = await CardModel.find({ userId }, { cardImage: 1, _id: 0 });
        return cardData;
    } catch (error) {
        throw error;
    }
}

接口返回示例

{
    "status": true,
    "success": [
        {
            "cardImage": "assets/GameCard_01.png" 
        },
        {
           "cardImage": "assets/GameCard_02.png" 
        }
    ]
}

现有Flutter页面代码

class CardsDisplayPage extends StatefulWidget {
  final String token;

  const CardsDisplayPage({Key? key, required this.token}) : super(key: key);

  @override
  _CardsDisplayPageState createState() => _CardsDisplayPageState();
}

class _CardsDisplayPageState extends State<CardsDisplayPage> {
  List<String> cardImages = [];

  int _currentPage = 0;
  late PageController _pageController;
  late String phoneNumber;

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

  void _pageListener() {
    setState(() {
      _currentPage = _pageController.page!.toInt() % cardImages.length;
    });
  }

  void fetchCardImages() async {
    try {
      // 解析token获取用户ID
      final decodedToken = jsonDecode(widget.token);
      final userId = decodedToken['userId'];

      final response = await http.post(
        Uri.parse(getUserCardList),
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
          'Authorization': 'Bearer ${widget.token}',
        },
        body: jsonEncode(<String, dynamic>{
          'userId': userId,
        }),
      );

      if (response.statusCode == 200) {
        List<dynamic> responseData = jsonDecode(response.body)['success'];
        setState(() {
          cardImages =
              responseData.map((card) => card['cardImage'] as String).toList();
        });
      } else {
        throw Exception('Failed to load card images');
      }
    } catch (e) {
      print('Error fetching card images: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Builder(
        builder: (BuildContext context) {
          return Center(
            child: Column(
              children: [
                SizedBox(
                  height: MediaQuery.of(context).size.height * 0.06,
                ),
                topAppBar(token: widget.token),
                SizedBox(height: MediaQuery.of(context).size.height * 0.05),
                SizedBox(
                  height: 255.0,
                  child: Stack(
                    children: [
                      PageView.builder(
                        controller: _pageController,
                        itemBuilder: (context, index) {
                          return GestureDetector(
                            child: Container(
                              decoration: BoxDecoration(
                                image: DecorationImage(
                                  image: AssetImage(
                                    cardImages[index % cardImages.length],
                                  ),
                                ),
                              ),
                            ),
                          );
                        },
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 10.0),
                Padding(
                  padding: const EdgeInsets.only(bottom: 20.0),
                  child: SizedBox(
                    height: 10.0,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: List.generate(
                        cardImages.length,
                        (index) => _buildDot(index),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          );
        },
      ),
      endDrawer: const menu(),
    );
  }

  Widget _buildDot(int index) {
    return Container(
      width: 7.5,
      margin: const EdgeInsets.symmetric(horizontal: 5.0),
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: _currentPage == index
            ? const Color.fromARGB(255, 20, 120, 202)
            : Color.fromARGB(255, 223, 223, 223),
      ),
    );
  }
}

问题分析与修复方案

核心问题

  1. PageView.builder 未指定itemCount:无明确数量会导致无限创建item,空数组时访问索引直接报错。
  2. 初始加载空数组风险:接口请求完成前cardImages为空,此时访问数组元素触发索引越界。
  3. 页面监听的除以0错误:空数组时cardImages.length为0,取模运算抛出异常。

修复后的代码

class CardsDisplayPage extends StatefulWidget {
  final String token;

  const CardsDisplayPage({Key? key, required this.token}) : super(key: key);

  @override
  _CardsDisplayPageState createState() => _CardsDisplayPageState();
}

class _CardsDisplayPageState extends State<CardsDisplayPage> {
  List<String> cardImages = [];
  bool _isLoading = true;
  int _currentPage = 0;
  late PageController _pageController;

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

  void _pageListener() {
    if (cardImages.isEmpty) return;
    setState(() {
      _currentPage = (_pageController.page?.toInt() ?? 0) % cardImages.length;
    });
  }

  void fetchCardImages() async {
    try {
      final decodedToken = jsonDecode(widget.token);
      final userId = decodedToken['userId'];

      final response = await http.post(
        Uri.parse(getUserCardList),
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
          'Authorization': 'Bearer ${widget.token}',
        },
        body: jsonEncode(<String, dynamic>{
          'userId': userId,
        }),
      );

      if (response.statusCode == 200) {
        List<dynamic> responseData = jsonDecode(response.body)['success'];
        setState(() {
          cardImages = responseData.map((card) => card['cardImage'] as String).toList();
          _isLoading = false;
        });
      } else {
        throw Exception('Failed to load card images');
      }
    } catch (e) {
      print('Error fetching card images: $e');
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  void dispose() {
    _pageController.removeListener(_pageListener);
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Builder(
        builder: (BuildContext context) {
          return Center(
            child: Column(
              children: [
                SizedBox(height: MediaQuery.of(context).size.height * 0.06),
                topAppBar(token: widget.token),
                SizedBox(height: MediaQuery.of(context).size.height * 0.05),
                // 加载状态与空数据处理
                _isLoading 
                    ? const SizedBox(height: 255, child: Center(child: CircularProgressIndicator()))
                    : cardImages.isEmpty
                        ? const SizedBox(height: 255, child: Center(child: Text('暂无卡片')))
                        : SizedBox(
                            height: 255.0,
                            child: PageView.builder(
                              controller: _pageController,
                              itemCount: cardImages.length,
                              itemBuilder: (context, index) {
                                return GestureDetector(
                                  child: Container(
                                    decoration: BoxDecoration(
                                      image: DecorationImage(
                                        image: AssetImage(cardImages[index]),
                                        fit: BoxFit.cover,
                                      ),
                                    ),
                                  ),
                                );
                              },
                            ),
                          ),
                const SizedBox(height: 10.0),
                // 仅当有卡片时显示指示器
                if (!_isLoading && cardImages.isNotEmpty)
                  Padding(
                    padding: const EdgeInsets.only(bottom: 20.0),
                    child: SizedBox(
                      height: 10.0,
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: List.generate(
                          cardImages.length,
                          (index) => _buildDot(index),
                        ),
                      ),
                    ),
                  ),
              ],
            ),
          );
        },
      ),
      endDrawer: const menu(),
    );
  }

  Widget _buildDot(int index) {
    return Container(
      width: 7.5,
      margin: const EdgeInsets.symmetric(horizontal: 5.0),
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: _currentPage == index
            ? const Color.fromARGB(255, 20, 120, 202)
            : const Color.fromARGB(255, 223, 223, 223),
      ),
    );
  }
}

关键修复点

  • 新增_isLoading状态,加载时显示进度指示器,避免空数组报错。
  • 给PageView.builder添加itemCount,明确轮播卡片数量。
  • 页面监听中增加数组非空判断,防止除以0错误。
  • 处理空数组场景,显示友好提示。
  • 添加PageController资源释放,避免内存泄漏。
  • 优化图片显示适配,添加fit: BoxFit.cover。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 06:05:59