본문 바로가기
스파르타/Flutter

[Flutter] 왓챠피디아 - API 연동 - ( week - 4 )

by bakcoding_sparta 2023. 4. 27.

구현 목표

Google Book API를 활용해서 왓챠피디아 앱에 책 검색 기능을 구현한다.

 

 

네트워크 통신을 하기 위해서 Dio 패키지를 사용한다.

Dio

dio 공식 문서에 나와있는 설치 명령어를 터미널에 입력한다.

 $ flutter pub add dio

 

Dio 사용법

사용할 기능은 GET 메서드를 사용해서 URL로 요청을 보내고 비동기를 동기화시켜서 데이터를 받아서 사용한다.

 

// GET 메서드를 사용해서 URL로 요청하기
main() {
	Dio().get("URL");
}
// 비동기 동작을 동기로 구현
main() async {
	Response result = await Dio().get("URL");	// get 함수는 Future를 반환한다.
	print(result.data); // data 안에 응답 내용이 들어 있습니다.
}

 

Google Book APIs 살펴보기

Sample API

 

예시 API를 호출해서 받을 수 있는 데이터의 구성을 살펴본다.

json 형식으로 각 데이터들은 Key, Value로 되어있다.

 

주소창을 보면 ? 뒤에 보내지는 파라미터에 q=고양이라는 값이 보내지고 있다.

q는 키값이고 검색하려는 내용으로 보인다.

 

 

주소창에서 해당 값만 수정해주어도 그 값에 대한 책 정보를 제공한다.

 

데이터 중에서 사용할 정보는 다음과 같다.

 

id, title, subtitle, thumbnail, proviewLink 이 정보들을 사용해서 앱에 책 리스트를 만들어내도록 한다.

 

id 값의 경우 보이는 부분에서는 사용되지 않고 책들을 구분하는 값으로 사용한다.

 

Book 클래스

책의 정보를 묶어서 담기 위한 클래스를 생성한다.

class Book {
  String id;
  String title;
  String subtitle;
  String thumbnail; // 썸네일 이미지 링크
  String previewLink; // ListTile 을 눌렀을 때 이동하는 링크

  Book({
    required this.id,
    required this.title,
    required this.subtitle,
    required this.thumbnail,
    required this.previewLink,
  });
}

Book 클래스에는 사용할 데이터를 저장할 변수를 선언하고 생성자에서 값들이 초기화되도록 한다.

 

 

검색 기능

검색 기능은 main.dart의 SearchPage 클래스 내부에서 실행된다.

class SearchPage extends StatelessWidget {
  SearchPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
~
        title: TextField(
          onSubmitted: (value) {},
~
~
}

TextField의 onSubmitted가 실행될 때 텍스트 필드에 입력한 값이 value에 들어오는데 이 값을 API로 전달해서 검색이 되도록 한다. provider를 사용할 것이기 때문에 book을 CRUD에 관한 내용들은 모두 book_service에 코드를 작성하고 관리한다.

 

 

book_service

provider의 service를 사용하기 위해서 book의 CRUD에 관련된 기능을 모두 여기서 처리하도록 한다.

 

검색하는 함수를 추가한다.

  void search(String q) async {
    if (q.isNotEmpty) {
      Response res = await Dio().get(
        "https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
      );
      List items = res.data["items"];
      print(items);
    }
  }

 

함수는 q 문자열을 받아서 get 메서드로 URL을 보내는데 이때 받아온 q를 매개변수에 추가해서 서버의 API를 호출한다.

이때 get은 await을 걸어서 값이 들어올 때까지 대기시키고 서버로부터 결과가 들어오면 해당 정보를 가지고 아이템을 생성한다.

 

해당 기능을 main.dart에서 사용하기 위해서는 우선적으로 최상위 위젯에서 provider가 선언되어야 어디서든 book 데이터에 접근을 할 수 있다.

 

MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => BookService()),
      ],
      child: const MyApp(),
    ),

 

이 검색기능을 SearchPage 클래스 내부에서 가져다 사용하면 되는데 이때 검색 될 때마다 화면을 갱신시켜야 하므로 Consumer 위젯이 필요하다.

 

main.dart

SearchPage의 Scaffold를 Consumer 위젯으로 감싸주고 인자를 추가한다.

