Flutter

6. GoRouter

모리선생 2023. 3. 15. 23:16
728x90

목표

앱/웹에서 사용한 라우팅 라이브러리인 GoRouter를 사용하여 Dynamic Link, Deep Link, Redirect, Refresh툴 등 유용한 기능의 존재를 확인하고 인증 시스템을 구성해본다.


*해당 내용은 본인의 개발 공부에 있어서 여러가지 참고하며 기록했던 내용을 복기하고 이해하고자 작성한 블로그 글입니다. 같이 공부를 해나가며 Flutter라는 언어에 취미를 가지신 분 들이 개념을 잡는데 도움이 되었으면 합니다. 참고한 홈페이지 등에 대해서는 각 페이지별 하단에 명시하여 두었습니다. 수정 및 문의 사항이 있으면 알려주시길 바랍니다.

 

정의

Flutter에서 GoRouter는 Flutter 앱 내에 쉽게 페이지 전환 및 라우팅을 관리하기 위한 패키지이다. 해당 패키지는 Flutter의 내장 패키지인 Navigator를 대신하여 사용할 수 있으며, 빠르고 간단한 페이지 전환 기능을 구현할 수 있도록 도와준다.

 

추가설명

이름과 경로를 사용해서 원하는 화면으로 쉽게 이동할 수 있도록 도와주는 라이브러리 이며, GoRouter를 통해 다른 화면 위에 새로운 화면을 쌓거나 (Push), 그리고 다른 화면으로 넘어가기 (Go)등을 할 수 있다.

 

특징

Flutter에서는 화면을 위젯이라고 하는 작은 부품으로 만든다. 그리고 그 위젯들을 트리라고 하는 나무 모양으로 배열한다. GoRouter는 이 트리를 바꿔주면서 화면을 바꾸어주는데, 예를 들어, A화면에서 B화면으로 넘어갈 때, A위젯을 빼고 B위젯을 넣어줄 수 있다. 그리고 이 과정에 애니메이션 같은 재미있는 효과를 줄 수도 있다. GoRouter는 더 많은 기능도 있으며, 예를 들어, 인터넷 주소(URL)로 화면을 찾거나, 화면에 정보(매개 변수)를 전달하거나, 여러 개의 화면을 겹치거나(중첩) 할 수 있다.

 

장점

코드가 간결하고 유지보수가 쉬우며, 대규모 어플리케이션의 페이지 전환과 라우팅을 관리하는것이 어려울 시 가독성과 유지보수성을 향상하기 위해 사용 중이다


사용법

 

Go router 설정

MaterialApp 위젯을 MaterialApp.router로 변경

GoRouter 객체 타입의 _router를 정의 후 routerConfig에 추가: GoRouter 페이지 각각 추가

‘/’ route의 접근은 HomePage

‘/classA’와 ‘/classB’ 접근은 각각 ClassA와 ClassB로 연결 구성

 

실제 코드

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

final GoRouter _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
   
    GoRoute(
      path: '/classA',
      builder: (context, state) => ClassA(),
    ),
   
    GoRoute(
      path: '/classB',
      builder: (context, state) => ClassB(),
    ),
  ],
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      title: 'Go router',
    );
  }
}

페이지 간 이동하기

다음의 두 메서드를 적용하여 이동이 가능하다

1. context.go(): 현재 페이지를 대체하는 새로운 페이지로 렌더링

    이전 페이지로 이동하기 위해서는 별도의 UI 필요

2. context.push(): 현재 페이지 위에 새로운 페이지를 적층

 

전달인자를 작성할 때는 GoRouter 앞에서 정의한 URL를 적어준다

주의점

    문자열 앞에 꼭 '/'를 같이 입력한다.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar : AppBar (
        title: const Text('HomePage'),
      ),
      body : Center(
        child : Column(
          mainAxisAlignment : MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => context.go('/classA'),
              child: const Text('classA'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => context.push('/classB'),
              child: const Text('classB'),
            ),
          ], 
        ),
      ),
    );
  }
}

