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

Flutter Widget测试:调用notifyListeners()后页面未更新问题排查

问题:Flutter Widget测试中点击按钮后UI未更新

编写Flutter Widget测试时,需验证按钮点击前后的计数器数值:初始页面正确显示0,但点击按钮后页面未更新为1。已确认notifyListeners()被调用,但Widget未按预期重建。计数器递增逻辑在服务类中,使用Mockito模拟该类,使其在按钮点击时返回1,但UI无变化。

相关代码

计数器页面代码

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

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  late ICounterService _testService;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _testService = Provider.of<ICounterService>(context);
    _testService.addListener(() {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_testService.testValue.toString()),
      ),
      body: Center(
        child: FilledButton(
          onPressed: () {
            _testService.incrementValue(null);
          },
          child: const Text('Change value'),
        ),
      ),
    );
  }
}

测试文件代码

void main() {
  late ICounterService mockCounterService;

  setUp(() {
    mockCounterService = MockICounterService();

    when(mockCounterService.testValue).thenReturn(0);
    when(mockCounterService.incrementValue(null)).thenAnswer((_) {
      when(mockCounterService.testValue).thenReturn(1);
      mockCounterService.notifyListeners();
    });
  });

  testWidgets('Widget tests for counter page', (tester) async {
    await tester.pumpWidget(
      MultiProvider(
        providers: [
          ChangeNotifierProvider<ICounterService>(
            create: (_) => mockCounterService,
          ),
        ],
        child: const MaterialApp(
          home: CounterPage(),
        ),
      ),
    );

    expect(find.text('0'), findsOneWidget);

    var changeValueText = find.text('Change value');
    expect(changeValueText, findsOneWidget);

    await tester.tap(changeValueText);
    await tester.pumpAndSettle();

    expect(find.text('1'), findsOneWidget);
  });
}

服务实现代码

import 'package:counter_application/interfaces/i_counter_service.dart';
import 'package:flutter/material.dart';

class CounterService extends ChangeNotifier implements ICounterService {
  int _testValue = 0;

  @override
  int get testValue => _testValue;

  @override
  void incrementValue(int? value) {
    _testValue += value ?? 1;
    this.notifyListeners();
  }
}

问题原因

  1. ICounterService未继承ChangeNotifier:ICounterService是独立抽象类,未继承ChangeNotifier,导致Mockito生成的MockICounterService无法拥有完整的监听逻辑——调用notifyListeners()时,不会触发之前通过addListener注册的回调,Widget收不到更新通知。
  2. 重复添加监听:didChangeDependencies会被多次触发,每次都会新增监听回调,虽不是测试失败的直接原因,但会引发不必要的重复重建。

修复方案

1. 修改ICounterService继承ChangeNotifier

让抽象类继承ChangeNotifier,确保Mock对象能正确处理监听逻辑:

abstract class ICounterService extends ChangeNotifier {
  int get testValue;
  void incrementValue(int? value);
}

2. 优化CounterPage的监听方式(二选一)

方式一:使用Provider自动监听(推荐)

移除手动添加的listener,直接在build方法中通过Provider.of获取服务并开启监听,Provider会自动处理状态更新和Widget重建:

class _CounterPageState extends State<CounterPage> {
  @override
  Widget build(BuildContext context) {
    // listen: true 为默认值,自动监听状态变化
    final counterService = Provider.of<ICounterService>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text(counterService.testValue.toString()),
      ),
      body: Center(
        child: FilledButton(
          onPressed: () {
            counterService.incrementValue(null);
          },
          child: const Text('Change value'),
        ),
      ),
    );
  }
}

方式二:手动管理监听(避免重复注册)

若需手动管理监听,在initState中注册并在dispose中移除,确保只注册一次:

class _CounterPageState extends State<CounterPage> {
  late ICounterService _testService;
  VoidCallback? _updateListener;

  @override
  void initState() {
    super.initState();
    // 延迟获取Provider,避免initState中context不可用问题
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _testService = Provider.of<ICounterService>(context, listen: false);
      _updateListener = () => setState(() {});
      _testService.addListener(_updateListener!);
    });
  }

  @override
  void dispose() {
    if (_updateListener != null) {
      _testService.removeListener(_updateListener!);
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_testService.testValue.toString()),
      ),
      body: Center(
        child: FilledButton(
          onPressed: () {
            _testService.incrementValue(null);
          },
          child: const Text('Change value'),
        ),
      ),
    );
  }
}

3. 验证测试逻辑

完成上述修改后,重新运行测试:mock对象的notifyListeners()会正确触发Widget重建,build时会读取到新的testValue=1,expect(find.text('1'), findsOneWidget)会通过。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 06:47:12