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

纯Flutter MVC加载更多示例问题:模型中记录未更新

问题分析与解决方案

核心问题

  • UI未随模型数据更新:首次加载和加载更多后,模型数据变化但未触发视图重绘,界面无更新。
  • ScrollController职责混乱:模型中持有ScrollController,违反MVC职责分离原则,且未与视图关联,导致加载更多的条件判断失效。
  • MVC分层不清晰:控制器仅转发调用,未承担数据变化通知职责;模型缺少数据更新的回调机制。

优化后的代码实现

1. 模型层(LoadMoreModelMVC)

移除ScrollController,添加数据更新回调,聚焦数据操作逻辑:

import 'package:http/http.dart' as http;
import 'dart:convert';

import '../config/config.dart';

class LoadMoreModelMVC {
  int _page = 0;
  final int _limit = 20;
  bool _hasNextPage = true;
  bool _isFirstLoadRunning = false;
  bool _isLoadMoreRunning = false;
  List _records = [];
  final _baseUrl = ApiEndpoint.loadMoreUrl;
  
  // 数据更新回调,通知外部数据变化
  Function()? onDataUpdated;

  // Getters
  List get records => _records;
  bool get hasNextPage => _hasNextPage;
  bool get isFirstLoadRunning => _isFirstLoadRunning;
  bool get isLoadMoreRunning => _isLoadMoreRunning;
  
  Future<void> firstLoad() async {
    _isFirstLoadRunning = true;
    _notifyUpdate();
    try {
      final res = await http.get(Uri.parse("$_baseUrl?_page=$_page&_limit=$_limit"));
      _records = json.decode(res.body);      
    } catch (err) {
      print('首次加载失败: $err');
    }
    _isFirstLoadRunning = false;
    _notifyUpdate();
  }

  Future<void> loadMore() async {
    if (_hasNextPage && !_isFirstLoadRunning && !_isLoadMoreRunning) {
      _isLoadMoreRunning = true;
      _notifyUpdate();
      _page += 1;
      try {
        final res = await http.get(Uri.parse("$_baseUrl?_page=$_page&_limit=$_limit"));
        final List fetchedRecords = json.decode(res.body);
        if (fetchedRecords.isNotEmpty) {
          _records.addAll(fetchedRecords);
        } else {
          _hasNextPage = false;
        }
      } catch (err) {
        print('加载更多失败: $err');
        _page -=1; // 加载失败回退页码
      }
      _isLoadMoreRunning = false;
      _notifyUpdate();
    }
  }

  Future<void> refresh() async {
    _page = 0;
    _hasNextPage = true;
    _records = [];
    await firstLoad();
  }

  // 触发数据更新通知
  void _notifyUpdate() {
    onDataUpdated?.call();
  }
}

2. 控制器层(LoadMoreControllerMVC)

承担模型与视图的中介职责,统一管理数据更新通知:

import '../models/LoadMoreModelMVC.dart';

class LoadMoreControllerMVC {
  final LoadMoreModelMVC model = LoadMoreModelMVC();
  
  // 注册数据更新监听
  void addDataListener(Function() listener) {
    model.onDataUpdated = listener;
  }

  // 移除监听
  void removeDataListener() {
    model.onDataUpdated = null;
  }

  Future<void> fetchFirstRecords() async {
    await model.firstLoad();
  }

  Future<void> fetchMoreRecords() async {
    await model.loadMore();
  }

  Future<void> refreshData() async {
    await model.refresh();
  }
}

3. 视图层(LoadMoreViewMVC)

负责UI渲染与交互,监听数据变化并触发重绘:

import 'package:flutter/material.dart';
import '../../controllers/controllers.dart';
import '../../widgets/widgets.dart';

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

  @override
  State<LoadMoreViewMVC> createState() => _LoadMoreViewMVCState();
}

class _LoadMoreViewMVCState extends State<LoadMoreViewMVC> {
  final LoadMoreControllerMVC controller = LoadMoreControllerMVC();
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    // 监听数据变化,触发UI刷新
    controller.addDataListener(() {
      if(mounted) setState(() {});
    });
    // 首次加载数据
    controller.fetchFirstRecords();
    // 添加滚动监听
    _scrollController.addListener(_loadMore);
  }

  @override
  void dispose() {
    controller.removeDataListener();
    _scrollController.removeListener(_loadMore);
    _scrollController.dispose();
    super.dispose();
  }

  void _loadMore() {
    // 滚动到底部附近时触发加载更多
    if (_scrollController.position.extentAfter < 300 && 
        controller.model.hasNextPage && 
        !controller.model.isLoadMoreRunning) {
      controller.fetchMoreRecords();
    }
  }

  Widget _buildList() {
    // 首次加载中显示加载框
    if (controller.model.isFirstLoadRunning) {
      return const Center(child: CircularProgressIndicator());
    }

    // 数据为空且未加载时显示空状态
    if (controller.model.records.isEmpty && !controller.model.isFirstLoadRunning) {
      return const Center(child: Text("暂无数据"));
    }

    return ListView.builder(
      controller: _scrollController,
      itemCount: controller.model.records.length + 
          (controller.model.hasNextPage && controller.model.isLoadMoreRunning ? 1 : 0),
      itemBuilder: (_, index) {
        if (index == controller.model.records.length) {
          return const Padding(
            padding: EdgeInsets.symmetric(vertical: 16),
            child: Center(child: CircularProgressIndicator()),
          );
        } else {
          final record = controller.model.records[index];
          return Card(
            margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
            child: ListTile(
              title: Text(record['title']),
              subtitle: Text(record['body']),
            ),
          );
        }
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: const MoonLightDrawer("Load More MVC"),
      appBar: const Navbar(title: "Load More MVC"),
      body: RefreshIndicator(
        onRefresh: controller.refreshData,
        child: _buildList(),
      ),
    );
  }
}

关键优化说明

  • 数据更新通知:模型通过回调通知控制器,视图监听后调用setState()触发重绘,解决数据更新不显示的问题。
  • 职责分离:移除模型中的ScrollController,由视图管理,符合MVC中视图负责UI组件的原则。
  • 分层清晰:控制器统一管理状态通知,视图专注UI渲染,模型专注数据逻辑。
  • 错误处理:加载更多失败时回退页码,避免无效请求;补充空状态、加载状态的合理展示。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 05:47:04