구현 예시

 

서브루트

페이지간 트리 구조가 연결되어 있는 경우 subroute를 구성하는 코드

즉, 앞서 보여준 예시의 classA나 classB아래에 서브루트를 작성하여 path를 만들어주면 됨 (studentA, studentB)

여기서는 subrouter 앞에 path 앞에 ‘/’(슬래시)를 입력하지 않음

 

즉, 페이지간 트리 구조가 연결되어 있는 경우 subroute를 구성하는 코드

final GoRouter _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
   
    GoRoute(
      path: '/classA',
      builder: (context, state) => ClassA(),
      routes: [
        GoRoute(
          path : 'studentA',
          builder: (context, state) => StudentA(),  
        ),
       
      ],
    ),
   
    GoRoute(
      path: '/classB',
      builder: (context, state) => ClassB(),
    ),
  ],
);
class ClassA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar : AppBar (
        title: const Text('ClassA'),
      ),
      body : Center(
        child : Column(
          mainAxisAlignment : MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => context.go('/classA/studentA'),
              child: const Text('studentA'),
            ),
          ],   
        ),
      ),
    );
  }
}

class StudentA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar : AppBar (
        title: const Text('StudentA'),
      ),
      body : Center(
        child : Column(
          mainAxisAlignment : MainAxisAlignment.center,
          children: [
            const Text('This is Student A')
          ],   
        ),
      ),
    );
  }
}

 

해당 상태에서 ClassA 페이지의 student A 버튼을 클릭하게 되면 context.go에 적힌 것처럼 /classA/studentA로 이동을 하게된다. 이때 context.go를 이용해서 이동했음에도 뒤로 가기 버튼이 화면에 나타나는 것으로 보아 subroute는 context.push와 같은 효과를 나타탤 수 있음을 알수 있다.

 

화면 예시

Named Router

 

정의
URL이 길어지고 복잡해지는 경우 각각의 router 별로 별명을 설정하고 이를 대신하여 사용하는 방법이 있는데 이를 Named Router라고 한다.

 

사용법
context.go() 대신에 context.pushNamed()로 변경

 

차이점
student A에 접근하기 위해서 경로를 모두 써줬던 기존의 방식에서와는 다르게 Name Router의 경우 설정한 name을 작성하여주면 된다.

final GoRouter _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
   
    GoRoute(
      path: '/classA',
      builder: (context, state) => ClassA(),
      routes: [
        GoRoute(
          path : 'studentA',
          builder: (context, state) => StudentA(),  
        ),
       
      ],
    ),
   
    GoRoute(
      name : 'classB',
      path: '/classB',
      builder: (context, state) => ClassB(),
      routes : [
        GoRoute(
          name: 'studentB',
          path: 'studentB',
          builder: (context, state) => StudentB(),
         
        ),
      ],
    ),
  ],
);


class ClassB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar : AppBar (
        title: const Text('ClassB'),
      ),
      body : Center(
        child : Column(
          mainAxisAlignment : MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => context.pushNamed('studentB'), // 이렇게 설정한 이름을 적어준다.
              child: const Text('studentB'),
            ),
          ],   
        ),
      ),
    );
  }
}

Go Router를 사용해서 딥링크나 다이나믹 링크를 구현할 수 있을까?

 

딥링크
일반적으로 검색 가능하거나 특정 웹 컨텐츠로 연결하도록 혹은 특정 화면에 도달할 수 있도록 하이퍼링크를 사용하는 것이다. 다른 말로 말하자면 어떤 화면을 바로 보여주는 주소(URL)을 말한다. iOS에서는 유니버설 링크라는 방식으로 기능을 제공한다. 사용자가 해당 URL을 클릭할 시 관련 앱 등을 보여주고 사용자가 다음 행동을 선택할 수 있도록 한다.

 

