28 분 소요

1. 머티리얼과 쿠퍼티노 디자인

1) 머터리얼 디자인

(1) 디버그 배너 빼기 - debugShowCheckedModeBanner

MaterialApp(
	debugShowCheckedModeBanner: false
)

(2) 테마 설정하기 - Theme

  • 테마 설정하기
      MaterialApp(
          theme: ThemeData(
              primarySwatch: Colors.pink  // 테마 : 핑크색
          )
      )
    
  • 앱바 색상만 변경하기
      MaterialApp(
          theme: ThemeData(
              primarySwatch: Colors.pink,
              appBarTheme: AppBarTheme(
                  backgroundColor: Colors.orange,
                  foregroundColor: Colors.black
      )))
    

✨ 코드 예시

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primarySwatch: Colors.pink,
          appBarTheme: AppBarTheme(
            backgroundColor: Colors.orange,
            foregroundColor: Colors.black,
          ),
        ),
        home: Scaffold(
            appBar: AppBar(
              title: Text('Test'),
            ),
            body: Center(
                child: Column(
              children: [
                ElevatedButton(onPressed: () {}, child: Text('Button')),
                Checkbox(value: true, onChanged: (value) {}),
                Text('HelloWorld')
              ],
            )),
            floatingActionButton: FloatingActionButton(
                child: Icon(Icons.add), onPressed: () {})));
  }
}

2) 쿠퍼티노 디자인

import 'package:flutter/cupertino.dart'
CupertinoApp()  // material 대체

(1) Platform API 활용하기

  • Platform의 정적 속성(static property)
    • isAndroid : 안드로이드 식별
    • isFuchsia : 푸크시아 식별
    • isIOS : iOS 식별
    • isLinux : 리눅스 식별
    • isMacOS : macOS 식별
    • isWindows : 윈도우 식별
  • 플랫폼 별 다른 UI 적용하기
      Widget platformUI() {
          if (Platform.isIOS) {
              return CupertinoApp()
          } else if (Platform.isAndroid) {
              return MaterialApp()
          }
      }
    

✨ 코드 예시

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:io';

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

class MyApp extends StatelessWidget {
  Widget platformUI() {
    if (Platform.isIOS) {
      return CupertinoApp(
          debugShowCheckedModeBanner: false,
          theme: CupertinoThemeData(brightness: Brightness.light),
          home: CupertinoPageScaffold(
              navigationBar:
                  CupertinoNavigationBar(middle: Text('Cupertino Title')),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  CupertinoButton(
                    onPressed: () {},
                    child: Text('click'),
                  ),
                  Center(child: Text('HelloWorld'))
                ],
              )));
    } else if (Platform.isAndroid) {
      return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
              appBar: AppBar(
                title: Text('Material Title'),
              ),
              body: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  ElevatedButton(onPressed: () {}, child: Text('click')),
                  Center(
                    child: Text('HelloWorld'),
                  )
                ],
              )));
    } else {
      return Text('unKnown Device',
          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 25));
    }
  }

  @override
  Widget build(BuildContext context) {
    return platformUI();
}}

3) 기기 모양에 대처하기

Scaffold(
	body: SafeArea(
		child: SingleChildScrollView(
			child: Column(
				children: getWidgets(),
))))

✨코드 예시

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  List<Widget> getWidgets() {
    List<Widget> widgets = [];
    for (var i = 0; i < 100; i++) {
      widgets.add(ListTile(title: Text('Hello World Item $i')));
    }
    return widgets;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        home: Scaffold(
            body: SafeArea(
          child: SingleChildScrollView(
              child: Column(
            children: getWidgets(),
          )),
        )));
  }
}

4) 스캐폴드 위젯

