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

Flutter:TextField获取焦点后键盘立即消失问题求助

问题:TextField点击后键盘弹出立即消失(ScreenUtil + GoRouter 冲突)

点击TextField准备输入时,弹出的键盘会立即消失。移除ScreenUtil组件后问题解决,将GoRouter替换为自定义路由并移除ScreenUtil也能解决,但需要保留ScreenUtil实现响应式布局。

相关代码

main.dart

import 'package:bloc/bloc.dart';
import 'package:elcampione/presentation/app.dart';
import 'package:elcampione/presentation/bloc_observer.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'data/repositories/repositories.dart';
import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 禁止屏幕旋转
  SystemChrome.setPreferredOrientations(
      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

  Bloc.observer = AppBlocObserver();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  final authenticationRepository = AuthenticationRepository();

  await authenticationRepository.user.first;

  runApp(App(authenticationRepository: authenticationRepository));
}

app.dart

import 'package:elcampione/blocs/auth/auth_bloc.dart';
import 'package:elcampione/presentation/themes/theme.dart';
import 'package:elcampione/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../data/repositories/repositories.dart';

class App extends StatelessWidget {
  const App({
    super.key,
    required AuthenticationRepository authenticationRepository,
  }) : _authenticationRepository = authenticationRepository;


  final AuthenticationRepository _authenticationRepository;

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider<AuthenticationRepository>(
      create: (context) => _authenticationRepository,
      child: BlocProvider(
        create: (_) => AuthBloc(
          authenticationRepository: _authenticationRepository,
        ),
        child: const AppView(),
      ),
    );
  }
}

class AppView extends StatelessWidget {
  const AppView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    final state = context.select((AuthBloc bloc) => bloc.state.status);
    final isAuthenticated = state == AuthStatus.authenticated;
    return ScreenUtilInit(
      builder: (context, child) => MaterialApp.router(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
        darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
        title: 'El Campione',
        locale: const Locale('en'),
        routerConfig: AppRouter.router(isAuthenticated),
      ),
      designSize: const Size(390, 844),
    );
  }
}

app_route_config.dart

import 'package:elcampione/presentation/screens/screens.dart';
import 'package:elcampione/routes/app_route_constants.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class AppRouter {
  static GoRouter router(bool isAuthenticated) {
    GoRouter routes = GoRouter(
      initialLocation: isAuthenticated ? '/home' : '/signin',
      routes: [
        GoRoute(
          name: AppRoutesConstants.home,
          path: '/home',
          builder: (context, state) => const HomeScreen(),
        ),
        GoRoute(
          name: AppRoutesConstants.signIn,
          path: '/signin',
          builder: (context, state) => const SignInScreen(),
        ),
        GoRoute(
          name: AppRoutesConstants.signUp,
          path: '/signup',
          builder: (context, state) => const SignUpScreen(),
        )
      ],
      redirect: (context, state) => !isAuthenticated ? '/signin' : null,
    );
    return routes;
  }
}

解决方案

问题根源在于AppView会随AuthBloc状态变化频繁重建,导致ScreenUtilInit和MaterialApp.router反复初始化,TextField焦点丢失,键盘随之消失。以下是针对性修复方案:

方案1:让GoRouter自行监听Auth状态并处理跳转

修改路由配置,让GoRouter直接监听AuthBloc的状态流,避免AppView因状态变化重建整个MaterialApp:

  1. 新增GoRouterRefreshStream类,用于监听Bloc状态流并通知GoRouter刷新:
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';

class GoRouterRefreshStream extends ChangeNotifier {
  GoRouterRefreshStream(Stream<dynamic> stream) {
    _subscription = stream.listen((_) => notifyListeners());
  }

  late final StreamSubscription<dynamic> _subscription;

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}
  1. 修改AppRouter,移除外部传入的isAuthenticated参数,改为内部监听AuthBloc状态:
import 'package:elcampione/blocs/auth/auth_bloc.dart';
import 'package:elcampione/presentation/screens/screens.dart';
import 'package:elcampione/routes/app_route_constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';

class AppRouter {
  static GoRouter router(BuildContext context) {
    return GoRouter(
      initialLocation: '/',
      routes: [
        GoRoute(
          name: AppRoutesConstants.home,
          path: '/home',
          builder: (context, state) => const HomeScreen(),
        ),
        GoRoute(
          name: AppRoutesConstants.signIn,
          path: '/signin',
          builder: (context, state) => const SignInScreen(),
        ),
        GoRoute(
          name: AppRoutesConstants.signUp,
          path: '/signup',
          builder: (context, state) => const SignUpScreen(),
        )
      ],
      redirect: (context, state) {
        final authStatus = context.read<AuthBloc>().state.status;
        final isAuthenticated = authStatus == AuthStatus.authenticated;
        final isOnAuthPage = state.matchedLocation == '/signin' || state.matchedLocation == '/signup';
        
        // 未认证且不在登录/注册页,跳转登录页
        if (!isAuthenticated && !isOnAuthPage) {
          return '/signin';
        }
        // 已认证且在登录/注册页,跳转首页
        if (isAuthenticated && isOnAuthPage) {
          return '/home';
        }
        return null;
      },
      // 监听AuthBloc状态变化,自动刷新路由
      refreshListenable: GoRouterRefreshStream(context.read<AuthBloc>().stream),
    );
  }
}
  1. 修改AppView,移除状态监听逻辑,直接传递context给路由:
class AppView extends StatelessWidget {
  const AppView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ScreenUtilInit(
      builder: (context, child) => MaterialApp.router(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
        darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
        title: 'El Campione',
        locale: const Locale('en'),
        routerConfig: AppRouter.router(context),
      ),
      designSize: const Size(390, 844),
    );
  }
}

方案2:将ScreenUtilInit移至页面层级

如果不想修改路由逻辑,可以把ScreenUtilInit从AppView移到每个页面的根节点,避免MaterialApp因ScreenUtil重建:

例如在SignInScreen中:

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

  @override
  Widget build(BuildContext context) {
    return ScreenUtilInit(
      designSize: const Size(390, 844),
      builder: (context, child) => Scaffold(
        // 页面内容
        body: ...,
      ),
    );
  }
}

可以封装一个BaseScreen组件统一处理,减少重复代码。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 00:05:53