Flutter-Week00

koeyhoyh·2022년 6월 22일
0

App_Flutter

목록 보기
2/19

잘못된 점이 있다고 생각되신다면 제가 틀릴 확률이 매우 높습니다... 언제든지 의견 남겨주시면 감사히 받겠습니다 : )


목표 : 백그라운드에서 지속적으로 사용자의 위치를 기록하고, 서버에 전송할 수 있는 Android / iOS 앱 만들기


ex00 : Framework

해당 목표에 가장 적합한 프레임워크 찾아보기


Xamarin : MS에서 인수해 개발중인 크로스 플랫폼

언어로 XAML, C# 을 사용한다.

장점 : 대부분의 크로스 플랫폼에서는 JS를 사용하는데, C#을 사용할 수 있다.
: 크로스 플랫폼으로 개발하지만, 네이티브로 빌드된다.

단점 : 자마린을 이용해 개발하는 개발자의 수가 다른 크로스 플랫폼에 비해 확연히 적다.

그로 인해, 커뮤니티도 적고, 문제가 발생해도 찾기 힘들다.


Cordova : 어도비 시스템즈 (아파치 재단)에서 인수한, '모바일 개발 플랫폼'이다.

언어로 JS, HTML, CSS 를 이용한다.

장점 : HTML 로 페이지만 잘 만들 수 있다면 쉽게 사용 가능하다.

단점 : 문서가 많이 미흡하다.
: HTML 페이지를 웹 뷰에서 표기하는 방식이기 때문에, 네이티브 어플리케이션과 유사하게 만들기 어렵다. 성능이 비교적 많이 떨어진다.


플러터 : 구글에서 만들고 지원하는 크로스 플랫폼

장점 1. 구글에서 개발했다. + 업데이트 주기가 빠르다.

이번에 3.0으로 업데이트 되었으며,
Android / iOS / Web / Window / MacOS / Linux
하나의 코드로 6개의 플랫폼에서 앱을 만들 수 있다.

장점 2. 직접 만들어둔 튜토리얼 예제 동영상과, 문서가 아주 잘 쓰여져 있어서 정보를 찾고, 물어보기 편하다.

docs를 보며, 설치부터 튜토리얼, 심화 과정까지 나아갈 수 있을 정도로 공식 문서가 잘 쓰여 있습니다.

개발자가 많아 커뮤니티가 비교적 크고, 그로 인해서 많은 정보들을 추가로 얻을 수 있다.

장점 3. Dart 언어는 JS 보다 비교적 유연하다.

정적, 동적 타입을 유동적으로 지닐 수 있다. -> JIT(Just In Time), AOT(Ahead Of Time) 컴파일 방식을 둘 다 사용할 수 있다.

장점 4. 네이티브 위젯을 흉내낸 방식으로 픽셀을 그리기 때문에, 성능이 어느정도 많이 보장된다.

단점 : 비교적 생소한, dart라는 언어를 사용한다.

dart 언어보다는 js를 익힌 개발자들이 훨씬 더 많다. -> dart를 배우기 위해 시간을 어느정도 소비해야 한다. 그리고 dart를 익혀도 현재로써는 플러터에서만 사용이 가능하다.


React Native : 전 페이스북, 현 메타에서 개발한 크로스 플랫폼

장점 : JS (react)를 사용해 개발할 수 있다.

react를 사용한 웹 사이트를 모바일로 만들기 비교적 쉽고, 또 react-native 를 사용한 모바일 앱을 웹으로 확장하기 쉽습니다.

이미 생태계가 가장 크게 구축이 되어 있다.

개발자 수가 가장 많고, 이미 많은 사람들이 여러 App을 React Native를 이용해 만들었다. 응용 방식과 개발자들의 수 많은 사용방식을 알 수 있다.

CodePush 라는 기능을 이용해, 앱스토어에 바로 업데이트 할 수 있다.

단점 : (사용자 관점) "버그가 나타나면, 무엇때문에 나타났는지 파악하기 어렵다." 라는 말들이 많다.

여러 가지 모듈들의 의존성 문제가 있고, 라이브러리들은 대부분java, objective-c 를 사용한다.

kotlin, swift를 추천하고, 권장하는 지금은 아쉬운 점이다.


그래서...?

결국에는 flutter 와 react native 사이에서 고민하게 되었습니다.