(1) 앱바 - appBar

  • 구성 요소 설정
    • leading : 왼쪽에 출력할 위젯
    • title : 타이틀 위젯
    • actions : 오른쪽에 사용자 이벤트를 위한 위젯들
    • bottom : 앱바 하단을 구성하기 위한 위젯
    • flexibleSpace : 앱바 상단과 하단 사이의 여백을 구성하기 위한 위젯
  • 앱바 출력하기
      Scaffold(
          appBar : AppBar(
              bottom : PreferredSize(
                  preferredSize : const Size.fromHeight(48.0),
                  child : Theme(
                      data: ThemeData.from(
                          colorScheme: ColorScheme.fromSwatch(accentColor: Colors.white)
                      ),
                      child: Container(
                          height: 48.0,
                          alignment : Alignment.center,
                          child : Text('AppBar Bottom Text')),
                  ),
                  flexibleSpace: Container(
                      decoration: BoxDecoration(
                          image: DecorationImage(
                              image: AssetImage('images/big.jpg'),
                              fit: BoxFit.fill)
                          )
                      ),
                      title : Text('AppBar Title'),
                      action : <Widget>[
                          IconButton(
                              icon : const Icon(Icons.add_alert),
                              onPressed : () {},
                          ),
                          IconButton(
                              icon : const Icon(Icons.phone),
                              onPressed : () {},
                  )])
      )))
    

(2) 하단 내비게이션 바 - bottomNavigationBar

Scaffold(
  BottomNavigationBar : BottomNavigationBar(
    type: BottomNavigationBarType.shifting,   // fixed, shifting
    item: <BottomNavigationBarItem>[
      BottomNavigationBarItem(
        icon: Icon(Icons.home),
        label: 'First',
        backgroundColor: Colors.green
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.business),
        label: 'Second',
        backgroundColor: Colors.red
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.school),
        label: 'Third',
        backgroundColor: Colors.purple
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.school),
        label: 'Fourth',
        backgroundColor: Colors.pink
      )
    ],
    currentIndex: _selectedIndex,
    selectedItemColor : Colors.amber[800],
    onTap : _onItemTapped
))

(3) 드로어 - drawer

Scaffold(
	drawer: Drawer(   // endDrawer (오른쪽으로 열리게 하기)
		child: ListView(
			padding: EdgeInsets.zero,
			children: <Widget>[
				DrawerHeadr(),
				ListTile(),
				ListTile()
])))

✨코드 예시

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  MyAppState createState() => MyAppState();
}
  
class MyAppState extends State<MyApp> {
  int _selectedIndex = 0;
  List<Widget> _widgetOptions = <Widget>[
    Text(
      'First Screen',
      style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
    ),
    Text(
      'Second Screen',
      style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
    ),
    Text(
      'Third Screen',
      style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
    ),
    Text(
      'Fourth Screen',
      style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
    ),
  ];
  
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        home: Scaffold(
            appBar: AppBar(
                bottom: PreferredSize(
                  preferredSize: const Size.fromHeight(48.0),
                  child: Theme(
                    data: ThemeData.from(
                        colorScheme:
                            ColorScheme.fromSwatch(accentColor: Colors.white)),
                    child: Container(
                        height: 48.0,
                        alignment: Alignment.center,
                        child: Text('AppBar Bottom Text')),
                  ),
                ),
                flexibleSpace: Container(
                  decoration: BoxDecoration(
                      image: DecorationImage(
                          image: AssetImage('images/big.jpg'),
                          fit: BoxFit.fill)),
                ),
                title: Text('AppBar Title'),
                actions: <Widget>[
                  IconButton(
                      onPressed: () {}, icon: const Icon(Icons.add_alert)),
                  IconButton(onPressed: () {}, icon: const Icon(Icons.phone))
                ]),
            body: Center(
              child: _widgetOptions.elementAt(_selectedIndex),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: () {},
              child: const Icon(Icons.add),
            ),
            bottomNavigationBar: BottomNavigationBar(
              type: BottomNavigationBarType.shifting,
              // fixed, shifting
              items: <BottomNavigationBarItem>[
                BottomNavigationBarItem(
                    icon: Icon(Icons.home),
                    label: 'First',
                    backgroundColor: Colors.green),
                BottomNavigationBarItem(
                    icon: Icon(Icons.business),
                    label: 'Second',
                    backgroundColor: Colors.red),
                BottomNavigationBarItem(
                    icon: Icon(Icons.school),
                    label: 'Third',
                    backgroundColor: Colors.purple),
                BottomNavigationBarItem(
                    icon: Icon(Icons.school),
                    label: 'Fourth',
                    backgroundColor: Colors.pink)
              ],
              currentIndex: _selectedIndex,
              selectedItemColor: Colors.amber[800],
              onTap: _onItemTapped,
            ),
            drawer: Drawer(
                child: ListView(
              padding: EdgeInsets.zero,
              children: <Widget>[
                DrawerHeader(
                  child: Text('Drawer Header'),
                  decoration: BoxDecoration(color: Colors.blue),
                ),
                ListTile(
                  title: Text('Item 1'),
                  onTap: () {},
                ),
                ListTile(
                  title: Text('Item 2'),
                  onTap: () {},
                ),
              ],
            ))));
  }
}

