카테고리 없음

[flutter] Splash페이지 - Go router

STUFIT 2024. 5. 9. 22:36
반응형

하아.... 일단 내가 구현하고 싶었던 것은, 앱을 최초 실행시키면 스플래시 페이지가 뜨고 fade아웃 처리 후에 로그인 화면이 뜨는 것이었다.

그런데 내가 router 설정을 잘못한 것인지 계속 에러가 발생하였다.

에러 및 해당 코드를 살펴보자면

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kakao_flutter_sdk/kakao_flutter_sdk.dart';
import 'package:provider/provider.dart';
import 'package:url_strategy/url_strategy.dart';
import 'package:wallet_worrior/router/app_router.dart';
import 'package:wallet_worrior/src/model/app_bar/app_bar_page_model.dart';
import 'package:wallet_worrior/src/state/login/login_state.dart';
import 'package:wallet_worrior/src/view/screens/login/login_page.dart';
import 'package:wallet_worrior/src/view/screens/login/splash_page.dart';


void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await dotenv.load(fileName: "assets/config/.env");
  KakaoSdk.init(
    nativeAppKey: dotenv.get('KAKAO_NATIVE_APP_KEY'),
    javaScriptAppKey: dotenv.get('KAKAO_JAVASCRIPT_KEY'),
  );
  setPathUrlStrategy();
  runApp(const AppRoot());
}
class AppRoot extends StatelessWidget {
  const AppRoot({super.key});


  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => LoginState()),
        ChangeNotifierProvider(create: (context)=>AppBarPageModel()),
      ],
      child: MyApp(), // MyApp이 이제 MultiProvider의 자식입니다.
    );
  }
}


class MyApp extends StatefulWidget {
  const MyApp({super.key});
<main.dart>

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // bool _isSplashVisible = true;
  //
  // @override
  // void initState() {
  //   super.initState();
  //   Timer(Duration(seconds: 3), () {
  //     setState(() {
  //       _isSplashVisible = false;
  //     });
  //   });
  // }

  @override
  Widget build(BuildContext context) {
    final loginState = Provider.of<LoginState>(context, listen: false);
    final router = AppRouter.createRouter(loginState);
    return ScreenUtilInit(
      designSize: const Size(430,932),
      minTextAdapt: true,       // 텍스트 크기를 자동으로 조정하여 화면에 맞추는 기능을 활성화
      splitScreenMode: true,    // 분할 화면 모드를 활성화
      builder: (context,child){
        return MaterialApp.router(
          routerConfig: router,
          debugShowCheckedModeBanner: false,
          builder: (BuildContext context, Widget? child) {
            return SplashPage();
          },
        );
      },
    );
  }
}

<app_router.dart>

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:wallet_worrior/enum/login_platform.dart';
import 'package:wallet_worrior/src/state/login/login_state.dart';
import 'package:wallet_worrior/src/view/screens/login/login_page.dart';
import 'package:wallet_worrior/src/view/screens/login/sign_up_page.dart';
import 'package:wallet_worrior/src/view/screens/login/splash_page.dart';
import 'package:wallet_worrior/src/view/screens/login/term_page.dart';
import 'package:wallet_worrior/src/view/screens/user/user_detail_page.dart';
import 'package:wallet_worrior/src/view/screens/user/user_list_page.dart';

class AppRouter {
  static const String splashPath = '/splash';
  static const String loginPath = '/login';
  static const String termPath = '/term';
  static const String userListPath = '/user/list';
  static const String userDetailsPath = '/user/details/:userNo';
  static const String signUpPath = '/signup';
  static const String mapPath = '/map';
  static const String searchPath = '/searchPage';

  static GoRouter createRouter(LoginState loginState) {
    return GoRouter(
      refreshListenable: loginState,
      initialLocation: splashPath,
      routes: [
        _goRoute(splashPath,const SplashPage()),
        _goRoute(loginPath, const LoginPage()),
        _goRoute(userListPath, const UserListPage()),
        _goRoute(termPath, TermPage()),
        _goRoute(signUpPath, SignUpPage()), // 회원가입 페이지 라우트 추가
        GoRoute(
          path: userDetailsPath,
          builder: (context, state) {
            final userNo = int.parse(state.pathParameters['userNo']!);
            return UserDetailsPage(userNo: userNo);
          },
        ),
      ],
      redirect: (context, state) => _redirectLogic(loginState, state),
    );
  }

  static String? _redirectLogic(LoginState loginState, GoRouterState state) {
    final isLoggedin = loginState.platform != LoginPlatform.none;
    final isAtLogin = state.uri.path == loginPath;
    final isAtTerm = state.uri.path == termPath;
    print(
        '소셜:${loginState.platform},맴버:${loginState.isMember}, 로그인:${isLoggedin}, 고잉로그인:${isAtLogin}');

    return null;
  }

  static GoRoute _goRoute(String path, Widget view) =>
      GoRoute(path: path, builder: (context, state) => view);
}