목표인 "백 그라운드에서 지속적으로 사용자의 위치를 기록하고, 서버에 전송할 수 있는 어플리케이션 만들기" 를 다시 생각해보며 여러 가지에 대해 검색해보았습니다.

flutter gps / flutter gps tracker

react native gps / react native gps tracker 등으로 검색해보니, 300 ~ 400 만개 정도로 비슷한 숫자의 검색 개수가 나왔습니다.

찾을 수 있는 정보의 양
flutter == react native

배우기 쉬운 정도
flutter >= react native

공식 참고 문서 (docs)나, 인터넷 페이지에 나와있는 정보의 양은 비슷하지만, 동영상 강의와 튜토리얼 예제등이 아주 잘 나와있어 flutter 가 마음에 들어서, flutter 를 선택하게 되었습니다.


참고 : react native와 flutter의 비교

https://medium.com/fm-stories/react-native-%EC%99%80-flutter%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8B%A4%EB%A5%BC%EA%B9%8C-e9ea4a81b6d5

5가지 크로스 플랫폼의 비교
https://doit.software/blog/flutter-vs-react-native#screen6


ex01 Hello world!

flutter 를 사용해서, Hello world! 앱을 Android 와 iOS 에 뛰워보기.

참고 : flutter docs // 처음 hello world 앱 만들어보기
https://docs.flutter.dev/get-started/codelab

lib/main.dart 코드를 아래의 코드로 바꾸면 됩니다.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "hello world",
      home: Scaffold(
        appBar: AppBar(
          title: const Text('hello World!'),
        ),
        body: const Center(
          child: Text('Hello, world!'),
        ),
      ),
    );
  }
}

튜토리얼을 따라 진행해보았습니다.
Android :

iOS :

코드는 github / Cryptolab-App_Flutter
https://github.com/happinessee/Cryptolab-App_Flutter/tree/main/week00


ex02 Lint

코드를 규칙에 따라 보기 쉽고 단순하게 작성하는 것은 중요합니다.

어떤 프로그램과 규칙을 사용할 지 고르기.

다른 사람들이 최대한 보기 쉽게 코드를 작성하기 위해, 그리고 다른 사람들과 협업할 때를 위해.

사람들은 모두 다 다른 코딩 스타일을 가지고 있고, lint는 같은 코드 규칙을 지키게 만들어 보기 쉽게 만들어줍니다.

google에서 기본으로 제공해주는 lint는 flutter_lints 입니다.

include: package:flutter_lints/flutter.yaml

그리고 저는 이 "flutter_lints" lint를 아래와 같은 이유로 사용하기로 결정했습니다.
-> 거의 모든 사람이 사용하는 표준입니다. // 이 이유 하나 만으로도 결정할 수 있었습니다.

세부 사항은 다른 사람들과 함께 조절할 수 있겠지만 거의 모든 사람들이 표준으로 알고 있고, 이 lint를 기준으로 코드를 작성하고 있는 것을 알 수 있습니다.

# 세부사항의 예제
avoid_print: false  # Uncomment to disable the `avoid_print` rule

참고 : VScode 에서의 formatter

VScode 에서는 Flutter Extension을 깔기만 하면 바로 자동으로 formatting이 됩니다.
또한 MacOS : option + shift + F 로도 정렬할 수 있습니다.
CLI 환경에서도 물론 가능합니다.

참고 : https://docs.flutter.dev/development/tools/formatting


ex03 State

상태 관리는 앱에서 중요한 요소입니다. 프레임워크들은 대부분 고유한 상태 관리 방법을 가지고 있습니다. 하지만 때로는 기본 상태 관리 방법보다 더 좋은 방법이 있기도 합니다.

사용할 수 있는 상태 관리 방법을 찾고 그 중 한가지 상태 관리 방법을 고르세요.

선택한 상태 관리 방법을 통해 버튼을 누를 경우 숫자가 더해지는 앱을 개발해보세요.

먼저, Flutter 에서 "상태"라는 것에 대해 알아보겠습니다.

넓은 의미에서 상태란 앱이 돌아가고 있을 때 메모리에 존재하는 모든 것을 뜻합니다. 그러나 모든 상태를 관리해야 하는 것은 아니므로 (ex / textures ...)

Flutter는 선언적이다. -> 앱의 현재 상태를 반영하도록 사용자 인터페이스를 만든다.
상태를 변경하면, UI의 redraw 가 작동된다.

