1. size.dart
로 여백/사이즈 관리하기
왜? ListView 같은 스크롤 위젯은 세로 길이가 사실상 무한대라서 Expanded나 Spacer를 쓸 수 없다. 이런 경우엔 SizedBox 로 높이를 직접 지정해 주자.어떻게? 무작정 숫자를 박기보다size.dart
파일로 상수화하여 일관성 있게 관리!
예시:
// size.dart
const double small_gap = 8.0;
const double medium_gap = 16.0;
const double large_gap = 24.0;
스크린 곳곳에서:
SizedBox(height: small_gap), SizedBox(height: medium_gap), SizedBox(height: large_gap),
2. Navigator로 화면 이동 (push/pop) & Named Routes
- Navigator = 스택(Stack) 기반 화면 관리자
- push: 화면 위에 올리기
- pop: 현재 화면 걷어내기(이전 화면으로 돌아감)
// 예: 일반 push
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
);
// 예: pop (현재 화면 제거 → 이전 화면 노출)
Navigator.pop(context);
Named Routes(명명된 경로)
- 경로를 문자열로 관리(예:
"/login"
,"/home"
).
- 라우트 맵을 미리 등록해 두면 깔끔하게 네비게이션 가능.
MaterialApp(
initialRoute: "/login",
routes: {
"/login": (context) => const LoginPage(),
"/home": (context) => const HomePage(),
},
);
// 이동
Navigator.pushNamed(context, "/home");
// 혹은 현재 화면 닫고 이전으로
Navigator.pop(context);
3) Form 위젯으로 입력 묶고, 한 번에 유효성 검사
- Form = 입력 위젯(TextFormField 등)을 묶는 컨테이너
- 여러 필드를 한 번에 유효성 검사하거나 전송할 때 사용
GlobalKey<FormState>
로 상태를 잡고, 버튼에서validate()
호출
class CustomForm extends StatelessWidget {
CustomForm({super.key});
final _formKey = GlobalKey<FormState>(); // 1) 글로벌 키
@override
Widget build(BuildContext context) {
return Form(
key: _formKey, // 2) Form에 연결
child: Column(
children: [
CustomTextFormField("Email"),
const SizedBox(height: 16),
CustomTextFormField("Password"),
const SizedBox(height: 24),
TextButton(
onPressed: () {
// 3) 버튼에서 검증 실행 → 각 필드의 validator 동작
if (_formKey.currentState!.validate()) {
// true일 때 다음 화면 이동 or 로그인 요청
Navigator.pushNamed(context, "/home");
}
},
child: const Text("Login"),
),
],
),
);
}
}
4) TextFormField: 키보드 인셋·스크롤·유효성 검사
- 키보드가 올라오면(인셋 영역) 화면 그릴 수 없는 높이가 생겨 Overflow 위험
- 해결: 상위에 스크롤을 달아주자(예:
SingleChildScrollView
orListView
)
TextFormField
는validator
로 유효성 검사 가능
class CustomTextFormField extends StatelessWidget {
final String text;
const CustomTextFormField(this.text);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(text),
SizedBox(height: small_gap),
TextFormField(
// 1. 느낌표는 null이 절대 아니다 라고 컴파일러에게 알려주는 것
validator: (value) => value!.isEmpty
? "Please enter some text"
: null, // 값이 없으면 Please enter some text 경고 화면 표시
obscureText:
// 2. 해당 TextFormField가 비밀번호 입력 양삭이면 **** 처리 해주기
text == "Password" ? true : false,
decoration: InputDecoration(
hintText: "Enter $text",
enabledBorder: OutlineInputBorder(
// 3. 기본 TextFormField 디자인
borderRadius: BorderRadius.circular(20),
),
focusedBorder: OutlineInputBorder(
// 4. 손가락 터치시 TextFormField 디자인
borderRadius: BorderRadius.circular(20),
),
errorBorder: OutlineInputBorder(
// 5. 에러발생시 TextFormField 디자인
borderRadius: BorderRadius.circular(20),
),
focusedErrorBorder: OutlineInputBorder(
// 6. 에러가 발생 후 손가락을 터치했을 때 TextFormField 디자인
borderRadius: BorderRadius.circular(20),
),
),
),
],
);
}
}
Tip즉시 에러를 보고 싶다면autovalidateMode: AutovalidateMode.onUserInteraction
를 추가하면 입력/포커스 시 바로 validator가 동작한다.
예:
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) => (value == null || value.trim().isEmpty)
? "Please enter some text"
: null,
...
)
5) SVG 위젯 사용하기
SVG(Vector) = 확대/축소해도 깨지지 않는 벡터 이미지
- Flutter에선
flutter_svg
패키지의SvgPicture
위젯 사용
pubspec.yaml
에 의존성 추가 후, 에셋 경로 등록하여 호출
dependencies:
flutter_svg: ^2.0.0
import 'package:flutter_svg/flutter_svg.dart';
SvgPicture.asset(
'assets/images/logo.svg',
width: 120,
height: 120,
)
6) 앱 전체 디자인을 Theme로 통일하기 (TextButton 예시)
- 반복되는 버튼 디자인은 개별 위젯 대신 Theme에 정의 → 앱 전역 일관성 + 생산성 UP
- 최신 Flutter(Material 3)에서는
TextButton.styleFrom
의primary
가 제거되고,foregroundColor
를 사용
import 'package:flutter/material.dart';
import 'package:login_app/pages/home_page.dart';
import 'package:login_app/pages/login_page.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// 테마 설정
theme: ThemeData(
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white, // ← primary 대신 foregroundColor
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
minimumSize: const Size(400, 60),
),
),
),
debugShowCheckedModeBanner: false,
initialRoute: "/login",
routes: {
"/login": (context) => const LoginPage(),
"/home": (context) => const HomePage(),
},
);
}
}
Share article