5) 커스텀 스크롤 뷰와 슬리버 앱바

  • CustomScrollView : 화면의 한 영역에서 스크롤이 발생할 때 다른 영역도 함께 스크롤 되게 함
    • SliverList, SliverGrid로 화면 구성
    • SliverAppBar : 함께 스크롤할 화면 상단을 구현
  • 커스텀 스크롤 뷰 사용/슬리버 앱바 설정
      CustomScrollView(
          slivers: [
              SliverAppBar(
                  floating: true  // 다시 나타날 때 가장 먼저 나타나야 하는지
                  pinned: true  // 스크롤되어 접힐 때 모두 사라져야 하는지 설정
                  snap: true  // 스크롤이 멈추었을 때 계속 나타나야 하는지 설정
              ),
              SliverFixedExtentList()
          ]
      )
    

✨코드 예시

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  MyAppState createState() => MyAppState();
} 

class MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        home: Scaffold(
            body: CustomScrollView(
          slivers: [
            SliverAppBar(
                leading: IconButton(
                  icon: Icon(Icons.expand),
                  onPressed: () {},
                ),
                expandedHeight: 200,
                floating: true,
                pinned: false,
                snap: true,
                elevation: 50,
                backgroundColor: Colors.pink,
                flexibleSpace: Container(
                  decoration: BoxDecoration(
                      image: DecorationImage(
                          image: AssetImage('images/big.jpg'),
                          fit: BoxFit.fill)),
                ),
                title: Text('AppBar Title'),
                actions: <Widget>[
                  IconButton(
                    icon: const Icon(Icons.add_alert),
                    onPressed: () {},
                  ),
                  IconButton(
                    icon: const Icon(Icons.phone),
                    onPressed: () {},
                  ),
                ]),
            SliverFixedExtentList(
              itemExtent: 50.0,
              delegate:
                  SliverChildBuilderDelegate((BuildContext context, int index) {
                return ListTile(title: Text('Hello World Item $index'));
              }),
            )
          ],
        )));
  }
}

2. 네비게이션을 이용한 화면 전환

1) 내비게이션 사용하기

(1) 라우트 이해하기

  • 플러터에서는 화면과 관련된 모든 것이 위젯
  • Route : 화면을 지칭하는 객체(화면을 구성하는 다양한 정보)
  • Navigator : Route객체를 화면으로 전환
  • 코드 예시
      void main() {
          runApp(MyApp());
      }
    	
      class MyApp extends StatelessWidget {
          @override
          Widget Build(BuildContext context) {
              return MaterialApp(
                  home: OneScreen()
              );
          }
          // 화면 전환하기
          ElevatedButton(
              child: Text('Go Two Screen'),
              onPressed: () {
                  Navigator.push(
                      context,
                      MaterialPageRoute(builder: (context) => TwoScreen()),
                  );
              }
          )
    	
          // 이전 화면으로 돌아가기
          ElevatedButton(
              onPressed: () {
                  Navigator.pop(context);
              },
              child: Text('Go back!')
          )
      }
    	
      // 앱 화면 구성
      class OneScreen extends StatelessWidget { }
      class TwoScreen extends StatelessWidget { }
    

(2) 라우트 이름으로 화면 전환하기

  • Navigator.push() 함수로 화면을 전환하는 방법은 여러 화면일 시 비효율적일 수 있음
  • 코드 예시
      class MyApp extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
              return MaterialApp(
                  initialRoute : '/',
                  routes: {
                      '/': (context) => OneScreen(),
                      '/two' : (context) => TwoScreen(),
                      '/three' : (context) => ThreeScreen()
                  }
              );
          }
    	
          ElevatedButton(
              child: Text('Go Two Screen'),
              onPressed: () {
                  Navigator.pushNamed(context, '/two');
              }
          )
      }
    