만약, 딥링크를 다른 사람에게 전달했을때 그 사람이 특정 앱을 깔지 않아 볼수가 없다면 앱스토어로 이동할 거이다. 이렇게 앱을 깔아도 원래 보려고 했던 화면으로 가게 해주는 주소를 지연된 딥링크 (deferred deep link)라고 한다.

 

딥링크 (안드로이드)에서 흐름
링크를 클릭하면 앱의 실행이라는 단계를 거친 뒤 해당 화면으로 이동이 가능하다.

다이나믹 링크
구글 파이어베이스에서 제공하는 서비스이며 딥링크와 유사하다. 하지만 딥링크를 포함하는 개념으로 보다 포괄적인 상황을 의미한다. 한가지의 링크를 통해서 다양한 상황에서 각자 다른 동작 그리고 동작의 연계성을 추구할 수 있다. 다이나믹 링크 동작 예시는 다음의 다이어그램에서 확인가능하다

 

다이나믹 링크의 사용방법 (Flutter)

예제 참고 (‘Seong-Am Kim’)

 

사전준비

> Firebase 프로젝트 생성
Dynamic Links에 도메인 등록
Flutter에서 Firebase의 설정
firebase dynamic links 설치

 

다이나믹 링크 설정

안드로이드

> 일반적인 딥링크 수신을 위한 설정과 동일하다.
수신을 위해 android/app/src/main/AndroidManifest.xml 파일에 인텐트 필터 설치
다음과 같은 코드를 입력한다 (이때 example.app 부분을 앞서 Dynamic Link에서 등록한 도메인을 쓴다.).

...
<activity android:name=".MainActivity" ...>
<!--For Dynamic links-->
<intent-filter>
  <action android:name="android.intent.action.VIEW"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <category android:name="android.intent.category.BROWSABLE"/>
  <data
      android:host="example.app"
      android:scheme="https"/>
</intent-filter>
</activity>

 

iOS
> Universal Link 등록
ios/Runner/Runner.entitlements 파일을 아래처럼 수정한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.developer.associated-domains</key>
  <array>
    <string>applinks:example.app</string>
  </array>
</dict>
</plist>

example.app 부분을 앞서 Dynamic Links에 등록한 도메인을 적는다.

 

커스텀 도메인을 등록했다면 ios/Runner/Info.plist 파일에 아래처럼 추가한다.

...
<key>FirebaseDynamicLinksCustomDomains</key>
<array>
  <string>https://example.app/share</string>
</array>
...

 

다이나믹 링크 생성 (짧은 동적링크 버전)

  1. uriPrefix에 해당하는 부분은 설정 도메인이다
  2. link에서는 도메인을 통해 특정 컨텐츠에 접근하기 위하여 URL 생성을 하도록 한다.
  3. 스크린이름을 path로 설정하였고 id라는 쿼리 파라미터는 설정해 아이디별로 다른 화면이 보이도록 설정하였다.
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';

Future<String> getShortLink(String screenName, String id) async {
  String dynamicLinkPrefix = 'https://example.app/share';
  final dynamicLinkParams = DynamicLinkParameters(
    uriPrefix: dynamicLinkPrefix, // 설정 도메인
    link: Uri.parse('$dynamicLinkPrefix/$screenName?id=$id'), // 특정 컨텐츠에 접근하기 위한 URL 생성
    androidParameters: const AndroidParameters(
      packageName: packageName,
      minimumVersion: 0,
    ),
    iosParameters: const IOSParameters(
      bundleId: packageName,
      minimumVersion: '0',
    ),
  );
  final dynamicLink =
      await FirebaseDynamicLinks.instance.buildShortLink(dynamicLinkParams);

  return dynamicLink.shortUrl.toString();
}

링크의 공유
1. 명령어 설치
        ‘flutter pub add share_plus’을 terminal에 입력
2. 코드
        짧은 다이나믹 링크 URL을 공유하기 위한 코드

Share.share(
await getShortLink(
  artist,
  _artist.id!,
),
);