먼저, Ephemeral State 가 있다.

flutter docs에서의 정의는

"Ephemeral state (sometimes called UI state or local state) is the state you can neatly contain in a single widget."

라고 되어있고, 문서에서 스스로 애매모호한 정의라면서 예제를 보여준다.

  • current page in a PageView
  • current progress of a complex animation
  • current selected tab in a BottomNavigationBar

다음 설명에서 조금 더 이해가 되었다.

there is no need to use state management techniques (ScopedModel, Redux, etc.) on this kind of state.
직역하자면, 상태 관리 기법을 사용할 필요 없는 상태라는 것이다.

오직, Stateful Widget 만이 상태 관리 기법이 필요하다.

아래는 예이다.

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

  
  _MyHomepageState createState() => _MyHomepageState();
}

class _MyHomepageState extends State<MyHomepage> {
  int _index = 0;

  
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      currentIndex: _index,
      onTap: (newIndex) {
        setState(() {
          _index = newIndex;
        });
      },
      // ... items ...
    );
  }
}

해당 bottom nevigation bar는 _index 변수를 가지고 있고,
_index 변수는 ephemeral state 이다.

그 다음, App state 가 있다.

앱의 많은 부분에서 공유할, 그리고 유저 세션에서 유지하고 싶은 상태가 App state 이다.

예로는

  • User preferences
  • Login info
  • Notifications in a social networking app
  • The shopping cart in an e-commerce app
  • Read/unread state of articles in a news app

가 있다.

그래서, 큰 그림으로 보면 상태는 보통 이렇게 결정된다.

Redux를 만든 사람은, 상태관리기법 중 "최고는 덜 어색한 것이다." 라고 말했다.

상태에 대해 요약하자면,

Ephemeral state는 "State" 와 "setState()" 에 의해 구현되며, 보통 단일 위젯의 안에 사용된다. 그리고, 어느 플러터 앱에도 두 가지의 상태가 있으므로 개발자의 선호도와, 앱의 복잡성 사이에서 덜 어색하게 잘 결정하면 될 것이다.


상태에 대해 알아보았으니...

이제는 상태 관리 기법에 대해 알아볼 차례이다.

docs 에서는 "provider" package 를 이용해 설명해준다. (시작하기 적합하고 많은 코드를 사용하지 않아 좋다고 한다.)

예제는 간단한 쇼핑 앱을 보여준다. 쇼핑 앱의 형식은 카탈로그와 내 장바구니로 되어있다.

여기에서, MyListItem은 MyCatalog에도, 또 MyCart에도 속해있을 수 있다. 그리고 MyListItem들을 추가할 수도 있어야 한다.

그렇다면, 우리는 카트의 현재 상태를 어디에 놓아야 할까??

나쁜 예제들

// BAD: DO NOT DO THIS
void myTapHandler() {
  var cartWidget = somehowGetMyCartWidget();
  cartWidget.updateWith(item);
}
// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
  return SomeWidget(
    // The initial state of the cart.
  );
}

void updateWith(Item item) {
  // Somehow you need to change the UI from here.
}

선언적 프레임워크인 플러터는 UI를 바꾸기 위해서는 rebuild를 반드시 해야 한다. 그것은 결코 쉬운 일이 아니다. 다른 말로 하자면, 위젯 밖에서 메소드를 호출한다고 해서 해당 위젯을 강제로 바꾸기 어렵다.

우리는 UI의 현재 상태를 고려하여 새 데이터를 추가해야 한다. 위의 방식으로 데이터를 추가한다면 버그를 피하기 어렵다...

플러터에서는 위젯의 내용물이 변경된다면 위젯을 새로 구성한다. 메소드를 호출해 업데이트하는 방식 대신, Constructor 를 이용한다.

BAD (MyCart.updateWith(new))
GOOD (MyCart(Content))

대신, 이 방식은 해당 위젯의 부모나 그 위가 살아있어야 가능한 방식이다. (당연히 자식을 새로 생성하려면 부모가 있어야 할 것이다.)

좋은 예제들

// GOOD
void myTapHandler(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  cartModel.add(item);
}
// GOOD
Widget build(BuildContext context) {
  var cartModel = somehowGetMyCartModel(context);
  return SomeWidget(
    // Just construct the UI once, using the current state of the cart.
    // ···
  );
}

