1. Flutter 숙련_ 책 검색 앱 만들기
주어진 Ui를 바탕으로 기초적인 레이아웃을 나누고 뼈대를 만드는 작업을 하였습니다.
<MVVM 구조 폴더 나누기 및 파일생성>
- lib/
- data/
- model/ => 데이터 받아와서 담을 Model 클래스들
- repository/ => 데이터 받아와서 Model 클래스로 변환할 Repository 클래스들
- ui/
- pages/ => 앱 내 페이지들 각 폴더별로 위치
- home/
- widgets/ => HomePage 내에서만 사용할 위젯
- home_page.dart
- home_view_model.dart
- detail
- widgets/ => DetailPage 내에서만 사용할 위젯
- detail_page.dart
- detail_view_model.dart
- widgets/ => 앱 전체적으로 사용할 위젯
- main.dart
<main.dart>
import 'package:flutter/material.dart';
import 'package:flutter_book_search_page/ui/pages/home/home_page.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp())); // ProviderScope 로 앱을 감싸 RiverPod을 ViewModel 관리할 수 있게 선언
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
<home_page.dart>
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Text('HomePage'),
);
}
}
<detail_page.dart>
import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Text('DetailPage'),
);
}
}
<TextField구현>
TextField(
// TextField의 값을 변경하거나, 값을 다른 위젯에서 사용하고 싶을 때!
controller: TextEditingController(),
// TextField의 스타일 설정
decoration: InputDecoration(
hintText: "검색어를 입력해 주세요",
// MaterialStateOutlineInputBorder.resolveWith :
// TextField 상태에 따라 다른 테두리 적용하고 싶을 때!
border: MaterialStateOutlineInputBorder.resolveWith(
(states) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey),
);
},
),
),
// 모바일에서 키보드 유형 설정
keyboardType: TextInputType.text,
// 키보드의 작업 버튼을 '완료'로 설정
textInputAction: TextInputAction.done,
// 텍스트 변경 시 호출되는 함수
onChanged: (text) {
print('Text changed: $text');
},
// 키보드에서 완료(엔터) 눌렀을 때 호출되는 함수
onSubmitted: (text) {
print('Submitted text: $text');
},
// true로 설정하면 입력한 텍스트가 가려짐 (비밀번호 입력 시 사용)
obscureText: false,
// 최대 글자 수 제한
maxLength: 50,
// 최대 입력 줄 수 제한
maxLines: 1,
// TextField 글자 스타일
style: TextStyle(fontSize: 16, color: Colors.black), // 입력 텍스트 스타일
)
<GoldView 구현>
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController textEditingController = TextEditingController();
void search(String text) {
print("search");
}
@override
void dispose() {
// 4. TextEditingController 는 반드시 사용 다하면 dispose를 호출해줘야 메모리에서 제거됨!
// 소거해주려면 StatefulWidget 사용!
textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// 3. UX 고려하기
// 현재 위젯(HomePage)에서 포커스를 가지고 있는 위젯이 있으면 포커스 해제해줌
// TextField
FocusScope.of(context).unfocus();
},
child: Scaffold(
appBar: AppBar(
// 1. TextField 구현
title: TextField(
maxLines: 1,
// TextField의 값을 검색 아이콘 터치했을때에도 사용할거라 사용!
controller: textEditingController,
onSubmitted: search,
// 2. TextStyle 꾸미기
decoration: InputDecoration(
hintText: '검색어를 입력해 주세요',
border: MaterialStateOutlineInputBorder.resolveWith(
(states) {
// MaterialStateOutlineInputBorder.resolveWith를 사용하면
// TextField의 값이 바뀔때마다 WidgetState enum 값을 전달해서 이 함수 실행!
// print(states);
if (states.contains(WidgetState.focused)) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.black),
);
}
return OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey),
);
},
),
),
),
actions: [
GestureDetector(
onTap: () {
// textEditingController의 text 속성으로 TextField의 값을 가져옴
search(textEditingController.text);
},
child: Container(
// 중요!!! UX를 위한 터치 반경
// 최소 44 => MS에서 실험했을 때 7mm, 기기픽셀로 44 픽셀이
// 100번 터치했을 때 1 번 꼴로 실수 나왔음
width: 50,
height: 50,
// Container에 배경색 지정하지 않으면 child, 여기서 Icon에만 터치 이벤트 적용
color: Colors.transparent,
child: Icon(Icons.search),
),
),
],
),
body: GridView.builder(
padding: EdgeInsets.all(20),
itemCount: 10,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 3 / 4,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
print('item tap');
},
child: Image.network(
'https://picsum.photos/300/400',
fit: BoxFit.cover,
),
);
},
),
),
);
}
}