Form 위젯
- TextField는 단순히 하나의 텍스트 입력을 다루는데 반해서, Form은 그 자체로 입력 필드를 가지고 있지 않지만, FormField 위젯들을 그룹화하여 관리하며, 복잡한 유효성 검사와 보다 쉽게 할 수 있는 위젯
- 다른 위젯과 달리 Form 위젯은 자체적인 화면을 제공하지는 않으며, 사용자가 입력한 데이터의 유효성 검증, 데이터 관리 관련 기능을 제공함
- Form 위젯 내에서 TextFormField 위젯을 사용하여 각 데이터 입력을 받는 것이 일반적임
- Form 위젯 작성 방법은 다음과 같음
- Form 위젯을 위한 GlobalKey를 만들어야 함. GlobalKey는 FormState 전체에 액세스하는 데 사용되며, 이 객체는 폼 데이터의 유효성을 검사하고 저장하는 데 사용됨
final _formKey = GlobalKey<FormState>();
- TextFormField 위젯들의 폼 컨트롤을 위젯을 Form 위젯으로 래핑하고, _formKey를 Form 위젯의 key 속성으로 설정
Form(
key: _formKey,
child: Column(
children: [
// 폼 컨트롤들
],
),
);
- TextFormField 위젯 또는 다른 폼 컨트롤을 Form 위젯에 추가합니다. 각 폼 컨트롤은 validator 함수와 선택적인 onSaved 함수를 가져야 함
- validator 함수는 사용자가 폼을 제출할 때 호출되며, 입력이 유효한지 확인.
- 입력이 유효하지 않으면 validator는 오류 메시지를 포함하는 문자열을 반환해야 함.
- 입력이 유효하면 validator는 null을 반환해야 함.
- onSaved 함수는 폼이 저장될 때 호출되며 사용자가 입력한 값을 변수나 데이터 모델에 저장해야 함.
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
),
validator: (value) {
if (value == null || value.isEmpty) {
return '이메일을 입력하세요';
}
return null;
},
onSaved: (value) {
_email = value;
},
),
- 폼에 제출 버튼을 추가하고 폼 제출을 처리할 함수를 정의
- _submitForm 함수는 FormState 객체의 validate 메서드(_formKey.currentState!.validate())를 사용하여 폼 유효성을 검사하고
- FormState 객체의 save 메서드(_formKey.currentState!.save())를 사용하여 폼 데이터를 저장해야 함
- _formKey.currentState!에서 _formKey는 GlobalKey 객체이고, 여기에서 currentState는 FormState 객체임
- 해당 객체가 없지 않다는 것을 명시적으로 알려주기 위해 !(Exclamation mark)를 기재한 것임
- 폼 데이터가 유효하면 _submitForm 함수는 데이터를 의도한대로 처리하게 됨
import 'package:flutter/material.dart';
void main() {
runApp(MyApp8());
}
class MyApp8 extends StatefulWidget {
const MyApp8({super.key});
@override
State<MyApp8> createState() => _MyApp8State();
} // end of class
class _MyApp8State extends State<MyApp8> {
// Form 위젯 만들어 보기
final _formKey = GlobalKey<FormState>();
String _name = '';
String _email = '';
String _password = '';
String _errorMessage = '';
// 멤버 함수 만들어보기
void _submitForm() {
print('_formKey.currentState!.validate() : ${_formKey.currentState!.validate()}');
if(_formKey.currentState!.validate()) {
// TextFormField --> validator 호출 --> 모두 통과 하면 true 를 반환 한다.
// 다음단계 --> onSaved 메서드를 실행 시킨다.
_formKey.currentState!.save(); // 각각에 formfield onSaved 메서드 호출 됨
setState(() {
_errorMessage = ''; // 상태 변경 처리
// 변수안에 값 확인 -->
print('_name : $_name');
print('_email : $_email');
print('_password : $_password');
// 통신 요청
//http.get(~);
//http.post(~);
// 응답 받아서 화면 이동 처리 , 메세지 던져 주기
});
} else {
setState(() {
_errorMessage = '필수값들을 입력하시오';
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Form Example'),
),
body: Container(
padding: const EdgeInsets.all(16.0),
// form 위젯은 위젯들을 구분할 수 있는 키가 필요하다.
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'name',
errorStyle: TextStyle(color: Colors.blue, fontSize: 10),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '이름을 입력하세요';
}
return null;
},
// value 매개변수 값이 null 이 될 수 있다.
onSaved: (value) {
// value! 강제 null 아니라고 명시 함
_name = value!;
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'email',
errorStyle: TextStyle(color: Colors.blue, fontSize: 10),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '이메일을 입력하세요';
}
return null;
},
// value 매개변수 값이 null 이 될 수 있다.
onSaved: (value) {
// value! 강제 null 아니라고 명시 함
_email = value!;
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'password',
errorStyle: TextStyle(color: Colors.blue, fontSize: 10),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '비밀번호을 입력하세요';
}
return null;
},
// value 매개변수 값이 null 이 될 수 있다.
onSaved: (value) {
// value! 강제 null 아니라고 명시 함
_password = value!;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _submitForm, // 코드 축약
child: Text('Submit'),
),
const SizedBox(height: 20),
Text(
_errorMessage,
style: TextStyle(color: Colors.red),
),
],
),
),
),
),
);
}
} // end of class
Form 위젯 연습
코드 임시 추가 !
import 'package:flutter/material.dart';
void main() {
runApp(FeedbackFormApp());
}
// 한 파일안에 여러개의 클래스를 만들 수 있다.
class FeedbackFormApp extends StatelessWidget {
const FeedbackFormApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.redAccent),
),
home: FeedbackForm(),
);
}
}
// 상태 변경 가능한 UI 선언
class FeedbackForm extends StatefulWidget {
const FeedbackForm({super.key});
@override
State<FeedbackForm> createState() => _FeedbackFormState();
}
class _FeedbackFormState extends State<FeedbackForm> {
final _formKey = GlobalKey<FormState>(); // 폼 상태를 추적하기 위한 글로벌 키
int _rating = 0;
bool _subscribe = false;
String _name = '';
String _email = '';
String _comments = '';
String _successMessage = '';
double _satisfaction = 0.0;
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('FeedbackForm'),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
),
body: SingleChildScrollView(
// formField 터치시 소프트 키보드가 올라 옴. 여백 공간 적절히 줘야 함
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: '이름',
hintText: '홍길동',
border: OutlineInputBorder()),
validator: (value) {
// 입력 값 검증
if (value == null || value.isEmpty) {
return '이름을 입력해주세요.';
} else {
// 정상적으로 입력 했다면
return null;
}
}, // end of validator
onSaved: (value) {
_name = value!;
},
),
const SizedBox(height: 16.0),
TextFormField(
decoration: InputDecoration(
labelText: '이메일',
hintText: 'abc@naver.ocm',
border: OutlineInputBorder()),
validator: (value) {
// 입력 값 검증
if (value == null || value.isEmpty) {
return '이메일을 입력해주세요.';
} else {
// 정상적으로 입력 했다면
return null;
}
}, // end of validator
onSaved: (value) {
_email = value!;
},
),
const SizedBox(height: 16.0),
TextFormField(
maxLines: 4, // 여러줄 입력 가능
decoration: InputDecoration(
labelText: '코멘트',
hintText: '경혐을 공유해주세요',
border: OutlineInputBorder()),
validator: (value) {
// 입력 값 검증
if (value == null || value.isEmpty) {
return '코멘트를 입력해주세요.';
} else {
// 정상적으로 입력 했다면
return null;
}
}, // end of validator
onSaved: (value) {
_comments = value!;
},
),
const SizedBox(height: 24),
Text(
'우리 앱을 어떻게 평가하시겠습니까?',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return IconButton(
onPressed: () {
setState(() {
// 버그는 추후 수정
_rating = index + 1; // 선택한 점수로 설정
});
},
color: Colors.deepPurple,
icon: Icon(
_rating > index ? Icons.star : Icons.star_border),
);
}),
),
const SizedBox(height: 24),
Text(
'고객 서비스 만족도는 어느 정도입니까?',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary),
),
Slider(
value: _satisfaction,
// 추후 수정
min: 0,
max: 10,
onChanged: (value) {
setState(() {
print('value $value');
_satisfaction = value!;
});
},
divisions: 10,
// 슬라이더 구간
label: _satisfaction.toString(),
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('뉴스레터를 구독하시겠습니까?'),
value: _subscribe,
onChanged: (value) {
setState(() {
_subscribe = value;
});
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// 통과
_formKey.currentState!.save(); // 실행
// 변수에 값 할당
setState(() {
_successMessage = '제출이 완료 되었습니다';
});
} else {
// 실패
setState(() {
_successMessage = '';
});
}
},
child: const Text('제출'),
),
// 다트 문법 활용
if (_successMessage.isNotEmpty)
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(
_successMessage,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 16
),
),
)
],
),
),
),
),
);
}
}
GlobalKey란 무엇인가요?
GlobalKey는 Flutter에서 위젯 트리 내에서 특정 위젯을 유일하게 식별할 수 있는 키입니다. 이 키를 사용하면 해당 위젯이나 그 상태(State)에 직접 접근하여 조작할 수 있습니다.
<aside> 💡
상태(State)란? 위젯이 현재 가지고 있는 데이터나 속성을 의미하며, 시간이 지남에 따라 변경될 수 있는 값입니다.
StatelessWidget VS StatefulWidget의 차이
StatelessWidget
- 정의: 상태를 가지지 않는 위젯입니다. 내부에 데이터나 속성을 가질 수 있으나 내부에 변경 가능한 데이터나 속성이 없기 때문에 자체적으로 UI가 업데이트되지 않습니다.
- 하지만(추가 설명)
- 외부로부터의 변화: StatelessWidget은 부모 위젯이나 외부에서 전달된 데이터가 변경되면 재빌드되어 UI가 업데이트될 수 있습니다.
- 한정된 업데이트: 하지만 자체적으로 상태를 관리하거나 변경하지 않으므로, 내부적인 상태 변화로 인한 UI 업데이트는 발생하지 않습니다.
StatefulWidget
- 정의: 상태(State)를 가지는 위젯입니다. 시간이 지남에 따라 상태가 변경될 수 있고, 이에 따라 UI도 업데이트됩니다.
- 추가 설명:
- 상태 관리: StatefulWidget은 State 객체를 통해 상태를 관리하며, 상태가 변경될 때마다 setState() 메서드를 호출하여 UI를 갱신합니다.
- 독립성: 자체적으로 상태를 관리하기 때문에, 외부의 변화뿐만 아니라 내부적인 상태 변화에도 대응할 수 있습니다. </aside>
GlobalKey의 주요 기능
- 위젯 식별: 동일한 타입의 위젯이 여러 개 있더라도, GlobalKey를 사용하면 특정 위젯을 구분할 수 있습니다.
- 상태 접근: StatefulWidget의 상태(State)에 직접 접근하여 값을 읽거나 변경할 수 있습니다.
- 위젯 위치 정보 얻기: 위젯의 위치나 크기 등의 정보를 얻어올 수 있습니다.
ListView 사용법과 주요 property
- 가장 일반적으로 사용되는 스크롤 위젯
- ListView는 주로 다음과 같은 방식으로 사용
- 일반적인 ListView를 명시적으로 호출하고 children 전달하는 방법 (적은 데이터에 사용시 용이함)
- ListView.builder를 사용하여 동적으로 호출
- ListView.separated는 ListView.builder 기능에 구분선 사용 가능
- 주요 property
- reverse: true이면 bottom에서부터 리스트 표시
- padding: 리스트 아이템 간격 (EdgeInsets로 적용)
- itemCount: 동적 리스트 아이템 개수 (ListView.builder/ListView.separated에서 사용 가능)
- itemBuilder: 각 동적 리스트 아이템 정의 (ListView.builder/ListView.separated에서 사용 가능)
- physics: 스크롤 방식 설정
ListTile
- Material Design의 리스트 스타일 따르는 위젯
- ListView와 함께 사용하여, 일반적인 스크롤을 지원하는 리스트 메뉴를 표현
ListView.builder 사용 해보기import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: ListView( padding: EdgeInsets.all(10), children: [ ListTile( leading: CircleAvatar( backgroundColor: Colors.amber, child: Text('1'), ), title: Text('Item1'), subtitle: Text('Item description'), trailing: IconButton( icon: Icon(Icons.more_vert), onPressed: () { print('click'); }, ), ), // More ListTile items (2 to 6) as shown in the code ], ), ), ); } }
GridView 위젯- 리스트 뷰와 마찬가지로 항목을 나열하는 위젯이지만, 리스트 뷰와 달리 한 줄에 여러 개를 함께 나열할 수 있음 (그리드 형태)
- 리스트 뷰와 마찬가지로 그리드뷰도 GridView.builder() 생성자를 제공하며,
- itemCount, itemBuilder 속성을 통해 항목의 개수와 위젯을 지정
- gridDelegate 속성을 설정해야 함
- 이 속성에는 SliverGridDelegateWithFixedCrossAxisCount 객체를 지정해줘야 함
- 해당 객체에서 crossAxisCount 값이 한 줄에 함께 나와야 하는 항목의 개수임
- 그리드 두 방향을 설정하지 않으면, crossAxisCount는 가로를 가리킴
- 참고: scrollDirection 속성에 Axis.horizontal을 설정하면, crossAxisCount는 세로 방향을 가리킴
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text('그리드뷰 위젯 연습'), ), body: GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 16, mainAxisSpacing: 16 ), itemCount: 10, itemBuilder: (context, index) { return Container( color: Colors.blueGrey, child: Center(child: Text('index number : $index')), ); }, ), ), ); } }
PageView 위젯- 스와이프 이벤트에 반응하여 항목을 보여주는 위젯
- PageController의 initialPage로 처음 보일 페이지를 설정할 수 있고, viewportFraction으로 현재 페이지가 화면에 차지하는 비율도 설정 가능
- 예: viewportFraction: 0.7이면 현재 페이지가 화면에 70%, 나머지 30%가 이전과 다음 페이지가 살짝 보이게 됨
- PageController 객체를 PageView의 controller 속성에 적용하여, PageController 설정을 반영시킬 수 있음
import 'package:flutter/material.dart'; void main() { runApp(const MyApp1()); } class MyApp1 extends StatelessWidget { const MyApp1({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text('페이지 뷰 위젯 연습'), ), body: PageView( controller: PageController( initialPage: 2, viewportFraction: 0.9 ), children: [ Container( margin: EdgeInsets.all(10), color: Colors.redAccent, ), Container( margin: EdgeInsets.all(10), color: Colors.blueGrey, ), Container( margin: EdgeInsets.all(10), color: Colors.green, ), ], ), ), ); } }
- ListView.separated 사용 해보기
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp1());
}
class MyApp1 extends StatelessWidget {
const MyApp1({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('페이지 뷰 위젯 연습'),
),
body: PageView(
controller: PageController(
initialPage: 2,
viewportFraction: 0.9
),
children: [
Container(
margin: EdgeInsets.all(10),
color: Colors.redAccent,
),
Container(
margin: EdgeInsets.all(10),
color: Colors.blueGrey,
),
Container(
margin: EdgeInsets.all(10),
color: Colors.green,
),
],
),
),
);
}
}
쇼핑 카트 앱 만들어 보기
https://github.com/flutter-coder/flutter-book/tree/master/flutter_shoppingcart
flutter-book/flutter_shoppingcart at master · flutter-coder/flutter-book
Contribute to flutter-coder/flutter-book development by creating an account on GitHub.
github.com
작업 순서
constants.dart 파일 작업 theme.dart 파일 작업 .. header.dart
… details.dart
constants.dart 파일 작업
import 'package:flutter/material.dart';
const kPrimaryColor = MaterialColor(
0xFFeeeeee,
<int, Color>{
50: Color(0xFFeeeeee),
100: Color(0xFFeeeeee),
200: Color(0xFFeeeeee),
300: Color(0xFFeeeeee),
400: Color(0xFFeeeeee),
500: Color(0xFFeeeeee),
600: Color(0xFFeeeeee),
700: Color(0xFFeeeeee),
800: Color(0xFFeeeeee),
900: Color(0xFFeeeeee),
},
);
const kSecondaryColor = Color(0xFFc6c6c6); // 기본 버튼 색
const kAccentColor = Color(0xFFff7643); // 활성화 버튼 색
theme.dart 파일 작업
임시 작업 1 - main.dart 파일
import 'package:flutter/material.dart';
import 'package:flutter_shoppingcart/components/shoppingcart_header.dart';
import 'package:flutter_shoppingcart/theme.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: theme(),
home: SafeArea(child: ShoppingCartPage()),
);
}
}
class ShoppingCartPage extends StatelessWidget {
const ShoppingCartPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildShoppingCartAppBar(),
body: Column(
children: [
ShoppingcartHeader(),
],
),
);
}
// private 메서드 만들어 보기
AppBar _buildShoppingCartAppBar() {
return AppBar(
leading: IconButton(
onPressed: () {},
icon: Icon(Icons.arrow_back),
),
actions: [
IconButton(
onPressed: () {},
icon: Icon(Icons.shopping_cart),
),
SizedBox(width: 16),
],
elevation: 0.0,
);
}
}
임시 작업 2 - ShoppingcartHeader 위젯
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_shoppingcart/constants.dart';
class ShoppingcartHeader extends StatefulWidget {
const ShoppingcartHeader({super.key});
@override
State<ShoppingcartHeader> createState() => _ShoppingcartHeaderState();
}
class _ShoppingcartHeaderState extends State<ShoppingcartHeader> {
int selectedId = 1;
List<String> selectedPic = [
'assets/p1.jpeg',
'assets/p2.jpeg',
'assets/p3.jpeg',
'assets/p4.jpeg',
];
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildHeaderPic(),
_buildHeaderSelector(),
],
);
}
Widget _buildHeaderPic() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: AspectRatio(
aspectRatio: 5 / 3,
child: Image.asset(
selectedPic[selectedId],
fit: BoxFit.cover,
),
),
);
}
Widget _buildHeaderSelector() {
return Padding(
padding: const EdgeInsets.only(left: 30, right: 30, top: 10, bottom: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildHeaderSelectorButton(0, Icons.directions_bike),
_buildHeaderSelectorButton(1, Icons.motorcycle),
_buildHeaderSelectorButton(2, CupertinoIcons.car_detailed),
_buildHeaderSelectorButton(3, CupertinoIcons.airplane),
],
),
);
} // end of _buildHeaderSelector
Widget _buildHeaderSelectorButton(int id, IconData mIcon) {
return Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: id == selectedId ? kAccentColor : kSecondaryColor,
borderRadius: BorderRadius.circular(20.0)),
child: IconButton(
onPressed: () {
setState(() {
selectedId = id;
});
},
icon: Icon(mIcon),
),
);
}
} // end of _ShoppingcartHeaderState
ShoppingcartDetail 위젯
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shoppingcart/constants.dart';
class ShoppingcartDetail extends StatelessWidget {
const ShoppingcartDetail({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(40.0)),
child: Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
children: [
_buildDetailNameAndPrice(),
_buildDetailRatingAndReviewCount(),
_buildDetailColorOptions(),
_buildDetailButton(context)
],
),
),
);
} // end of build
Widget _buildDetailNameAndPrice() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Urban Soft AL 10.0',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
'\$699',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
)
],
);
}
Widget _buildDetailRatingAndReviewCount() {
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
children: [
Icon(Icons.star, color: Colors.yellow),
Icon(Icons.star, color: Colors.yellow),
Icon(Icons.star, color: Colors.yellow),
Icon(Icons.star, color: Colors.yellow),
Icon(Icons.star, color: Colors.yellow),
// row 위젯 기준으로 남은 여백 다 차지
Spacer(),
Text('Review '),
Text(
'(26)',
style: TextStyle(color: Colors.blue),
)
],
),
);
}
Widget _buildDetailColorOptions() {
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Column(
children: [
Text('Color Options'),
SizedBox(height: 10),
Row(
children: [
// 재사용을 위해 함수로 설계
_buildDetailIcons(Colors.black),
_buildDetailIcons(Colors.green),
_buildDetailIcons(Colors.orange),
_buildDetailIcons(Colors.grey),
_buildDetailIcons(Colors.white),
],
)
],
),
);
}
// 여러번 추후 호출 해야 한다.
Widget _buildDetailIcons(Color mColor) {
return Padding(
padding: const EdgeInsets.only(right: 10),
child: Stack(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(),
shape: BoxShape.circle),
),
Positioned(
left: 5,
top: 5,
child: ClipOval(
child: Container(
color: mColor,
width: 40,
height: 40,
),
),
)
],
),
);
}
Widget _buildDetailButton(BuildContext context) {
return Align(
child: TextButton(
onPressed: () {
// alert dialog 사용법
// 플랫폼 별 준비,
// 1. showDialog 호출
// 2. iOS -> showCupertinoDialog 함수
// 3.
showCupertinoDialog(
context: context,
builder: (context) {
return CupertinoAlertDialog(
actions: [
CupertinoDialogAction(
child: Text('확인'),
onPressed: () {
// 플러터 프레임워크가
// 화면간에 이동시에 위젯들을
// 스택구조로 관리해주는 객체이다.
Navigator.pop(context);
},
),
],
);
});
},
style: TextButton.styleFrom(
backgroundColor: kAccentColor,
minimumSize: Size(300, 50),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
child: Text(
'Add to Cart',
style: TextStyle(
color: Colors.white,
),
),
),
);
}
}
BottomSheet 위젯
- 화면 아래쪽에서 올라오는 다이얼로그
- showBottomSheet() 및 showModalBottomSheet()의 builder 속성에 지정한 위젯을 화면 아래쪽에서 올라오도록 보여줌
- showModalBottomSheet()는 사용자가 Bottom Sheet 외부를 터치하면 Bottom Sheet가 자동으로 닫히지만, showBottomSheet()는 사용자가 Bottom Sheet 외부를 터치해도 Bottom Sheet가 닫히지 않음
- 다이얼로그를 닫기 위해서는 닫히게 하려면, Navigator.of(context).pop()을 호출하면 됨
import 'package:flutter/material.dart';
void main() {
runApp(BottomSheetApp());
}
class BottomSheetApp extends StatelessWidget {
const BottomSheetApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'myApp1',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)
),
home: Scaffold(
appBar: AppBar(title: const Text('Bottom Sheet Sample'),),
body: MyBottomSheet(),
),
);
}
}
// 새로운 클래스
class MyBottomSheet extends StatelessWidget {
const MyBottomSheet({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('show bottom sheet'),
onPressed: () {
// 이벤트 핸들러 처리
// showBottomSheet(context: context, builder: builder)
showModalBottomSheet(context: context, builder: (context) {
return Container(
child: Column(
children: [
ListTile(
leading: Icon(Icons.add),
title: Text('Add Item'),
onTap: () {
Navigator.of(context).pop();
},
),
ListTile(
leading: Icon(Icons.remove),
title: Text('Remove Item'),
onTap: () {
Navigator.of(context).pop();
},
)
],
),
);
});
},
),
);
}
}
TabBar 위젯
TabBar와 TabBarView 개념
- TabBar: 여러 개의 탭을 화면에 표시하며, 각 탭을 클릭할 수 있도록 해주는 위젯입니다. 사용자가 탭을 클릭하면 해당 탭에 대한 내용을 표시합니다.
- TabBarView: TabBar와 연결되어 각 탭에 해당하는 내용을 표시하는 역할을 합니다. 사용자가 탭을 선택하면 이 위젯이 해당하는 내용을 보여줍니다.
- TabController
- TabController: 탭의 상태를 관리하는 클래스입니다. 탭의 개수와 현재 활성화된 탭을 추적합니다. SingleTickerProviderStateMixin을 사용하여 애니메이션과 상태 관리를 돕습니다.
- length: 탭의 개수를 지정합니다.
- vsync: 애니메이션의 성능을 최적화하는 데 사용됩니다. 이 예제에서는 this를 사용하여 현재 상태를 참조합니다.
- 위젯 생명주기
- initState(): 위젯이 처음 생성될 때 호출됩니다. 이곳에서 TabController를 초기화합니다.
- dispose(): 위젯이 더 이상 필요하지 않을 때 호출됩니다. 이곳에서 TabController를 정리하여 메모리 누수를 방지합니다.
import 'package:class_v03/main.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
// StatefulWidget 클래스 - 상태를 관리 표현
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
// 부모 클래스에 상태를 접근 수정 등 역할 수정.
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
// 멤버 변수 선언
late TabController _tabController;
@override
void initState() {
super.initState();
// 메모리에 올라 갈때 단 한번 호출 되는 메서드
_tabController = TabController(length: 3, vsync: this);
}
@override
void dispose() {
// 위젯 제거 될 때 호출 된다.
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('TabBar'),
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(text: 'HOME'),
Tab(text: 'Favorites', icon: Icon(Icons.star)),
Tab(text: 'Settings', icon: Icon(Icons.settings)),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
Center(child: Text('HOME')),
Center(child: Text('Favorites')),
Center(child: Text('Settings')),
],
),
),
);
}
}
'Flutter > Flutter UI 프레임워크' 카테고리의 다른 글
플러터 기본기 다지기 - 4 (0) | 2024.12.11 |
---|---|
# Dio 통신 연습 (0) | 2024.12.11 |
플러터 기본기 다지기 - 2 (2) | 2024.11.21 |
플러터 기본기 다지기 - 1 (1) | 2024.11.21 |
dart - 2 (0) | 2024.11.21 |