예제에서는, content가 반드시 MyApp에 있어야 한다. content가 변경된다면, 위쪽에서 MyCart를 rebuild한다. 이 방식 때문에, MyCart는 생애 주기에 대해 걱정할 필요가 없어진다. MyCart는 그저 안에 있는 내용물을 보여주기만 하면 된다! Content가 변경되면, 위젯은 사라졌다가 완전히 새 것으로 대체된다.

이것이 플러터에서 말하는 위젯의 불변함이다. 위젯은 변하지 않는다. 사라지고 새로운 위젯으로 대체 될 뿐이다.

상태에 접근하려면...?

이제 카탈로그에서 아이템에 클릭해, 카트에 담았다. 그러나, 카트가 MyListItem 위에 살아있을 때부터는 우리는 아이템을 카트에 담기 위해 어떻게 해야할까??

가장 간단한 방법은 MyListItem에게 클릭될 때마다 call할 수 있는 callback을 제공하는 것이다.


Widget build(BuildContext context) {
  return SomeWidget(
    // Construct the widget, passing it a reference to the method above.
    MyListItem(myTapCallback),
  );
}

void myTapCallback(Item item) {
  print('user tapped on $item');
}

이 방법은 꽤 괜찮다. 하지만 App state 를 많은 다른 곳에서 수정해야할 때, 수 많은 콜백들이 지나쳐가서 위젯을 금방금방 대체해야 할 것이다.

다행히, 플러터는 자신의 데이터와 서비스를 그 밑의 후손 위젯들에게 (단순히 자식 뿐만이 아니라 아래의 위젯들도 포함) 제공해주는 기능을 가지고 있다.

플러터는 모든것이 위젯이므로, 이 기능들도 특별한 종류의 위젯에 불과하다.
(InheritedWidget, InheritedNotifier, InheritedModel 등등...)

이것들은 우리가 하려는 작업에 비해 꽤 low-level의 작업이므로, 이러한 low-level의 작업들을 쉽게 할 수 있게 해주는 패키지를 사용할 것이다.
해당 패키지가 바로 "provider" 이다.

해당 패키지를 사용하려면, pubspec.yaml 파일의 dependencies 부분에 provider를 추가해야 한다.
추가했다면,

import 'package:provider/provider.dart';

로 import해 provider를 사용할 수 있다!!!

provider 를 사용하기 위해서는, 아래의 3가지 개념에 대해 알아야 한다.

  • ChangeNotifier
  • ChangeNotifierProvider
  • Consumer

ChangeNotifier

Flutter의 SDK 에 있는 간단한 클래스이다. listener에게 변경 알림(?)을 제공한다.

ChangeNotifier는 App state를 캡슐화하기 위한 한 가지 방법이다.
그래서 간단한 앱에서는 하나의 ChangeNotifier면 되겠지만, 복잡한 앱에서는 여러 개의 ChangeNotifier을 사용해야 한다.

class CartModel extends ChangeNotifier {
  /// Internal, private state of the cart.
  final List<Item> _items = [];

  /// An unmodifiable view of the items in the cart.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => _items.length * 42;

  /// Adds [item] to cart. This and [removeAll] are the only ways to modify the
  /// cart from the outside.
  void add(Item item) {
    _items.add(item);
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }

  /// Removes all items from the cart.
  void removeAll() {
    _items.clear();
    // This call tells the widgets that are listening to this model to rebuild.
    notifyListeners();
  }
}

item을 추가하고, 지울 때마다 notifyListener() 를 통해서 이 모델을 rebuild 한다.

ChangeNotifier는 flutter:foundation의 일부이며, 다른 높은 클래스에 의존하지 않는다.

ChangeNotifierProvider

ChangeNotifierProvider는 ChangeNotifier를 후손들에게 제공해주는 위젯이다. 이름에 맞게, provider 패키지에 들어가있다.

ChangeNotifierProvider는 사용하는 위젯 위에서 접근해야하기 때문에, 여기에서는 MyCart 와 MyCatalog 위에 존재한다.

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

ChangeNotifierProvider는 상당히 똑똑해서, rebuild가 필요할 때가 아니면 rebuild를 하지 않고, 또 해당 instance가 필요 없어지면 자동으로 dispose()를 선언한다.

