카테고리 없음

4.14 TIL

개발일지27 2025. 4. 14. 21:02

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,
              ),
            );
          },
        ),
      ),
    );
  }
}