수신된 다이나믹 링크로 목표 컨텐츠 화면으로 이동
1. 다이나믹링크 수신방법을 두가지 상황으로 나누어 진행할 수 있다.
    앱이 실행중인 경우
    앱이 실행중이 아닌 경우
2. 앱이 실행중인 경우
    리스너를 등록 하여 다이나믹 링크 수신시 콜백 URL을 동작시킨다.
    이후 _redirectScreen에서 원하는 화면으로 네비게이션을 바꿔준다.

FirebaseDynamicLinks.instance.onLink.listen((
PendingDynamicLinkData dynamicLinkData,
) {
_redirectScreen(dynamicLinkData);
}).onError((error) {
logger.e(error);
});
3. 앱이 종료된 경우
    다이나믹 링크를 가져오기 위해 별도의 라이브러리를 설치한다
        flutter pub add uni_links
    이후 아래 코드를 작성하여 앱 종류시에도 다이나믹 링크를 수신할 수 있도록 한다
Future<bool> _getInitialDynamicLink() async {
final String? deepLink = await getInitialLink();

if (deepLink != null) {
  PendingDynamicLinkData? dynamicLinkData = await FirebaseDynamicLinks
      .instance
      .getDynamicLink(Uri.parse(deepLink));

  if (dynamicLinkData != null) {
    _redirectScreen(dynamicLinkData);

    return true;
  }
}

return false;
}

 

이제 전체 코드를 확인하여 각 부분이 어떻게 구성되었는지 다시 확인해보자.

import 'package:artcalendar/models/enums.dart';
import 'package:artcalendar/screens/artist_screen.dart';
import 'package:artcalendar/screens/exhibition_detail_screen.dart';
import 'package:artcalendar/screens/exhibitor_screen.dart';
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:get/get.dart';
import 'package:uni_links/uni_links.dart';

import 'constants.dart';
import 'logger.dart';

class DynamicLink {
  Future<bool> setup() async {
    bool isExistDynamicLink = await _getInitialDynamicLink();
    _addListener();

    return isExistDynamicLink;
  }

  Future<bool> _getInitialDynamicLink() async {
    final String? deepLink = await getInitialLink();

    if (deepLink != null) {
      PendingDynamicLinkData? dynamicLinkData = await FirebaseDynamicLinks
          .instance
          .getDynamicLink(Uri.parse(deepLink));

      if (dynamicLinkData != null) {
        _redirectScreen(dynamicLinkData);

        return true;
      }
    }

    return false;
  }

  void _addListener() {
    FirebaseDynamicLinks.instance.onLink.listen((
      PendingDynamicLinkData dynamicLinkData,
    ) {
      _redirectScreen(dynamicLinkData);
    }).onError((error) {
      logger.e(error);
    });
  }

  void _redirectScreen(PendingDynamicLinkData dynamicLinkData) {
    if (dynamicLinkData.link.queryParameters.containsKey('id')) {
      String link = dynamicLinkData.link.path.split('/').last;
      String id = dynamicLinkData.link.queryParameters['id']!;

      switch (link) {
        case exhibition:
          Get.offAll(
            () => ExhibitionDetailScreen(
              mainBottomTabIndex: MainBottomTabScreenType.exhibitionMap.index,
            ),
            arguments: {
              "exhibitionId": id,
            },
          );
          break;
        case artist:
          Get.offAll(
            () => ArtistScreen(),
            arguments: {
              "artistId": id,
            },
          );
          break;
        case exhibitor:
          Get.offAll(
            () => ExhibitorScreen(),
            arguments: {
              "exhibitorId": id,
            },
          );
          break;
      }
    }
  }

  Future<String> getShortLink(String screenName, String id) async {
    final dynamicLinkParams = DynamicLinkParameters(
      uriPrefix: dynamicLinkPrefix,
      link: Uri.parse('$dynamicLinkPrefix/$screenName?id=$id'),
      androidParameters: const AndroidParameters(
        packageName: packageName,
        minimumVersion: 0,
      ),
      iosParameters: const IOSParameters(
        bundleId: packageName,
        minimumVersion: '0',
      ),
    );
    final dynamicLink =
        await FirebaseDynamicLinks.instance.buildShortLink(dynamicLinkParams);

    return dynamicLink.shortUrl.toString();
  }
}