<splash_page.dart>

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:wallet_worrior/router/app_router.dart';
import 'package:wallet_worrior/src/state/login/login_state.dart';

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

  @override
  State<SplashPage> createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateMixin {
  AnimationController? _animationController;
  Animation<double>? _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 1000),
      vsync: this,
    );
    _animation = CurvedAnimation(
      parent: _animationController!,
      curve: Curves.easeOut,
    );
    // 애니메이션이 바로 시작되도록 설정
    _animationController!.forward();
    // 3초 후에 페이드 아웃 시작


    Future.delayed(const Duration(seconds: 3), () {
      print("dhdn");
      WidgetsBinding.instance.addPostFrameCallback((_) {
        print("dydl");
        if (mounted) {
          print("호이?");
          context.go(AppRouter.loginPath);
        }
      });
    });

  }
  
  @override
  void dispose() {
    _animationController?.dispose();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _animation!,
      child: Scaffold(
        body: Container(
            color: Colors.redAccent,
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Image.asset(
                    'assets/images/imoji.png',
                    width: 300,
                  ),
                  const SizedBox(
                    height: 50,
                  ),
                  const Text(
                    "매일 나의 지갑을 지켜보는\n전사가 되어보자!",
                    style: TextStyle(
                      fontFamily: 'BMJUA',
                      color: Colors.white,
                      fontSize: 25,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  const SizedBox(
                    height: 50,
                  ),
                  const CircularProgressIndicator(
                    valueColor: AlwaysStoppedAnimation(
                      Colors.white,
                    ),
                  ),
                ],
              ),
            )),
      ),
    );
  }

}

위의 코드로 실행시켰을 시 발생 에러는

======== Exception caught by scheduler library =====================================================
The following assertion was thrown during a scheduler callback:
No GoRouter found in context
'package:go_router/src/router.dart':
Failed assertion: line 507 pos 12: 'inherited != null'

When the exception was thrown, this was the stack: 
#2      GoRouter.of (package:go_router/src/router.dart:507:12)
#3      GoRouterHelper.go (package:go_router/src/misc/extensions.dart:25:16)
#4      _SplashPageState.initState.<anonymous closure>.<anonymous closure> (package:wallet_worrior/src/view/screens/login/splash_page.dart:41:19)
#5      SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1386:15)
#6      SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1322:11)
#7      SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1169:5)
#8      _invoke (dart:ui/hooks.dart:312:13)
#9      PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:399:5)

이렇게 inherited !=null 이라는 에러가 발생하였다.

인터넷을 뒤적거려본 결과 initState에서는 build가 이뤄지지 않아서 context를 호출할 수 없기 때문에 goRouter를 사용할 수 없다고 하였었는데,  이 때

WidgetsBinding.instance.addPostFrameCallback

를 사용하면 해결된다고 하여 집어넣었지만 마찬가지였다.

그러던 찰나 main.dart에서 

 builder: (BuildContext context, Widget? child) {
            return SplashPage();
          },

이 부분을 주석처리해보니 잘 돌아가는게 아닌가?!!!!!

gpt의 답변은 이러하다.

Flutter의 MaterialApp.router에서 builder를 사용할 때 발생하는 문제를 이해하는 것은 Flutter의 위젯 트리 구조와 컨텍스트 시스템을 고려하는 것이 필수적입니다.

builder를 사용할 때 주어진 context는 MaterialApp.router 위젯 자체가 제공하는 컨텍스트입니다.

이 컨텍스트는 아직 MaterialApp.router의 자식 위젯들에 의해 추가된 다른 기능들, 예를 들어 Navigator 같은 것들을 포함하고 있지 않습니다.

문제의 원인

  1. 컨텍스트의 스코프: builder 내에서 반환된 SplashPage는 MaterialApp.router가 제공하는 context 하에서 생성됩니다. 이 컨텍스트는 아직 Navigator와 같은 라우팅 관련 정보를 포함하지 않습니다. 따라서, SplashPage 내에서 context를 사용하여 라우팅 관련 작업을 하려고 하면, 필요한 Navigator 위젯을 찾을 수 없어 에러가 발생합니다.
  2. 위젯 트리의 위치: builder가 SplashPage를 직접 반환하면, SplashPage는 MaterialApp.router의 직접적인 자식으로서 위치하게 됩니다. 이는 SplashPage가 Navigator 위젯이 포함된 context에서 분리되어 구성되어, 필요한 Navigator를 찾을 수 없게 됩니다.

해결 방법

  • builder를 사용하지 않고, SplashPage를 home 또는 초기 라우트로 직접 설정하거나, 라우터 설정에서 적절히 관리하는 것이 좋습니다. 이렇게 하면 SplashPage가 MaterialApp.router에 의해 자동으로 제공되는 context를 사용할 수 있게 되어, Navigator를 포함한 모든 필요한 위젯에 정상적으로 접근할 수 있습니다.
  • builder를 사용해야 하는 경우는, 해당 builder 내에서 라우팅이나 Navigator에 의존하는 작업을 하지 않도록 주의해야 합니다. 대신, 상태 관리 라이브러리나 테마 설정과 같은 다른 목적으로 사용하면 적합합니다.

예를 들어, SplashPage home 속성을 사용하여 설정하거나, GoRouter 초기 설정에서 관리할 있습니다:

 

위의 이유 때문에 builder를 다시 반환하는 것을 지우니 초기 라우터 설정값인 splash 페이지가 작동하는 것을 확인할 수 있었다.

반응형