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

Flutter报错:缺少MaterialApp祖先组件致Dialog无法显示

Flutter中MultiProvider+ModularApp架构下showDialog缺少MaterialApp祖先的问题解决

问题描述

在使用MultiProvider和ModularApp架构的Flutter应用中,尽管AppWidget里已经嵌套了MaterialApp.router,但在AuthViewModel中调用showDialog时,仍然触发"No MaterialLocalizations found"错误,提示缺少MaterialApp祖先组件。

简化后的代码结构

main.dart

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  runApp(
    ModularApp(
      module: AppModule(),
      child: const AppWidget(),
    ),
  );
}

AppModule.dart

class AppModule extends Module {
  // ... (模块配置)
}

AppWidget.dart

class AppWidget extends StatelessWidget {
  const AppWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    LocalJsonLocalization.delegate.directories = ['lib/assets/i18n'];

    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) => ChatNotificationService(),
        ),
        ChangeNotifierProvider(
          create: (_) => AuthUseCase(),
        ),
        ChangeNotifierProvider(
          create: (context) => AuthViewModel(
            AuthUseCase(),
            context,
          ),
        ),
      ],
      child: MaterialApp.router(
        theme: ThemeData(
            fontFamily: 'Lato',
            colorScheme: ColorScheme.fromSwatch().copyWith(
              primary: const Color(0xFF0096C7),
              secondary: Colors.orange.shade700,
              tertiary: Colors.white,
              outline: Colors.grey,
            )),
        debugShowCheckedModeBanner: false,
        scrollBehavior: AppScrollBehavior(),
        title: 'app_name'.i18n(),
        localizationsDelegates: [
          GlobalMaterialLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
          LocalJsonLocalization.delegate
        ],
        supportedLocales: const [
          Locale('pt', 'BR'),
          Locale('en', 'US'),
        ],
        routerConfig: Modular.routerConfig,
      ),
    );
  }
}

AuthViewModel.dart

class AuthViewModel extends ChangeNotifier {
  final AuthUseCase _authUseCase;
  final BuildContext context;

  AuthViewModel(this._authUseCase, this.context);

  bool _isLoading = false;

  Future<void> login(String email, String password) async {
    try {
      _isLoading = true;
      notifyListeners();

      final errorMessage = await _authUseCase.login(email, password);

      if (errorMessage != null) {
        _showErrorDialog(errorMessage);
      } else {
        Modular.to.navigate('/home/');
      }
    } on AuthException catch (error) {
      _handleAuthException(error);
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
  
  void _showErrorDialog(String msg) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text('error_occurred'.i18n()),
        content: Text(msg),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('close'.i18n()),
          ),
        ],
      ),
    );
  }

  void _handleAuthException(AuthException error) {
    // 处理认证异常
  }
}

LoginScreen.dart

Future<void> _submit() async {
  final isValid = _formKey.currentState?.validate() ?? false;

  if (!isValid) {
    return;
  }

  _formKey.currentState?.save();

  AuthViewModel authViewModel =
      Provider.of<AuthViewModel>(context, listen: false);

  await authViewModel.login(
    _formData.email,
    _formData.password,
  );
}

错误信息