코드앱 시작시 DynamicLinks().setup();으로 호출해주면 Dynamic Links가 수신되어 특정 컨텐츠 화면으로 이동한다.


그외의 툴에 대해서도 알아보자.

 

Pop 함수

(참고 dohy-9443.log: 해당 글은 쉽게 설명을 하니 참조를 하도록 하자)

: 뒤로가기 기능이다.

import 'package:go_router/go_router.dart';

...

ElevatedButton(
  onPressed: () {
    context.pop();
  },
  child: const Text('home (pop)')
),

ErrorScreen 함수

(참고 dohy-9443.log: 해당 글은 쉽게 설명을 하니 참조를 하도록 하자)

: 테스트용으로 error_screen.dart를 만들어서 error page 확인을 해보자.

 

에러페이지 만들기

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_study/layout/default_layout.dart';

class ErrorScreen extends StatelessWidget {
  final String error;
  const ErrorScreen({super.key, required this.error});

  @override
  Widget build(BuildContext context) {
    return DefaultLayout(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(error),
          ElevatedButton(
            onPressed: () {
              context.go('/');
            },
            child: const Text('HOME')
          )
        ]
      ),
    );
  }
}
main dart에서 Navigation 진행시 일어나는 에러를 적어놓는다.
...
initialLocation: '/',
// error가 나면 작성한 error page로 이동
errorBuilder: (context, state) {
  return ErrorScreen(error: state.error.toString());
},

routes: [
...
home_screen.dart에서 고의적으로 없는 url를 지정하여 이동하는 버튼을 만들어보자.
ElevatedButton(
  onPressed: () {
    context.go('error');
  },
  child: const Text('error page test')
),

 

Redirect와 Refresh

Refresh를 설명하자면 새로운 화면을 보여주는 것으로, A화면에서 B화면으로 넘어갈시 다시 A화면으로 넘어오면 A 화면에 새로운 정보가 업데이트되어 있는 것이다. GoRouter에서 Refresh를 할때에는 context를 사용하며, 이는 앱의 상태나 위치를 나타낸다.

 

(참고 dohy-9443.log: 해당 글은 쉽게 설명을 하니 참조를 하도록 하자)

: 특정 변화가 존재할 시 변화를 감지하여 자동으로 라우팅을 변경하는 기능이다.

  • 시작하기 전에 준비를 하기 위해 model과 provider를 만든다 (여기서는 rivderpod을 추가한다).

pubspec.yaml

flutter_riverpod: ^2.1.3

 

user_model.dart

class UserModel {
  final String name;

  UserModel({ required this.name });
}

 

auth_provider.dart를 생성 후 main.dart에서 작성했던 router를 옮김

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_study/screen/error_screen.dart';
import 'package:go_router_study/screen/home_screen.dart';
import 'package:go_router_study/screen/one_screen.dart';
import 'package:go_router_study/screen/three_screen.dart';
import 'package:go_router_study/screen/two_screen.dart';

final routerProvider = Provider<GoRouter>((ref) {
  return GoRouter(
    initialLocation: '/',
    errorBuilder: (context, state) {
      return ErrorScreen(error: state.error.toString());
    },
    routes: [
      GoRoute(
        path: '/',
        builder: (_, state) => HomeScreen(),
        routes: [
          GoRoute(
            path: 'one',
            builder: (_, state) => OneScreen(),
            routes: [
              GoRoute(
                path: 'two',
                builder: (_, state) => TwoScreen(),
                routes: [
                  // http://.../one/two/three
                  GoRoute(
                    path: 'three',
                    name: ThreeScreen.routeName,
                    builder: (_, state) => ThreeScreen(),
                  )
                ]
              )
            ]
          ),
        ]
      ),
      // http://.../three
      // GoRoute(
      //   path: 'three',
      //   builder: (_, state) => ThreeScreen(),
      // )
    ]
  );
});

 

이후 main.dart를 consumerWidget으로 변경

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router_study/provider/auth_provider.dart';

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends ConsumerWidget {
  MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context , WidgetRef ref) {
    final router = ref.watch(routerProvider);
    return MaterialApp.router(
      // route 정보 전달
      routeInformationProvider: router.routeInformationProvider,
      // URI String을 상태 및 Go Router에서 사용할 수 있는 형태로 변환해주는 함수
      routeInformationParser: router.routeInformationParser,
      // 위에서 변경된 값으로 실제로 어떤 라우트를 보여줄지 정하는 함수
      routerDelegate: router.routerDelegate,
    );
  }
}

 

