본문 바로가기

Flutter/개발일지

[Flutter/개발 일지 3] Write your first app 2편 무작정 해석하며 따라하기

3번째 일지 22년 03월 07일

수업 시간에 간략하게 설명해주신 코드를 다시 차근차근 보면서 스스로 해석해보는 시간을 가졌습니다.

https://codelabs.developers.google.com/codelabs/first-flutter-app-pt2#0

 

Write Your First Flutter App, part 2  |  Google Codelabs

Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. In this codelab, you’ll extend a simple mobile app to add interactivity, navigation, and change its theme color.

codelabs.developers.google.com



1. Add icons to the list


저번에 준비했던 Write your first app의 2번째 단계입니다.

저번에 만든 리스트에서 좋아요/좋아요 취소 , 좋아요 리스트 등이 추가되었습니다.

 

이 기능을 만들기 위해서 필요한 것은 좋아요 버튼, 그리고 버튼을 눌렀을 때와 누르지 않았을 때 구분

좋아요 리스트를 저장할 리스트 등이 있습니다.

 

특히 리스트는 중복되게 담지 않아야 해서 살짝 다르게 처리되었는데

아래 코드에서 자세하게 다루겠습니다.

 

결과 화면

더보기

여기까지 코드입니다.

_saved 리스트를 보시면 위에 suggestions랑 다르게 {} 컬 리브 라켓인데

[]는 단순 list인데 반해서 {}은 map (dictionary로 칭하는 언어도 있습니다)을 의미합니다.

중복되지 않은 것들만 들어올 수 있는 set을 구현하는 게 목적이므로

map을 사용해서 좀 더 우리가 편하게 다루었습니다.

 

이외에 코드에 특이한 점은 없고 삼항 연산자를 통해 이미 있는 아이콘과 색상을 추가하였습니다.

// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

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

class RandomWords extends StatefulWidget {
  const RandomWords({ Key? key }) : super(key:key) ;
  @override
  _RandomWordsState createState() => _RandomWordsState() ;
}

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[] ;
  final _saved = <WordPair>{} ;
  final _biggerFont = const TextStyle(fontSize: 18.0) ;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Startup Name Generator'),
      ),
      body: ListView.builder
        (
        padding: const EdgeInsets.all(16.0),
        itemBuilder: /*1*/ (context, i)
        {
          if(i.isOdd) return const Divider() ; /*2*/

          final index = i ~/ 2 ; /*3*/
          if(index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10)) ;/*4*/
          }

          final alreadySaved = _saved.contains(_suggestions[index]);

          return ListTile
            (
            title: Text
              (
              _suggestions[index].asPascalCase,
              style: _biggerFont,
            ),
            trailing: Icon
              (
              alreadySaved ? Icons.favorite : Icons.favorite_border,
              color: alreadySaved ? Colors.red : null,
              semanticLabel: alreadySaved ? 'Remove from saved' : 'Save',
            ),
          ) ;
        },
      ),
    ) ;
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Startup Name Generator',
      home: RandomWords(),
    );
  }
}


2. Add interactivity


여기서는 눌렀을 때 set에 있는 항목이라면 추가, 없는 항목이라면 삭제를 진행합니다.

코드도 특별할 것 없고 Widget에 내장되어있는 onTap 기능을 사용합니다.

/*
_RandomWordsState의 Widget 아래에 body의 trailing 부분 바로 아래에 달아서 쓰시면 됩니다.
trailing : Icon
(
	~~~~
),
*/
onTap: ()
{
  setState(() {
    if(alreadySaved)
      {
        _saved.remove(_suggestions[index]) ;
      }
    else
      {
        _saved.add(_suggestions[index]) ;
      }
  });
},

결과 화면



3. Navigate to a new screen


좋아요를 누른 리스트들을 따로 보는 페이지를 구현하는 부분입니다.

여기서 Flutter의 재밌는 점을 하나 더 알 수 있습니다.

 

플라터는 내비게이션 기능을 지원하는데 모든 Widget(페이지)들이 Stack구조로 저장됩니다.

그래서 새로운 페이지로 넘어가는 것도 Widget으로 Stack구조 상단에 쌓이고

만약 뒤로 가기를 누르면 Stack 상단의 내용물을 빼내게 되어 가장 최근, 바로 뒷페이지로 돌아가기가 가능해지는 것이죠

 

