And Brain said,
Flutter, 미운오리였던 Dart의 우아한 날갯짓 - [2] 본문
목차
1. Widget
2. MaterialApp, Scaffold, AppBar
3. Text, Column, Row, Container
4. 상태 관리(setState, Provider)
5. 사용자 입력 처리(Button, TextField, Checkbox, Radio, Switch)
6. 폼 및 유효성 검사(TextFormField)
7. 애니메이션(AnimatedContainer, AnimationController, Tween)
8. 네비게이션과 라우팅(Navigator, routes)
9. 비동기 처리(Future, async, await, FutureBuilder)
오늘은 Flutter의 기초 이론에 대해서 알아보는 시간을 가져보자.
1. Widget
Flutter에서는 화면 구성 요소를 위젯(widget)이라고 부른다.
모든 UI 요소는 작은 위젯들의 결합이다.
자, 그러면 StatelessWidget과 StatefulWidget에 대해 이해해보자.
Flutter는 기본적인 두 가지 위젯이 있는데, StatelessWidget은 상태를 가지지 않는 위젯으로, 한 번 생성되면 변경되지 않는 UI를 표현하게 된다.
StatefulWidget은 상태를 가지는 위젯으로, 상태가 변경되면 UI가 업데이트된다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Hello Flutter')),
body: Center(child: Text('Welcome to Flutter!')),
),
);
}
}
2. MaterialApp, Scaffold, AppBar
MaterialApp은 앱의 최상위 위젯으로, Material Design(Google이 개발한 디자인 시스템) 애플리케이션을 구현하는 데 사용된다.
Scaffold 앱의 기본 레이아웃을 제공하는 위젯으로, 주로 appBar, body, floatingActionButton 등의 속성을 사용한다.
AppBar는 앱의 상단에 있는 바를 나타내는 위젯이다.
3. Text, Column, Row, Container
Text는 텍스트를 표시하는 위젯으로, style 속성을 사용하여 텍스트 스타일을 지정할 수 있다.
Column은 자식 위젯들을 세로 방향으로 배치하는 위젯이고 Row는 자식 위젯들을 가로 방향으로 배치하는 위젯이다.
Container는 스타일과 여백을 적용할 수 있는 상자 형태의 위젯.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Basic Widgets')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Hello, Flutter!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
color: Colors.red,
margin: EdgeInsets.all(8),
),
Container(
width: 100,
height: 100,
color: Colors.green,
margin: EdgeInsets.all(8),
),
],
),
],
),
),
);
}
}
4. 상태 관리
Frontend의 상태 관리의 중요성은 말하면 입 아프기 때문에 넘어가겠다.
당연히 Flutter 앱에서도 상태 관리는 중요한 부분이므로 간단히 setState와 Provider 패키지에 대해 알아보자.
setState는 StatefulWidget에서 상태를 변경하고 UI를 업데이트하는 데 사용되는데, 상태를 변경할 때 setState 함수 내부에서 상태를 업데이트하면 Flutter는 해당 위젯을 다시 빌드하여 UI를 갱신한다
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter = _counter + 1;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('setState Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
.
다음은, Provider 패키지로 상태를 쉽게 공유하고 관리할 수 있도록 도와주는 상태 관리 패키지다.
이 패키지를 이용해 간단한 카운터 앱을 만들어 보자.
먼저 pubspec.yaml 파일에서 Provider 패키지를 추가한다.
dependencies:
flutter:
sdk: flutter
provider: ^6.0.1
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Provider Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Consumer<Counter>(
builder: (context, counter, child) => Text(
'${counter.count}',
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).incrementCounter();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void incrementCounter() {
_count++;
notifyListeners();
}
}
Counter라는 클래스를 생성하고, ChangeNotifier를 사용하여 상태 변경을 통지하며, Consumer 위젯을 사용하여 카운터 값을 업데이트하고, Provider.of<Counter>(context, listen: false)를 사용하여 카운터 상태를 변경하게 된다.
5. 사용자 입력 처리
이번에는 앱이 사용자와 상호작용할 수 있도록 사용자 입력을 처리해보자.
버튼은 사용자가 터치하여 액션을 실행할 수 있는 기본적인 입력 위젯으로, Flutter에서는 다양한 버튼 스타일을 제공한다.
가장 일반적인 버튼은 ElevatedButton, TextButton, IconButton, OutlinedButton 등이 존재한다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Buttons Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
print('ElevatedButton pressed');
},
child: Text('ElevatedButton'),
),
TextButton(
onPressed: () {
print('TextButton pressed');
},
child: Text('TextButton'),
),
IconButton(
onPressed: () {
print('IconButton pressed');
},
icon: Icon(Icons.add),
),
OutlinedButton(
onPressed: () {
print('OutlinedButton pressed');
},
child: Text('OutlinedButton'),
),
],
),
),
),
);
}
}
텍스트 입력을 위해 Flutter에서는 TextField 위젯을 사용한다.
TextField는 사용자가 텍스트를 입력하고, 수정하고, 선택할 수 있는 기본적인 입력 위젯이다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('TextField Example')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username',
),
),
SizedBox(height: 16),
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
obscureText: true,
),
],
),
),
),
);
}
}
Flutter에서는 또한 체크박스(Checkbox), 라디오 버튼(Radio), 스위치(Switch) 등의 다양한 선택 입력 위젯을 제공한다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isChecked = false;
int _radioValue = 0;
bool _isSwitched = false;
void _handleRadioValueChange(int value) {
setState(() {
_radioValue = value;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Checkbox, Radio, Switch Example')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Checkbox(
value: _isChecked,
onChanged: (bool value) {
setState(() {
_isChecked = value;
});
},
),
Text('Checkbox'),
],
),
Row(
children: [
Radio(
value: 0,
groupValue: _radioValue,
onChanged: _handleRadioValueChange,
),
Text('Radio 1'),
Radio(
value: 1,
groupValue: _radioValue,
onChanged: _handleRadioValueChange,
),
Text('Radio 2'),
],
),
Row(
children: [
Switch(
value: _isSwitched,
onChanged: (bool value) {
setState(() {
_isSwitched = value;
});
},
),
Text('Switch'),
],
),
],
),
),
),
);
}
}
6. 폼 및 유효성 검사
Flutter에서 폼을 구성하고 유효성 검사를 수행하는 방법을 살펴보자.
Form 위젯을 사용하여 폼을 생성하고, TextFormField를 사용하여 입력 필드를 추가해보자.
Form은 폼 안의 모든 TextFormField 위젯을 관리하고 유효성 검사를 수행할 수 있게 도와준다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Form Example')),
body: Padding(
padding: EdgeInsets.all(16),
child: MyForm(),
),
),
);
}
}
class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Username'),
validator: (value) {
if (value.isEmpty) {
return 'Please enter your username';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
// Form is valid, perform submit action
print('Form is valid');
}
},
child: Text('Submit'),
),
],
),
);
}
}
validator 속성을 사용하여 각 TextFormField의 유효성 검사 함수를 지정한다.
함수는 입력 값에 대해 유효성 검사를 수행하고, 오류 메시지를 반환하거나 (유효하지 않은 경우) null을 반환하게 된다 (유효한 경우).
마지막으로, 제출 버튼을 누르면 _formKey.currentState.validate()를 호출하여 폼의 모든 필드에 대한 유효성 검사를 수행하게 된다.
7. 애니메이션
Flutter에서는 다양한 애니메이션 효과를 쉽게 구현할 수 있다.
먼저, AnimatedContainer 위젯을 사용하여 간단한 애니메이션을 구현해보자.
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Animation Example')),
body: Center(
child: MyAnimatedContainer(),
),
),
);
}
}
class MyAnimatedContainer extends StatefulWidget {
@override
_MyAnimatedContainerState createState() => _MyAnimatedContainerState();
}
class _MyAnimatedContainerState extends State<MyAnimatedContainer> {
double _width = 100;
double _height = 100;
Color _color = Colors.red;
BorderRadiusGeometry _borderRadius = BorderRadius.circular(8);
void _randomize() {
setState(() {
_width = Random().nextDouble() * 200;
_height = Random().nextDouble() * 200;
_color = Color.fromRGBO(
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256),
1,
);
_borderRadius = BorderRadius.circular(Random().nextInt(100).toDouble());
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _randomize,
child: AnimatedContainer(
width: _width,
height: _height,
decoration: BoxDecoration(
color: _color,
borderRadius: _borderRadius,
),
duration: Duration(seconds: 1),
curve: Curves.easeInOut,
),
);
}
}
AnimatedContainer 위젯을 사용하여 너비, 높이, 색상 및 모서리 반경을 무작위로 변경하는 애니메이션을 생성하였다.
사용자가 위젯을 탭하면 _randomize 함수가 호출되어 애니메이션 효과가 실행된다.
이제, AnimationController와 Tween을 사용하여 고급 애니메이션을 구현해보자.
이번에는 로고를 무한히 회전시키는 애니메이션을 만들어 보자.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Advanced Animation Example')),
body: Center(
child: RotationLogo(),
),
),
);
}
}
class RotationLogo extends StatefulWidget {
@override
_RotationLogoState createState() => _RotationLogoState();
}
class _RotationLogoState extends State<RotationLogo>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0, end: 2 * pi).animate(_animationController)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.repeat();
}
});
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: _animation.value,
child: Container(
width: 100,
height: 100,
child: Image.network('https://flutter.dev/images/favicon.png'),
),
);
}
}
SingleTickerProviderStateMixin을 사용하여 애니메이션 컨트롤러의 vsync를 제공한다.
initState 메서드에서 애니메이션 컨트롤러를 초기화하고, Tween을 사용하여 0에서 2π 사이의 값을 지정한 다음 애니메이션의 상태를 처리하고 애니메이션이 완료되면 애니메이션을 반복하게 된다.
build 메서드에서 Transform.rotate를 사용하여 로고 이미지를 회전시키고, _animation.value를 사용하여 회전 각도를 설정한다.
이렇게 로고 이미지가 무한히 회전하는 애니메이션까지 만들어 보았다.
AnimationController와 Tween을 사용하면 더 복잡한 애니메이션도 구현할 수 있으니 여러분들도 해보시길 바란다.
8. 네비게이션과 라우팅
Flutter에서는 애플리케이션의 화면 간 이동 및 라우팅을 쉽게 구현할 수 있다.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
child: Text('Go to Second Screen'),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back to First Screen'),
),
),
);
}
}
이 예제에서는 Navigator.push를 사용하여 첫 번째 화면에서 두 번째 화면으로 이동하고, Navigator.pop를 사용하여 두 번째 화면에서 첫 번째 화면으로 돌아간다.
또한, 애플리케이션의 라우트를 관리하기 위해 라우트 이름을 사용할 수 있는데,
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => FirstScreen(),
'/second': (context) => SecondScreen(),
},
);
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/second');
},
child: Text('Go to Second Screen'),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back to First Screen'),
),
),
);
}
}
routes 매개변수를 사용하여 애플리케이션의 라우트 이름과 빌더 함수를 지정하여, Navigator.pushNamed와 Navigator.pop를 사용하여 라우트 이름을 사용하여 화면 간 이동을 수행할 수 있게 된다.
이렇게 라우트를 사용하면, 앱의 전체적인 네비게이션 구조를 한눈에 파악할 수 있고, 특정 화면 간 이동을 보다 간편하게 처리할 수 있다.
9. 비동기 처리
Flutter에서는 비동기 처리를 위해 Future, async, await를 사용하여 비동기 코드를 쉽게 작성하고 관리할 수 있다.
그럼 먼저, Future를 사용하여 비동기 작업을 수행하는 방법을 살펴보자.
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 3));
return '이리오너라!';
}
위 예제에서 fetchData 함수는 3초 후에 '이리오너라!' 문자열을 반환하는 비동기 작업을 수행하게 된다.
다음은, async와 await를 사용하여 비동기 작업을 수행하고 결과를 기다린 후 처리하는 예제를 한 번 살펴보자.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Async Example')),
body: AsyncExample(),
),
);
}
}
class AsyncExample extends StatefulWidget {
@override
_AsyncExampleState createState() => _AsyncExampleState();
}
class _AsyncExampleState extends State<AsyncExample> {
String _data = 'No data yet';
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 3));
return '이리오너라!';
}
void _updateData() async {
String data = await fetchData();
setState(() {
_data = data;
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_data),
SizedBox(height: 16),
ElevatedButton(
onPressed: _updateData,
child: Text('Fetch Data'),
),
],
),
);
}
}
이 예제에서 fetchData 함수는 3초 후에 '이리오너라!' 문자열을 반환하는 비동기 작업을 수행하고, 버튼을 누르면 _updateData 함수가 호출되어 비동기 작업이 수행되고 결과를 기다린 후 _data 변수를 업데이트하게 된다.
마지막으로, FutureBuilder를 사용하여 비동기 작업을 수행하고 그 결과를 기반으로 UI를 빌드하는 예제까지 살펴보자.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('FutureBuilder Example')),
body: FutureBuilderExample(),
),
);
}
}
class FutureBuilderExample extends StatelessWidget {
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 3));
return '이리오너라!';
}
@override
Widget build(BuildContext context) {
return Center(
child: FutureBuilder<String>(
future: fetchData(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('Data: ${snapshot.data}');
}
},
),
);
}
}
이 예제에서 FutureBuilder 위젯을 사용하여 비동기 작업을 수행하고 그 결과를 기반으로 UI를 빌드한다.
fetchData 함수는 3초 후에 '이리오너라!' 문자열을 반환하는 비동기 작업을 수행하며, FutureBuilder의 builder 메서드에서는 AsyncSnapshot 객체를 사용하여 비동기 작업의 상태와 결과에 따라 다른 UI를 빌드하게 된다.
끝으로,
이 글을 통해 Flutter에 대한 기본적인 개념, 문법, 상태 관리, 사용자 입력 처리 등의 주제를 살펴보았다.
공식 문서가 아주 친절하게 작성되어 있으니 여러분들도 한 번 직접 찾아보시면 크게 도움이 될 것이다.
Flutter는 지속적으로 발전하고 있는 프레임워크이기 때문에 항상 최신 정보와 업데이트를 주시하는 것이 좋다.
공식 문서, 커뮤니티, 블로그 등 다양한 자료를 참고하여 여러분의 지식을 더욱 확장해 나가시길 바란다.
당신의 앱 개발 프로젝트의 성공을 기원하며,
Thanks for watching, Have a nice day.
References
https://docs.flutter.dev/development/ui/widgets-intro
https://docs.flutter.dev/development/ui/layout
https://flutter.dev/docs/development/ui/interactive
https://flutter.dev/docs/cookbook/forms/text-input
https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro
https://docs.flutter.dev/development/ui/animations
https://docs.flutter.dev/development/ui/navigation
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
'IT > Flutter' 카테고리의 다른 글
Flutter, 미운오리였던 Dart의 우아한 날갯짓 (0) | 2023.03.06 |
---|