이때 구조가 비슷한 Builder로 한번 감싼 다음 바꾸어 주는 게 편하다.

  Widget build(BuildContext context) {
    return Consumer<BookService>(
      builder: (context, bookService, child) {
        return Scaffold(

 

이제 BookService 내부의 함수를 사용할 때 해당 함수에 notifylisteners()를 호출하고 있다면 Consumer로 감싸진 위젯 전체가 화면에 다시 그려지게 된다.

 

이제 TextField 위젯의 onSubmitted 속성에 호출할 함수를 넣어준다. 이때 search 함수는 해당 API를 호출한 결과를 전부 print 중이기 때문에 콘솔에서 결과를 확인할 수 있다.

              onSubmitted: (value) {
                bookService.search(value);
              },

 

이제 가져온 데이터를 파싱하고 순회하면서 Book 클래스를 생성하고 변수를 할당해서 하나의 Book이 만들어질 때마다 Book List에 담고 책 목록을 보여줄 때는 Book List를 순회하면서 아이템을 생성하면 된다.

 

book_service

  void search(String q) async {
    bookList.clear(); // 검색 버튼 누를때 이전 데이터들을 지워주기

    if (q.isNotEmpty) {
      Response res = await Dio().get(
        "https://www.googleapis.com/books/v1/volumes?q=$q&startIndex=0&maxResults=40",
      );
      List items = res.data["items"];

      for (Map<String, dynamic> item in items) {
        Book book = Book(
          id: item['id'],
          title: item['volumeInfo']['title'] ?? "",
          subtitle: item['volumeInfo']['subtitle'] ?? "",
          thumbnail: item['volumeInfo']['imageLinks']?['thumbnail'] ??
          previewLink: item['volumeInfo']['previewLink'] ?? "",
        );
        bookList.add(book);
      }
    }
    print(bookList);
  }
 

서버로부터 받은 데이터를 순회하면서 각각의 필요한 정보를 담아서 하나의 책을 완성하고 리스트에 담는다 이 함수는 검색할 때마다 호출되기 때문에 검색 시 리스트를 한번 비워주어야 이전에 검색한 책들은 지울 수 있다.

 

이때 값이 비어있는 경우를 대비해서??로 예외처리를 해준다.

 

bookList를 출력해 보면 book 인스턴스들이 담겨있는 걸 확인할 수 있다.

 

 

아이템 그리기

이제 SearchPage에서 리스트 형식으로 책들을 보여준다.

          body: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12),
            child: ListView.separated(
              itemCount: bookService.bookList.length,
              separatorBuilder: (context, index) {
                return Divider();
              },
              itemBuilder: (context, index) {
                Book book = bookService.bookList.elementAt(index);
                return ListTile(
                  title: Text(book.title),
                );
              },
            ),
          ),

우선 책의 타이틀 정보만 가져와서 데이터들이 잘 들어있는지만 확인해 본다.

타이틀들이 잘 출력되는 것으로 book들이 잘 들어가 있는 걸 확인할 수 있다.

이제 book에 담겨있는 정보들을 UI에 보이도록 위젯들을 추가해 준다.

 

                return ListTile(
                  onTap: () {},
                  leading: Image.network(
                    book.thumbnail,
                    fit: BoxFit.fitHeight,
                  ),
                  title: Text(
                    book.title,
                    style: TextStyle(fontSize: 16),
                  ),
                  subtitle: Text(
                    book.subtitle,
                    style: TextStyle(color: Colors.grey),
                  ),
                  trailing: IconButton(
                    onPressed: () {},
                    icon: Icon(Icons.star_border),
                  ),
                );

ListTile을 사용해서 책의 정보들을 표시한다.

 

 

여기서 검색한 내용의 결과가 없는 경우 에러가 발생한다. 그 이유는 화면을 새로 그려야 할 때 리스트가 비어있는 예외처리를 안 했기 때문에 비어있는 리스트의 인덱스에 접근하면서 발생하는 문제로 해당 부분을 예외처리해 준다.

              itemBuilder: (context, index) {
                if (bookService.bookList.isEmpty) return SizedBox();
                Book book = bookService.bookList.elementAt(index);
                return ListTile(

 

여기까지 검색 기능으로 서버에서 API를 통해서 해당 책들을 검색해서 나온 결과를 데이터로 받아와서 눈에 보이는 위젯으로 만드는 것까지 만들어졌다.