# Stateless Widget
내부 변수의 변경 사항이 다른 메서드에 반영되지 못하는 특성을 가지고 있다.
아직까지의 파악으로는 UI의 디자인을 전게해나아는데 특화해서 사용하는 Widget으로 파악된다.
# StatefulWidget
통상 State Widget을 내부 객체로 포함하며,
State Widget의 내부 변수의 변경사항에 setState() 선행 매서드를 통해 다른 구성요소에 전달되도록 하는 메카니즘을 가지고 있다.
# Stateless Widget과 StatefulWidget(State)의 예제
MyApp이라는 Flutter Tutorial에 두번째로 출현하는 예제이다. 여기서, MyApp자체가 StatelessWidget을 상속받아서 생성되어 있다.(뭐 딱히 동적으로 변경해야할 변수들을 가지고 있지 못하다.) 그리고 StatelessWidget의 build 메서드를 override(덮어 씌우기)해서 새롭게 MaterialApp을 생성하고 있다.
(MaterialApp은 구글이 밀고 있는 Material Design을 계승하는 Widget이라고 한다.)
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
/*
void main() {
runApp(MyApp());
}
*/
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
primaryColor: Colors.white,
),
home: RandomWords(),
);
}
}
아직 Dart언어를 모르기 때문에 하나 짚고 넘어가야할 사항이 있다. main 함수의 형태이다.
주석으로 처리되어 있는 main함수는 익히 알고 있는 C 언어 형태의 서술이고 아래의 '=>'로 엮여진 main함수는 아마도 한줄짜리로 표현한 Dart 언어 문법인 것 같다. (뭐 시작을 C언어와 유사하다고 해서 들여다 보고 있는 형편이라서 과감하게 추정해 보자면 Lambda 함수인것 같다.)
MaterialApp의 home 섹션은 RandomWords라는 Widget으로 들어차게 만든다.
class RandomWords extends StatefulWidget {
@override
RandomWordsState createState() => RandomWordsState();
}
자 여기서, RandomWords가 StatefulWidget으로 등장하고 있다. 또 override를 수행한다. 앞에서와 다르게 여긴 createState()로 구현한다. 또 과감하게 추정하자면 둘다 이름이 다를뿐 일종의 생성자 같다. 물론 부모의 기능을 초기화해야 해서 생성자 전체를 override하진 않는 것같다. 아마도 StatelessWidget과 StatefulWidget의 생성자 내부에는 여러개의 Method로 이루어져 있는 것 같다. 그 중 하나를 override하고 있다고 추정하는 바이다.
여기까지, StatelessWidget과 StatelessWidget의 구성이다. 각각 build()와 createState()라는 override를 통해서 구현하고 있다. 이 둘의 이름에서 알 수 있듯이 State가 없고 있고의 차이가 있다.
아직 설명 안된 구석이 있는데 그것은 RandomWordState라는 Widget이다.
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Startup Name Generator'),
actions: [
IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
]
),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: /*1*/ (context, i) {
if (i.isOdd) return Divider(); /*2*/
final index = i ~/ 2; /*3*/
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,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
}
);
}
void _pushSaved() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
final tiles = _saved.map((WordPair pair) {
return ListTile(
title: Text(pair.asPascalCase, style: _biggerFont)
);
});
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return Scaffold(
appBar: AppBar(
title: Text('Saved Suggestions'),
),
body: ListView(children: divided),
);
},
),
);
}
}
길다. RandomWordsState는 예상했듯이 State로 부터 상속받아서 생성된다. 여기서, 의문점은 State<RandomWords>라는 구문이다. 내가 아는 C언어 선입견을 동원하면 State class를 RandomWords의 타입을 같도록 생성한 template인 것 같다. 말이 어렵다 State가 RandomWords라는 내부 항목을 가지고 작업할 것이다라고 이해하자.
아직 정체가 불확실한 final로 시작하는 변수 생성 이것도 마찬가지 그냥 넘어가자. 그냥 변수다라고만 인지하고 나중에 차차 살펴보자.
첫 번째 Method인 build()는 StatelessWidget에서 보았듯이 일종의 생성자 중 하나라고 보자. 여기서, Scaffold라는 MaterialApp과는 다른 Widget을 만들고 있다. 새로운것들이 많이 등장한다. 우선은 Scaffold라는 것은 AppBar라는 일종의 윈도우의 title bar와 내부항목에 해당하는 body라는 섹션을 가지고 있다.
먼저 AppBar는 문자로된 title을 가지고 있고 그에 해당하는 동작을 actions에 담고 있다. IconButton을 가지고 뭘 하는 것 같다. 우선 넘어가자. 그리고 body에는 _buildSuggestions()이라는 method로 뭔가 _suggestions이라는 리스트를 만들고 보여주는 것을 수행하는 것 같다.
_buildSuggestions과 엮인 다른 Method는 생략한다. 지친다.
# 전체 코드
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
/*
void main() {
runApp(MyApp());
}
*/
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
theme: ThemeData(
primaryColor: Colors.white,
),
home: RandomWords(),
);
}
}
class RandomWords extends StatefulWidget {
@override
RandomWordsState createState() => RandomWordsState();
}
class RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _saved = Set<WordPair>();
final _biggerFont = const TextStyle(fontSize: 18.0);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Startup Name Generator'),
actions: [
IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
]
),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: /*1*/ (context, i) {
if (i.isOdd) return Divider(); /*2*/
final index = i ~/ 2; /*3*/
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,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
}
);
}
void _pushSaved() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
final tiles = _saved.map((WordPair pair) {
return ListTile(
title: Text(pair.asPascalCase, style: _biggerFont)
);
});
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return Scaffold(
appBar: AppBar(
title: Text('Saved Suggestions'),
),
body: ListView(children: divided),
);
},
),
);
}
}