여러 개를 사용하고 싶다면, MultiProvider: 를 사용하면 된다

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => CartModel()),
        Provider(create: (context) => SomeOtherClass()),
      ],
      child: const MyApp(),
    ),
  );
}

Consumer

우리가 provider를 사용하기 위해서, 접근하고자 하는 모델의 타입을 특정해야 한다. 여기서는 CartModel이므로, 우리는 CartModel 타입을 특정해 사용할 것이다.

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text('Total price: ${cart.totalPrice}');
  },
);

Consumer 위젯은 간단하게 이런 식으로 사용된다.
-> 우리가 notifyListener()을 호출한다.
-> Consumer 안에 있는 모든 builder 메소드가 실행된다.

return Consumer<CartModel>(
  builder: (context, cart, child) => Stack(
    children: [
      // Use SomeExpensiveWidget here, without rebuilding every time.
      if (child != null) child,
      Text('Total price: ${cart.totalPrice}'),
    ],
  ),
  // Build the expensive widget here.
  child: const SomeExpensiveWidget(),
);

인자가 3개 필요한데,

1번째 인자는 context 이다. 모든 build method 를 사용할 때 필요하다.

2번째 인자는 instance of the ChangeNotifier 이다. 이 인자로 인해서, 우리는 해당 모델의 데이터를 사용해 정해진 지점에서 UI의 모양을 정의할 수 있다.

3번째 인자는 child 이다. 이 인자는 최적화를 위해 필요하다. (?) Consumer 아래에 모델이 변경되어도 변경되지 않을 거대한 트리 위젯들이 있다면 한 번 만들고, 빌더로 넘겨줄 수 있다.

나쁜 예제

// DON'T DO THIS
return Consumer<CartModel>(
  builder: (context, cart, child) {
    return HumongousWidget(
      // ...
      child: AnotherMonstrousWidget(
        // ...
        child: Text('Total price: ${cart.totalPrice}'),
      ),
    );
  },
);

좋은 예제

// DO THIS
return HumongousWidget(
  // ...
  child: AnotherMonstrousWidget(
    // ...
    child: Consumer<CartModel>(
      builder: (context, cart, child) {
        return Text('Total price: ${cart.totalPrice}');
      },
    ),
  ),
);

Provider.of

가끔, 우리는 모델 안의 데이터는 필요없지만 그 데이터에 접근은 해야 하는 상황이 온다. Cart 안의, ClearCart가 그 예이다. 카트의 모든 것을 삭제하지만, 카트의 내용물을 보여줄 필요는 없다.

우리는 Consumer<CartModel>을 사용할 수도 있지만, 우리는 rebuilt 할 필요가 없기 때문에 많은 부분이 낭비된다.

이런 경우에는, Provider.of 를 사용할 수 있다. listen parameter를 false 로 바꾸어주기만 하면 된다.

Provider.of<CartModel>(context, listen: false).removeAll();

Provider을 이용한 버튼 App (기본)

Provider를 이용하기 전

import 'package:flutter/material.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

Provider 패키지 이용한 버튼 App

import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:window_size/window_size.dart';

void main() {
  setupWindow();
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: const MyApp(),
    ),
  );
}

const double windowWidth = 360;
const double windowHeight = 640;

void setupWindow() {
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
    WidgetsFlutterBinding.ensureInitialized();
    setWindowTitle('Provider Counter');
    setWindowMinSize(const Size(windowWidth, windowHeight));
    setWindowMaxSize(const Size(windowWidth, windowHeight));
    getCurrentScreen().then((screen) {
      setWindowFrame(Rect.fromCenter(
        center: screen!.frame.center,
        width: windowWidth,
        height: windowHeight,
      ));
    });
  }
}

class Counter with ChangeNotifier {
  int value = 0;