✨코드 예시

  • main.dart
      void main() {
        runApp(MyApp());
      } 
    	
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            initialRoute: '/one',
            routes: {
              '/one': (context) => OneScreen(),
              '/two': (context) => TwoScreen(),
              '/three': (context) => ThreeScreen(),
              '/four': (context) => FourScreen(),
      },);}}
    
  • TwoScreen예시
      import 'package:flutter/material.dart';
    	
      class TwoScreen extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return MaterialApp(
              home: Scaffold(
                  appBar: AppBar(
                    title: Text('TwoScreen'),
                  ),
                  body: Container(
                      color: Colors.green,
                      child: Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              'TwoScreen',
                              style: TextStyle(color: Colors.white, fontSize: 30),
                            ),
                            ElevatedButton(
                                onPressed: () {
                                  Navigator.pushNamed(context, '/three');
                                },
                                child: Text('Go Three')),
                            ElevatedButton(
                                onPressed: () {
                                  Navigator.pop(context);
                                },
                                child: Text('Pop')),
                          ],
                        ),
                      ))));
        }
      }
    

(3) 화면 전환할 때 데이터 전달하기

  • push() 함수로 화면 전환할 때 데이터 전달
      Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => TwoScreen("hello"))
      )
    
  • pushNamed() 함수로 화면 전환할 때 데이터 전달
      ElevatedButton(
          onPressed: () {
              Navigator.pushNamed(
                  context,
                  '/three',
                  arguments: 10 // 데이터
              );
          },
          child: Text('Go Three Screen')
      )
    
    • 데이터 얻기
        int arg = ModalRoute.of(context)?.settings.arguments as int;
      
    • JSON 타입으로 데이터 여러 개 전달하기
        Navigator.pushNamed(
            context,
            '/three',
            arguments: {
                "arg1" : 10,
                "arg2" : "hello",
                "arg3" : User('kkang', 'seoul')  // 사용자 정의 객체 전달
        });
      
    • JSON 타입의 데이터 얻기
        Map<String, Object> args = ModalRoute.of(context)?.settings.arguments as Map<String, Object>;
      
  • pop() 함수로 화면 전환할 때 데이터 전달
      ElevatedButton(
          onPressed: () async {
              final result = Navigator.pop(context,
              User('kim', 'busan')   // 사용자 정의 객체 전달하기
              );
          },
          child: Text('Go Back')
      )
    
    • 데이터 얻기
        onPressed: () async {
            final result = await Navigator.pushNamed(
                context,
                '/three',
            );
            print('result: ${(result as User).name}');
        }
      

(4) 동적 라우트 등록 방법 - onGenerateRoute

  • 정적 라우트 등록
      routes: {
          '/two' : (context) => TwoScreen()
      }
    
  • 동적 라우트 등록(routes와 함께 적용 가능)
      onGeneratedRoute: (settings) {
          if (settings.name == '/two') {
              final arg = settings.arguments;
              if (arg != null) {
                  return MaterialPageRoute(
                      builder: (context) => TwoScreen(),
                      settings: settings
                  );
              } else {
                  return MaterialPageRoute(
                      builder: (context) => TwoScreen()
                  );
      }}}
    