E/RecaptchaCallWrapper( 4976): Initial task failed for action RecaptchaAction(action=signUpPassword)with exception - The email address is badly formatted.
D/InsetsController( 4976): show(ime(), fromIme=true)
E/flutter ( 4976): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: No MaterialLocalizations found.
E/flutter ( 4976): _InheritedProviderScope<AuthViewModel?> widgets require MaterialLocalizations to be provided by a Localizations widget ancestor.
E/flutter ( 4976): The material library uses Localizations to generate messages, labels, and abbreviations.
E/flutter ( 4976): To introduce a MaterialLocalizations, either use a MaterialApp at the root of your application to include them automatically, or add a Localization widget with a MaterialLocalizations delegate.
E/flutter ( 4976): The specific widget that could not find a MaterialLocalizations ancestor was:
E/flutter ( 4976):   _InheritedProviderScope<AuthViewModel?>
E/flutter ( 4976): The ancestors of this widget were:
E/flutter ( 4976):   ChangeNotifierProvider<AuthViewModel>
E/flutter ( 4976):   _NestedHook
E/flutter ( 4976):   _InheritedProviderScope<AuthUseCase?>
E/flutter ( 4976):   ChangeNotifierProvider<AuthUseCase>
E/flutter ( 4976):   _NestedHook
E/flutter ( 4976):   _InheritedProviderScope<ChatNotificationService?>
E/flutter ( 4976):   ChangeNotifierProvider<ChatNotificationService>
E/flutter ( 4976):   _NestedHook
E/flutter ( 4976):   MultiProvider
E/flutter ( 4976):   AppWidget
E/flutter ( 4976):   _ModularInherited
E/flutter ( 4976):   ModularApp
E/flutter ( 4976):   MediaQuery
E/flutter ( 4976):   _MediaQueryFromView
E/flutter ( 4976):   _ViewScope
E/flutter ( 4976):   View-[GlobalObjectKey FlutterView#70e35]
E/flutter ( 4976):   [root]
E/flutter ( 4976): #0      debugCheckHasMaterialLocalizations.<anonymous closure> (package:flutter/src/material/debug.dart:93:7)
E/flutter ( 4976): #1      debugCheckHasMaterialLocalizations (package:flutter/src/material/debug.dart:113:4)
E/flutter ( 4976): #2      showDialog (package:flutter/src/material/dialog.dart:1287:10)
E/flutter ( 4976): #3      AuthViewModel._showErrorDialog (package:estudo/src/feature/auth/viewmodel/auth_view_model.dart:82:5)

原因分析

错误核心在于**AuthViewModel初始化时传入的context属于MultiProvider层级,而MultiProvider在MaterialApp.router的上层**。MaterialApp才是提供MaterialLocalizations(以及Navigator、Theme等Material组件依赖的上下文)的组件,所以这个上层context无法访问到MaterialLocalizations资源,调用showDialog时自然会报错。

另外,AuthViewModel持有BuildContext本身就不符合MVVM的设计原则,ViewModel不应该依赖UI层的上下文,容易引发内存泄漏(比如ViewModel生命周期长于页面时)。

解决方案

方案1:ViewModel不持有Context,由UI层处理Dialog(推荐)

修改ViewModel,移除Context依赖,通过状态通知UI层显示Dialog,遵循单一职责原则:

修改AuthViewModel

class AuthViewModel extends ChangeNotifier {
  final AuthUseCase _authUseCase;
  String? _errorMessage;
  bool _isLoading = false;

  String? get errorMessage => _errorMessage;
  bool get isLoading => _isLoading;

  AuthViewModel(this._authUseCase);

  Future<void> login(String email, String password) async {
    try {
      _isLoading = true;
      _errorMessage = null;
      notifyListeners();

      final errorMessage = await _authUseCase.login(email, password);

      if (errorMessage != null) {
        _errorMessage = errorMessage;
      } else {
        Modular.to.navigate('/home/');
      }
    } on AuthException catch (error) {
      _errorMessage = error.message;
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  // 清除错误信息,避免重复显示Dialog
  void clearErrorMessage() {
    _errorMessage = null;
    notifyListeners();
  }
}

修改LoginScreen,监听错误状态并显示Dialog

@override
Widget build(BuildContext context) {
  return Scaffold(
    // ... 其他UI代码
    body: Consumer<AuthViewModel>(
      builder: (context, viewModel, child) {
        // 使用addPostFrameCallback确保在UI构建完成后显示Dialog
        if (viewModel.errorMessage != null) {
          WidgetsBinding.instance.addPostFrameCallback((_) {
            _showErrorDialog(context, viewModel.errorMessage!);
            viewModel.clearErrorMessage();
          });
        }

        return Form(
          key: _formKey,
          // ... 表单内容
        );
      },
    ),
  );
}

// 把Dialog显示逻辑移到UI层
void _showErrorDialog(BuildContext context, String msg) {
  showDialog(
    context: context,
    builder: (ctx) => AlertDialog(
      title: Text('error_occurred'.i18n()),
      content: Text(msg),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(ctx).pop(),
          child: Text('close'.i18n()),
        ),
      ],
    ),
  );
}

方案2:调整Provider位置,传入正确的Context

如果坚持在ViewModel中处理Dialog,需要将AuthViewModel的Provider移到MaterialApp内部,确保初始化时传入的Context是MaterialApp的后代:

修改AppWidget.dart

class AppWidget extends StatelessWidget {
  const AppWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    LocalJsonLocalization.delegate.directories = ['lib/assets/i18n'];

    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => ChatNotificationService()),
        ChangeNotifierProvider(create: (_) => AuthUseCase()),
      ],
      child: MaterialApp.router(
        // ... 原有配置
        routerConfig: Modular.routerConfig,
        // 使用builder将AuthViewModel的Provider嵌套在MaterialApp内部
        builder: (context, child) {
          return ChangeNotifierProvider(
            create: (context) => AuthViewModel(
              Provider.of<AuthUseCase>(context, listen: false),
              context, // 此时context包含MaterialLocalizations
            ),
            child: child,
          );
        },
      ),
    );
  }
}

注意:这种方法虽然能解决问题,但ViewModel持有Context存在内存泄漏风险,不推荐长期使用。

内容的提问来源于stack exchange,提问作者Guilherme Queiroz Ribeiro

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 08:09:53