준비단계 완료되었으니 다음 단계로 넘어가자.

 

다음단계

: refresh 사용 준비

changeNotifier를 사용하거나 Stream을 사용한다

redirect 로직과 refresh 로직 그리고 route 정보들을 changeNotifier에 삽입.

auth_provider.dart

...

// 여기다가 route정리
class AuthNotifier extends ChangeNotifier {
  List<GoRoute> get _route => [
    GoRoute(
      path: '/',
      builder: (_, state) => HomeScreen(),
      routes: [
        GoRoute(
          path: 'one',
          builder: (_, state) => OneScreen(),
          routes: [
            GoRoute(
              path: 'two',
              builder: (_, state) => TwoScreen(),
              routes: [
                // http://.../one/two/three
                GoRoute(
                  path: 'three',
                  name: ThreeScreen.routeName,
                  builder: (_, state) => ThreeScreen(),
                )
              ]
            )
          ]
        ),
      ]
    ),
  ];
}

 

그리고 AuthNotifier을 가져오기 위해, 다음의 코드 삽입

...

final routerProvider = Provider<GoRouter>((ref) {
  final authStateProvider = AuthNotifier();

  return GoRouter(
    initialLocation: '/',
    errorBuilder: (context, state) {
      return ErrorScreen(error: state.error.toString());
    },
    // redirect
    // refresh를 사용할라면 changeNotifier를 사용해야된다. 또는 Stream
    // redirect로직이랑 refresh로직이랑 route 정보들을 따로 changeNotifier에다가 집어넣는다.

    routes: authStateProvider._route
  );
});

...

 

user 관련 코드 작성

...
class UserStateNotifier extends StateNotifier<UserModel?> {
  UserStateNotifier(): super(null);

  login({ required String name }) {
    state = UserModel(name: name); // 로그인하면 이름을 넣고
  }

  logout() {
    state = null; // 로그아웃하면 null로 바꾼다.
  }
}

 

‘?’을 삽입한 이유는 로그인에서 UserModel 인스턴스 상태로 넣어주고, 로그아웃에서는 null 상태로 넣어주기 위함.

 

그 후 UserStateNotifier  상단에 userProvider를 작성한다.

...

final userProvider = StateNotifierProvider<UserStateNotifier, UserModel?>(
  (ref) => UserStateNotifier()
);

class UserStateNotifier extends StateNotifier<UserModel?> {

...

 

그리고 이 userProvider를 AuthNotifier에서 사용하기 위해서는 ref를 받을 준비를 하는 코드를 삽입한다.

class AuthNotifier extends ChangeNotifier {
  final Ref ref;