✨코드 예시

  • 전송 데이터
      class User {
        String name;
        String address;
        User(this.name, this.address);
      }
    
  • main.dart
      import 'package:flutter/material.dart';
      import 'four_screen.dart';
      import 'three_screen.dart';
      import 'two_screen.dart';
      import 'one_screen.dart';
    	
      void main() {
        runApp(MyApp());
      } 
    	
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            initialRoute: '/one',
            routes: {
              '/one': (context) => OneScreen(),
              '/two': (context) => TwoScreen(),
              // '/three': (context) => ThreeScreen(),
              // '/four': (context) => FourScreen(),
            },
            onGenerateRoute: (settings) {
              if (settings.name == '/three') {
                return MaterialPageRoute(
                    builder: (context) => ThreeScreen(), settings: settings);
              } else if (settings.name == '/four') {
                return MaterialPageRoute(
                    builder: (context) => FourScreen(), settings: settings);
      }},);}}
    
  • TwoScreen예시
      import 'package:flutter/material.dart';
      import 'user.dart';
    	  
      class TwoScreen extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          Map<String, Object> args =
              ModalRoute.of(context)?.settings.arguments as Map<String, Object>;
          return MaterialApp(
              home: Scaffold(
                  appBar: AppBar(
                    title: Text('TwoScreen'),
                  ),
                  body: Container(
                      color: Colors.green,
                      child: Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              'TwoScreen',
                              style: TextStyle(color: Colors.white, fontSize: 30),
                            ),
                            Text(
                                'sendData:${args['arg1']}, ${args['arg2']}, ${(args['arg3'] as User).name}'),
                            ElevatedButton(
                                onPressed: () {
                                  Navigator.pushNamed(context, '/three');
                                },
                                child: Text('Go Three')),
                            ElevatedButton(
                                onPressed: () {
                                  Navigator.pop(context, User('kim', 'busan'));
                                },
                                child: Text('Pop')),
                          ],
                        ),
                      ))));
        }
      }
    

(5) 네비게이터 스택 제어하기

  • pushNamed() : 스택이 다층 구조로 쌓인다.
  • pop() : 스택 맨 위의 라우트 객체 제거한 후 그 다음 라우트 화면 출력
    • 맨 마지막 라우트였다면? 👉👉 앱 종료
    • 맨 마지막 라우트 일때 앱 종료하고 싶지 않다면, maybePop()
        Navigator.maybePop(context);
      
    • 맨 마지막 라우트인지 확인하고 싶다면, canPop()
        Navigator.cnaPop(context)    // true or flase
      
  • pushReplacementNamed(), popAndPushNamed() : 현재 위젯 대체
      Navigator.pushReplacementNamed(context, '/three');
      Navigator.popAndPushNamed(context, '/three');
    
  • pushNamedAndRemoveUntill() : 원하는 위치까지 스택에서 제거한 후 화면이동
    • 단순 스택 쌓기
        Navigator.pushNamedAndRemoveUntill(context, '/four', (route) => true);
      
    • 새 스택 쌓기
        Navigator.pushNamedAndRemoveUntill(context, '/four', (route) => false);
      
    • 특정 위젯까지만 남기고 추가
        Navigator.pushNamedAndRemoveUntil(context, '/four', ModalRoute.withName('/one'));  // OneScrean부터 다시 쌓기
      
  • popUntil() : 특정 위젯으로 되돌아가기
      ElevatedButton(
          onPressed: () {
              Navigator.popUntil(context, ModalRoute.withName('/two')); //TwoScreen 쌓여 있는 곳까지 가기
          }
          child: Text('Pop Until')
      )
    

2) 내비게이션 2.0 사용하기

  • 내비게이션 2.0 : 웹앱 지원 및 다양한 화면 전환 기법 수요로 추가된 API를 총칭

(1) 내비게이션 2.0 기본 구조

  • Router : 페이지 정보를 스택 구조로 가지는 위젯
    • Page : 출력할 화면 정보
  • RouterDelegate : 다양한 상황에 맞는 라우팅 처리
    • RouteInformationParser : 라우팅 요청 분석

(2) 내비게이터와 페이지

  • Navigator : 라우터의 스택구조를 활용하여 화면을 구성(build함수에서 반환한 위젯)
    • pages : 라우터에 포함할 여러 페이지 추가 가능
    • onPopPage : (Not Null) 뒤로 갈 페이지 동적 설정(AppBar가 제공하는 back-button을 누를 때 호출 됨)
    • 코드 예시
        class MyApp extends StatelessWidget {
            @override
            Widget build(BuildContext context) {
                return MaterialApp(
                    title: 'Navigator Test',
                    home : Navigator(
                        pages: [
                            MaterialPage(
                                child : OneScreen()
                            )
                        ],
                        onPopPage : (route, result) => route.didPop(result)
        ))}}
      
  • 딥 링크(deep link) : 첫 화면이 아닌 다른 화면부터 출력
    • 코드 예시
        class MyApp extends StatelessWidget {
            boll _isDeepLink = true;
            @override
            Widget build(BuildContext context) {
                return MaterialApp(
                    title: 'Navigator Test',
                    home: Navigator(
                        pages: [
                            MaterialPage(
                                child: OneScreen()
                            ),
                            if (_isDeepLink) MaterialPage(child: TwoScreen())
                        ],
                        onPopPage: (route, result) => route.didPop(result)
        ));}}
      

