Flutter - 완전 기초 ③: ListView에 아이콘 추가, 상호작용 추가, 새 화면 이동
플러터 코드랩을 이어서
- 각 아이템에 하트 아이콘을 달고
- 아이템을 저장하고
- 새 화면으로 이동해서 저장된 것들을 보자
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()
로 패스를 네비게이터 스택으로 푸시한다.
MaterialPageRoute
의 builder
에 새 페이지를 빌드한다.
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();
}
실행 화면
잘 동작한다.
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 단어 짝”을 출력하기로 하자
실행 화면
타일을 눌렀을 때는 아이콘의 변화가 없지만, 아이콘을 눌렀을 때만 하트 아이콘이 변화가 있고 목록에도 저장된다.
눌렀을 때의 출력 화면이다.
이 정도면 대충 감은 잡히는 것 같다
다른 자세한 사항들은 이제 프로젝트 진행하면서 알아보면 될 것 같다~~
댓글남기기