  AuthNotifier({ required this.ref });

...

 

final routerProvider = Provider<GoRouter>((ref) {
  final authStateProvider = AuthNotifier(ref: ref);
...

 

이후 AuthNotifier의 실행문을 작성한다.

AuthNotifier({ required this.ref }) {
  // listen했을 때 반환받을 거는 UserModel 또는 null
  ref.listen<UserModel?>(userProvider, (previous, next) {
    // 반환받으면 - 상태가 변경됐다는거만 알려줄 것이다.
    // notifyListeners(); 이것은 state가 바꼈다는걸 감지하는 것으로 추측이 되며
    // ChangeNotifier 얘를 바라보고있는 위젯들이 리빌드하라고 알려주는 데 이는 notifyListeners 얘가
    // 결국 state 변경하는거랑 같다고 보면 된다

    // 그럼 언제값이 변경되는가라고 보았을때
    // if (기존값이랑 다음값이 다르면) {}
    if (previous != next) {
      notifyListeners();
    }
  });
}

 

그 다음 redirect 함수를 작성한다.

......

// 이제 Redirect 로직을 작성하자.

// redirect 함수는 페이지를 이동할 때마다 동작한다

// 반환값을 이동할 route 이기때문에 String이기 때문이다
String? _redirectLogic(GoRouterState state) {
  // 이거 만들면 GoRouterState state 이거 주는데 여기에는 현재 라우팅에 정보 , 경로 등등 다 준다고한다.

  // 1. 유저의 현재 상태를 알고있어야하며,
  // UserModel의 인스턴스(로그인 상태) 또는 null(로그아웃 상태)
  final user = ref.read(userProvider);

  // 2. 유저가 로그인을 하려는 상태인지 (로그인 페이지라면 로그인을 하려는 상태인지 알 수 있음)
  final loggingIn = state.location == '/login';

  // 3. 로직 작성
  // if (유저정보가 null) <- 로그인한 상태가 아니고
  // { 유저정보가 없고 로그인하려는 중이 아니라면 로그인페이지로 이동한다. }
  if (user == null) {
    // null이면 이동하려던 곳 그대로 이동한다.
    return loggingIn ? null : '/login';
  }

  // 유저 정보가 있는데 로그인페이지라면? : 홈으로 이동
  if (loggingIn) {
    return '/';
  }

  // 위 두조건 둘다아니면 너 가고싶은데로 가
  return null;
}
...

 

작성 후 GoRouter에서 해당 코드를 입력한다.

initialLocation: '/',
errorBuilder: (context, state) {
  return ErrorScreen(error: state.error.toString());
},
// redirect
redirect: authStateProvider._redirectLogic,

 

해당상태로는 에러가 발생하니 _redirectLogic 함수에 파라미터를 추가한다.

String? _redirectLogic(_, GoRouterState state) {

 

이제 로그인 페이지를 추가한다.

  1. 로그인 페이지
  2. 경로 추가
  3. initialLocation 경로 변경
  4. home_screen.dart 에 버튼 추가

 

(1)

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router_study/layout/default_layout.dart';
import 'package:go_router_study/provider/auth_provider.dart';

class LoginScreen extends ConsumerWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return DefaultLayout(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          ElevatedButton(
            onPressed: () {
              ref.read(userProvider.notifier).login(name: 'Code Factory');
            },
            child: const Text('Login')
          ),
          ElevatedButton(
            onPressed: () {
              context.go('/one');
            },
            child: const Text('one으로 이동해보기')
          ),
        ]
      ),
    );
  }
}

 

(2)

GoRoute(
  path: '/login',
  builder: (_, state) => LoginScreen(),
)

 

(3)

initialLocation: '/login',

 

(4)

ElevatedButton(
  onPressed: () {
    context.go('/login');
  },
  child: const Text('login test')
),
ElevatedButton(
  onPressed: () {
    ref.read(userProvider.notifier).logout();
  },
  child: const Text('logout test')
),

 

참고

  1. 평범한개발자 (유튜브) https://www.youtube.com/watch?v=PrHIa1htAHo
  2. Kodeco https://www.kodeco.com/flutter/paths/flutter-fundamentals
  3. 바쁜일상속작은틈비집기 https://pitching-gap.tistory.com/53
  4. 일상, 기록 https://skyfox83.tistory.com/567
  5. Seong-Am Kim https://jay-flow.medium.com/flutter-dynamic-links-를-활용하여-deep-link-구현하기-ce41cb9baf2c
  6. dohy-9443.log https://velog.io/@dohy-9443/Go-Router-이론

 

728x90