✨코드 예시

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  bool _isDeepLink = true;
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Navigator Test',
        home: Navigator(pages: [
          MaterialPage(child: OneScreen()),
          if (_isDeepLink) MaterialPage(child: TwoScreen())
        ], onPopPage: (route, result) => route.didPop(result)));
  }
}
  
class OneScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('OneScreen'),
        ),
        body: Container(
          color: Colors.red,
          child: Center(
              child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('OneScreen',
                  style: TextStyle(color: Colors.white, fontSize: 30))
            ],
          )),
        ));
  }
} 

class TwoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text('TwoScreen')),
        body: Container(
            color: Colors.cyan,
            child: Center(
                child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'TwoScreen',
                  style: TextStyle(color: Colors.white, fontSize: 30),
                ),
                ElevatedButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: Text('Pop'))
              ],
            ))));
  }
}

(3) 라우터 델리게이트와 정보 분석기

  • 화면 구성이 복잡할 때 이용
  • 라우터 경로 클래스
      class MyRoutePath {
          String? id;
    	
      MyRoutePath.home() : this.id = null;
      MyRoutePath.detail(this.id);
      }
    
  • 라우터 델리게이트와 정보 분석기 등록
          class _MainAppState extends State<MainApp> {
              @override
              Widget build(BuildContext context) {
                  return MaterialApp.router(
                  routerDelegate: MyRouterDelegate(),
                  routeInformationParser: MyRouteInformationParser());
              }
          }
    
  • 라우터 정보 분석기(Route InformationParser) : 화면을 전환하는 라우트 정보를 분석해 경로(RoutePath)에 담음
    • 앱의 라우팅 정보 분석 : 앱을 처음 실행하거나 라우팅될 때 정보 분석
    • 앱의 라우팅 정보 저장 : 라우팅 결정된 후 현재 상태 저장
      class MyRouteInformationParser extends RouteInformationParser<MyRoutePath> {
        // parseRouteInformation() 함수 구현
        @override
        Future<MyRoutePath> parseRouteInformation(
            RouteInformation routeInformation) async {
          final uri = Uri.parse(routeInformation.location ?? '/');
          if (uri.pathSegments.length >= 2) {
            var remaining = uri.pathSegments[1];
            return MyRoutePath.detail(remaining);
          } else {
            return MyRoutePath.home();
          }
        }
    	  
        // restoreRouteInformation() 함수 구현
        @override
        RouteInformation restoreRouteInformation(MyRoutePath configuration) {
          print('restoreRouteInformation.. id : ${configuration.id}');
    	
          if (configuration.id != null) {
            return RouteInformation(location: '/detail/${configuration.id}');
          } else {
            return RouteInformation(location: '/');
          }
        }
      }
    
  • 라우터 델리게이트(RouterDelegate) : 라우팅 대리자로서 정보 분석기가 분석한 경로를 받아 내비게이터를 만듬
    • currentConfiguration() : build() 함수 호출 직전에 자동 호출, 해당 함수에서 만들어진 라우터 경로가 정보 분석기의 restoreRouteInformation() 함수에 전달되어 라우팅 상태 저장
    • setNewRoutePath() : parseRouteInformation() 함수에서 반환한 값을 받아 라우터 델리게이트가 초기화될 때 호출(추상 클래스)
    • notifyListeners() : 화면 전환환
      // 상속, with 필수
      class MyRouterDelegate extends RouterDelegate<MyRoutePath>
          with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRoutePath> {
          final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    
          // 내비게이터 구현하기
          @override
          Widget build(BuildContext context) {
              return Navigator(
                  key: navigatorKey,
                  pages: [
                      MaterialPage(child: HomeScreen(_handleOnPressed)),
                      if (selectId != null) MaterialPage(child: DetailScreen(selectId))
                  ],
                  onPopPage: (route, result) {}
              );
          }
    
          // currentConfiguration() 함수 구현
          @override
          MyRoutePath get currentConfiguration {
              if (selectId != null) {
                  return MyRoutePath.detail(selectId);
              } else {
                  return MyRoutePath.home();
              }
          }
    
          // setNewRoutePath() 함수 구현
          @override
          Future<void> setNewRoutePath(MyRoutePath configuration) async {
              print('MyRouterDelegate... setNewRoutePath ... id : ${configuration.id}');
              if(configuration.id != null) {
                  selectId = configuration.id;
              }
          }
    
          // notifyListeners() 함수 구현
          void _handleOnPressed(String id) {
              selectId = id;
              print('MyRouterDelegate... notifylistener call..');
              notifyListeners();
          }
      }
    

