main.dart
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'book.dart'; import 'book_service.dart'; void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => BookService()), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } class HomePage extends StatelessWidget { HomePage({Key? key}) : super(key: key); /// 검색어를 가져올 수 있도록 TextField와 연결해 줍니다. final TextEditingController searchController = TextEditingController(); /// 검색 함수 /// 엔터를 누르거나 돋보기 아이콘을 누를 때 호출 void search(BookService bookService) { String keyword = searchController.text; if (keyword.isNotEmpty) { bookService.getBookList(keyword); } } Widget build(BuildContext context) { return Consumer<BookService>( builder: (context, bookService, child) { return Scaffold( appBar: AppBar( backgroundColor: Colors.white, // 배경 색상 iconTheme: IconThemeData(color: Colors.black), // app bar icon color title: Text( 'Book Store', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black, ), ), actions: [ Container( alignment: Alignment.bottomCenter, padding: const EdgeInsets.only(right: 12), child: Text( "total ${bookService.bookList.length}", style: TextStyle( color: Colors.black, fontSize: 16, ), ), ), ], bottom: PreferredSize( preferredSize: Size.fromHeight(70.0), child: Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: searchController, decoration: InputDecoration( labelText: '원하시는 책을 검색해주세요', enabledBorder: OutlineInputBorder( borderSide: const BorderSide(width: 0.5), borderRadius: BorderRadius.circular(1), ), // 돋보기 아이콘 suffixIcon: IconButton( icon: Icon(Icons.search), onPressed: () { search(bookService); }, ), ), onSubmitted: (v) { // 엔터를 누르는 경우 search(bookService); }, ), ), ), ), body: Column( children: [ Expanded( child: ListView.builder( itemCount: bookService.bookList.length, itemBuilder: (context, index) { Book book = bookService.bookList[index]; return ListTile( leading: Image.network( book.thumbnail, width: 80, height: 80, fit: BoxFit.cover, ), title: Text(book.title), subtitle: Text(book.subtitle), onTap: () { // 클릭시 previewLink 띄우기 launch(book.previewLink); }, ); }, ), ), ], ), ); }, ); } }
book_service.dart
import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'book.dart'; class BookService extends ChangeNotifier { // 책 목록 List<Book> bookList = []; /// 검색어로 책 정보 불러오기 void getBookList(String q) async { bookList.clear(); // 기존에 들어있는 데이터 초기화 // API 호출 Response res = await Dio().get( "https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40", ); List items = res.data["items"]; // items 접근 for (Map<String, dynamic> item in items) { Map<String, dynamic> volumeInfo = item["volumeInfo"]; // volumeInfo 접근 Book book = Book.fromJson(volumeInfo); // Map -> Book bookList.add(book); // Book 추가 } // 화면 갱신 notifyListeners(); } }
book.dart
class Book { String title; String subtitle; String thumbnail; String previewLink; Book({ required this.title, required this.subtitle, required this.thumbnail, required this.previewLink, }); // Map<String, dynamic>을 전달받아 Book 클래스 인스턴스를 반환하는 함수 // factory 키워드를 붙여서 생성자로 사용 factory Book.fromJson(Map<String, dynamic> volumeInfo) { return Book( // title이 없는 경우 빈 문자열 할당 title: volumeInfo["title"] ?? "", // subtitle이 없는 경우 빈 문자열 할당 subtitle: volumeInfo["subtitle"] ?? "", // imageLisks 또는 thumbnail이 없을 때 빈 이미지 추가 thumbnail: volumeInfo["imageLinks"]?["thumbnail"] ?? "https://i.ibb.co/2ypYwdr/no-photo.png", // previewLink가 없는 경우 빈 문자열 할당 previewLink: volumeInfo["previewLink"] ?? "", ); } }