내가 이때 동안 배운 자료구조 중에서 가장 단순한 구조가 바로 이런 곳에서 쓰인다는 것이 참 놀라웠습니다.

아.. 내가 이렇게 바로 옆에 자연스럽게 있어서 몰랐구나 하는 생각을 하였습니다.

내비게이터는 가장 가까운 것을 찾고, 여러 개를 사용할 수 있습니다.

 

나중에 응용하여 tree 구조로 생각하여 sub main을 정해주면

최상단 내비게이터를 참조하는 것이 아니라 중간 내비게이션을 참조하여

완전 메인 세션이 아닌 중간 메인 세션으로 돌아갈 수 있도록 지정할 수 있다는 걸 알게 됐습니다!

더보기

class _RandomWordsState 상단부에는 첫 번째 코드를 삽입하고 내부에 함수로 따로 2번째 코드를 삽입합니다.

 

1.

class _RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Startup Name Generator'),
        // Add from here ...
        actions: [
          IconButton(
            icon: const Icon(Icons.list),
            onPressed: _pushSaved,
            tooltip: 'Saved Suggestions',
          ),
        ],
        // ... to here
      ),
      body: _buildSuggestions(),
    );
  }
  ...
}

2.

  void _pushSaved() {
    Navigator.of(context).push(
      // Add lines from here...
      MaterialPageRoute<void>(
        builder: (context) {
          final tiles = _saved.map(
            (pair) {
              return ListTile(
                title: Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = tiles.isNotEmpty
              ? ListTile.divideTiles(
                  context: context,
                  tiles: tiles,
                ).toList()
              : <Widget>[];

          return Scaffold(
            appBar: AppBar(
              title: const Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ), // ...to here.
    );
  }

4. Change the UI using themes


마지막으로 UI 색상을 변경합니다.

theme의 장점은 수백 페이지가 되더라도 이 한 줄만 고쳐놓으면 모든 페이지에 적용됩니다!

정말 코드 자체도 그렇게 길지 않은데 추가되어 테마가 완전하게 변합니다!

마지막 단계라서 모든 코드를 추가해놓았습니다.

또한 마지막에 추가된 부분만 맨 위에 /***/의 주석을 추가해놓았습니다.

더보기

 

// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
      /******************추가부분************************
      theme: ThemeData(
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.white,
          foregroundColor: Colors.black,
        ),
      ),
      ***************주석을 삭제하세요*************************/
      home: const RandomWords(),
    );
  }
}

class RandomWords extends StatefulWidget {
  const RandomWords({ Key? key }) : super(key:key) ;
  @override
  _RandomWordsState createState() => _RandomWordsState() ;
}

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _saved = <WordPair>{};

  final _biggerFont = const TextStyle(fontSize: 18.0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Startup Name Generator'),
        actions:
        [
          IconButton
            (
            icon: const Icon(Icons.list),
            onPressed: _pushSaved,
            tooltip: 'Saved Suggestions',
          ),
        ],
      ),
      body: ListView.builder
        (
        padding: const EdgeInsets.all(16.0),
        itemBuilder: /*1*/ (context, i) {
          if (i.isOdd) return const Divider();
          /*2*/

          final index = i ~/ 2; /*3*/
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10)); /*4*/
          }

          final alreadySaved = _saved.contains(_suggestions[index]);

          return ListTile
            (
            title: Text
              (
              _suggestions[index].asPascalCase,
              style: _biggerFont,
            ),
            trailing: Icon
              (
              alreadySaved ? Icons.favorite : Icons.favorite_border,
              color: alreadySaved ? Colors.red : null,
              semanticLabel: alreadySaved ? 'Remove from saved' : 'Save',
            ),
            onTap: () {
              setState(() {
                if (alreadySaved) {
                  _saved.remove(_suggestions[index]);
                }
                else {
                  _saved.add(_suggestions[index]);
                }
              });
            },
          );
        },
      ),
    );
  }

  void _pushSaved() {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (context) {
          final tiles = _saved.map(
                (pair) {
              return ListTile(
                title: Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = tiles.isNotEmpty
              ? ListTile.divideTiles(
                  context: context,
                  tiles: tiles,
              ).toList()
              : <Widget>[];

          return Scaffold(
            appBar: AppBar(
              title: const Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ),
    );
  }
}