✨코드 구현

import 'package:flutter/material.dart';

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

class MainApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MainAppState();
} 

// 라우터 델리게이트, 정보 분석기 등록
class _MainAppState extends State<MainApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
        routerDelegate: MyRouterDelegate(),
        routeInformationParser: MyRouteInformationParser());
  }
} 

// 라우터 경로
class MyRoutePath {
  String? id;
  MyRoutePath.home() : this.id = null;
  MyRoutePath.detail(this.id);
}

// 라우터 정보 분석기
class MyRouteInformationParser extends RouteInformationParser<MyRoutePath> {
  // parseRouteInformation() 함수 구현
  @override
  Future<MyRoutePath> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.location ?? '/'); //location은 과거버전
    if (uri.pathSegments.length >= 2) {
      var remaining = uri.pathSegments[1];
      return MyRoutePath.detail(remaining);
    } else {
      return MyRoutePath.home();
    }
  }

  // restoreRouteInformation() 함수 구현
  @override
  RouteInformation restoreRouteInformation(MyRoutePath configuration) {
    if (configuration.id != null) {
      return RouteInformation(location: '/detail/${configuration.id}'); //location은 과거버전
    } else {
      return RouteInformation(location: '/'); //location은 과거버전
    }
  }
}

// 라우터 델리게이트
class MyRouterDelegate extends RouterDelegate<MyRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRoutePath> {
  String? selectId;
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  // currentConfiguration() 함수 구현
  @override
  MyRoutePath get currentConfiguration {
    if (selectId != null) {
      return MyRoutePath.detail(selectId);
    } else {
      return MyRoutePath.home();
    }
  }

  // 내비게이터 구현하기
    // notifyListeners()
  @override
  Widget build(BuildContext context) {
    return Navigator(
        key: navigatorKey,
        pages: [
          MaterialPage(child: HomeScreen(_handleOnPressed)),
          if (selectId != null) MaterialPage(child: DetailScreen(selectId))
        ],
        onPopPage: (route, result) {
          if (!route.didPop(result)) {
            return false;
          }
          selectId = null;
          notifyListeners();
          return true;
        });
  }

  // setNewRoutePath() 함수 구현
  @override
  Future<void> setNewRoutePath(MyRoutePath configuration) async {
    if (configuration.id != null) {
      selectId = configuration.id;
    }
  }

  // notifyListeners()	
  void _handleOnPressed(String id) {
    selectId = id;
    notifyListeners();
  }
} 

// 홈화면
class HomeScreen extends StatelessWidget {
  final ValueChanged<String> onPressed;
  HomeScreen(this.onPressed);
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: Container(
          color: Colors.red,
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Home Screen',
                  style: TextStyle(color: Colors.white, fontSize: 30),
                ),
                ElevatedButton(
                    onPressed: () => onPressed('1'),
                    child: Text('go detail with 1')),
                ElevatedButton(
                    onPressed: () => onPressed('2'),
                    child: Text('go detail with 2')),
              ],
            ),
          ),
        ));
  }
}

// 디테일 화면
class DetailScreen extends StatelessWidget {
  String? id;
  
  DetailScreen(this.id);
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(),
        body: Container(
            color: Colors.green,
            child: Center(
                child: Text('Detail Screen $id',
                    style: TextStyle(color: Colors.white, fontSize: 30)))));
  }
}

댓글남기기