Flutter - 완전 기초 ③: ListView에 아이콘 추가, 상호작용 추가, 새 화면 이동

3 분 소요


플러터 코드랩을 이어서

  1. 각 아이템에 하트 아이콘을 달고
  2. 아이템을 저장하고
  3. 새 화면으로 이동해서 저장된 것들을 보자


Ex) 무한 스크롤 ListView 아이템 저장해서 모아 보기

State 수정: 아이콘 추가 및 상호작용

RandomWordsState에 final _saved = <WordPair>{};를 추가해서, 단어 짝을 저장하는 셋을 정의한다.

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return ListTile(
      title: Text(
        pair.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(pair);
          } else {
            _saved.add(pair);
          }
        });
      },
    );
  }
}

_buildRow를 수정한다.

trailing: 타일의 뒷 부분을 의미한다. Icon()으로 아이콘을 추가할 수 있다.
alreadySaved_saved에 해당 단어 짝이 이미 있는 지를 확인하는 변수다. 아이콘을 상태에 따라 삼항 연산자로 지정해준 것을 볼 수 있다.
alreadySaved인 경우 꽉 찬 하트, 아닌 경우 테두리만 있는 하트이며, color는 alreadySaved인 경우 붉은 색이다. 또, semanticLabel은 alreadySaved인 경우 이미 있는 상태이니까 saved에서 지움을 표시하고, 아닌 경우 이제 저장할 거니까 save를 표시한다.
+ leading은 타일의 앞 부분

onTap: 타일이 눌러졌을 때, setState()로 바뀐 상태를 다시 그려준다.


새 화면 이동

이제 위쪽 바에 목록 아이콘 버튼 ≡을 누르면 저장된 단어 짝들을 볼 수 있는 다른 화면으로 이동하자

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _biggerFont = const TextStyle(fontSize: 18.0);
  final _saved = <WordPair>{};
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
        actions: [
          IconButton(
            icon: const Icon(Icons.list),
            onPressed: _pushSaved,
            tooltip: 'Saved Suggestions',
          ),
        ],
      ),
      body: _buildSuggestions(),
    );
  }

Appbar 안에 actions를 추가해서, IconButton을 넣는다.

onPressed: 눌렸을 때의 행동을 정의한다. 여기서는 _pushSaved 함수를 넘겨줬다.

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),
          );
        },
      ),
    );
  }

Navigator.of(context).push()로 패스를 네비게이터 스택으로 푸시한다.
MaterialPageRoutebuilder에 새 페이지를 빌드한다.

tiles_saved 셋에 있는 단어 짝들을 .map()으로 ListTile 타입들로 변환해서 저장한다.

divided에는 tiles가 비어있지 않은 경우 ListTile.divideTiles()로 구분선이 있게 저장하고, 비어 있을 경우 빈 배열을 저장한다.


전체 코드

import 'package:flutter/material.dart';
import 'package:english_words/english_words.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: 'Temp',
      home: RandomWords(),
    );
  }
}

class HeaderTile extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Image.network("https:..."),
    );
  }
}

class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _biggerFont = const TextStyle(fontSize: 18.0);
  final _saved = <WordPair>{};
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
        actions: [
          IconButton(
            icon: const Icon(Icons.list),
            onPressed: _pushSaved,
            tooltip: 'Saved Suggestions',
          ),
        ],
      ),
      body: _buildSuggestions(),
    );
  }

  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),
          );
        },
      ),
    );
  }

  Widget _buildSuggestions() {
    return ListView.builder(
        padding: const EdgeInsets.all(16.0),
        itemBuilder: (context, i) {
          if( i == 0 ) return HeaderTile();
          if( i % 2 == 1 ) return Divider();
          final index = i ~/ 2;
          if( index >= _suggestions.length ){
            _suggestions.addAll(generateWordPairs().take(10));
          }
          return _buildRow(_suggestions[index]);
        });
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return ListTile(
      title: Text(
        pair.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(pair);
          } else {
            _saved.add(pair);
          }
        });
      },
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  RandomWordsState createState() => RandomWordsState();
}


실행 화면

6
잘 동작한다.


Ex) 아이콘을 눌렀을 때만 저장 되기

위 예제는 타일 아무데나 눌러도 저장이 된다. 그런데 하트 아이콘을 누르면 저장은 되어야 하지만, 타일 자체를 누르면 상세 페이지가 떠야 하는 등의 상황을 위해 코드를 고쳐 보자

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair);
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: IconButton(
        icon: Icon(
          alreadySaved ? Icons.favorite : Icons.favorite_border,
          color: alreadySaved ? Colors.red : null,
          semanticLabel: alreadySaved ? 'Remove from saved' : 'Save',
        ),
        onPressed: (){
          setState(() {
              if (alreadySaved) {
                _saved.remove(pair);
              } else {
                _saved.add(pair);
              }
            });
          },
      ),
      onTap: (){
        print("Pressed " + pair.asPascalCase);
      },
    );
  }
}

_buildRow()에서 trailing에 Icon으로 정의한 하트 아이콘을 IconButton으로 만들면 된다. icon에서 Icon()으로 같은 내용을 적어 주면 상황에 따라 계속 변하는 아이콘 버튼을 만들 수 있다.

임시로 그냥 타일을 눌렀을 때는 “Pressed 단어 짝”을 출력하기로 하자


실행 화면

7
타일을 눌렀을 때는 아이콘의 변화가 없지만, 아이콘을 눌렀을 때만 하트 아이콘이 변화가 있고 목록에도 저장된다.

8
눌렀을 때의 출력 화면이다.



이 정도면 대충 감은 잡히는 것 같다
다른 자세한 사항들은 이제 프로젝트 진행하면서 알아보면 될 것 같다~~

태그:

카테고리:

업데이트:

댓글남기기