  void increment() {
    value += 1;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'button_app',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('push_button_app'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You have pushed the button this many times:'),
            Consumer<Counter>(
              builder: (context, counter, child) => Text(
                '${counter.value}',
                style: Theme.of(context).textTheme.headline4,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          var counter = context.read<Counter>();
          counter.increment();
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

전에 말한대로, pubspec.yaml 파일의 dependencies 에 패키지를 추가해야 한다!!!

Bloc, provider, Riverpod, getX 등의 많은 상태관리 기법, 패키지들이 있는데 이런 간단한 앱을 만들기 위해서 어려운 상태관리 기법들을 적용하지는 않아도 될 것이라고 생각했습니다.

참고 :
flutter docs의 상태와 기본적인 상태관리 기법
https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro

상태 관리 라이브러리란? (Redux, Mobx 등)
https://velog.io/@velopert/redux-or-mobx


ex04 Route

1번 화면에서 버튼을 누르면 2번 화면으로,
2번 화면에서 버튼을 누르면 1번 화면으로 이동하는 앱을 개발해보자.

왜 과제 이름이 Route일까?
Flutter에서는 화면들과 페이지들을 Route라고 부른다!

예제를 따라 진행해보았습니다.

일단 2개의 화면을 만듭니다

class FirstRoute extends StatelessWidget {
  const FirstRoute({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Route'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Open route'),
          onPressed: () {
            // Navigate to second route when tapped.
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  const SecondRoute({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Route'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Navigate back to first route when tapped.
          },
          child: const Text('Go back!'),
        ),
      ),
    );
  }
}

화면 2개를 만들었다면, 1번째 페이지가 기본, push 버튼을 통해서 2번 페이지로 이동시켜야 합니다.

1번째 페이지에 버튼을 만듭니다.
Nevigator.push() 를 이용합니다.
이 때, Nevigator는 화면을 stack처럼 쌓이게 만들어줍니다.

그리고, 2번째 페이지에도 버튼을 만듭니다.
이 때는 Nevigator.pop()을 이용해, 원래 있던 1번째 화면으로 이동할 것입니다.

첫 번째 버튼

// Within the `FirstRoute` widget
onPressed: () {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => const SecondRoute()),
  );
}

두 번째 버튼

// Within the SecondRoute widget
onPressed: () {
  Navigator.pop(context);
}


소스코드

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatelessWidget {
  const FirstRoute({super.key});
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Route'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Open route'),
          style: ElevatedButton.styleFrom(primary: Colors.red),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondRoute()),
            );
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  const SecondRoute({super.key});
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Second Route"),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text('Go back!'),
        ),
      ),
    );
  }
}

참고 :
Nevigate to a new screen and back
https://docs.flutter.dev/cookbook/navigation/navigation-basics


ex05 Nevigation bar

화면을 바꾸는데 많이 쓰이는 방법 중 하나는 'Navigation bar'입니다.
예시와 같이 화면 하단에 1, 2, 3번 화면으로 이동할 수 있는 Navigation bar가 있는 앱을 개발해보세요.

보통의 바텀 네비게이션 바의 형식은 이렇게 된다.

일반 네비게이션 바의 형식이다.

만드려면, 토글 버튼 형식으로 만들어야 할 것 같았지만

일단, 보통의 네비게이션 바 형식으로 만들어보기로 결정했다.
예제가 정말 잘 나와있어서 처음에는 예제를 따라 만들었다.

예제는 아래의 네비게이션 바 버튼을 누를 때마다, 인덱스를 받아와서 텍스트 위젯을 새로 써주는 방식이었다.

이렇게 텍스트 위젯 하나만 있을 때는, 화면을 새로 쓰지 않고 위젯을 바꾸어주기만 하면 되겠지만 위젯이 여러 개로, 복잡하게 구성되어 있다면 매우 비효율적인 방법이었다.

그래서 일단 화면을 3개 만들기로 했다.

그 다음에 화면에 네비게이션 바를 집어넣고 네비게이션 바를 누를 때마다 화면이 움직이게 만들어주어야 했다.

처음에는 4에서 이용했던 Navigator의 pushNamed 를 이용해 만드려고 했었다. NavigationBar의 요소를 클릭하면 Navigator의 pushNamed가 작동해 해당 페이지로 이동하는 것이다.

두 번째로 생각한 방법은 "네비게이션 바의 기능 중 내가 원하는 route change 같은 기능이 분명히 있을 것이다." 라고 생각하고 네비게이션 바 자체만 이용하는 방법이었다.
-> 방법이 있었다. 네비게이션 바의 버튼을 누르면 눌린 버튼의 index를 가져올 수 있다. 바뀐 index를 화면 배열의 index에 넣어서 새로운 스크린을 다시 만든다.

참고 : https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html

Flutter Tutorial - NEW Material You Navigation Bar | The New Way [2021] Flutter Navigation Bar

profile
내가 만들어낸 것들로 세계에 많은 가치를 창출해내고 싶어요.

0개의 댓글