[Flutter] Http 통신 / Rest API 호출하기 1편 - Http

Tyger·2023년 2월 27일
2

Flutter

목록 보기
25/57

Http 통신 / Rest API 호출하기 1편 - Http

Http 통신 / Rest API 호출하기 2편 - Dio
Http 통신 / Rest API 호출하기 3편 - Get Connect

http | Dart Packages
url_launcher | Flutter Package

Lorem Picsum

이번 글에서는 Http 통신 및 Rest API 호출에 대해서 작성하려고 한다.

앱을 개발할 때 꼭 사용하는게 API 통신이다. Flutter에서 API 통신을 지원하는 라이브러리를 간단한 예제를 통해서 살펴보도록 하겠다.

무료 API를 사용하기 좋은 사이트가 있어서 위에 링크를 올려놨다. Lorem Picsum을 방문해 보면 다른 예제도 올라와 있으니 연습해보면 좋을 것 같다.

Flutter

Flutter에서 HTTP 통신 및 Rest API 호출을 사용하는 방법 중 가장 기본적인 라이브러리인 http 라이브러리를 사용하여 통신을 해보자.

dependencies

dependencies:
	http: ^0.13.5

Model

Rest API로 리턴 구조의 객체 모델이다. 이미지를 가져오는 API를 get 방식으로 호출할 것이고, 이미지 id, 작성자, 가로 세로 높이, 이미지 웹 url, 이미지를 다운받을 수 있는 downloadUrl 이렇게 되어 있다.

해당 API 데이터를 가져올 때 json 형태의 구조를 앱에서 사용할 객체로 변환하여야 한다.

class PiscumPhotoModel {
  final String id;
  final String author;
  final int width;
  final int height;
  final String url;
  final String downloadUrl;

  PiscumPhotoModel({
    required this.id,
    required this.author,
    required this.width,
    required this.height,
    required this.url,
    required this.downloadUrl,
  });
}

json을 변환할 때는 아래 두 가지 코드 모두 변환할 수 있는 코드이기에, 편한 방법을 사용하면 된다.

 PiscumPhotoModel.fromJson(Map<String, dynamic> json)
      : id = json["id"],
        author = json["author"],
        width = json["width"],
        height = json["height"],
        url = json["url"],
        downloadUrl = json["download_url"];
factory PiscumPhotoModel.fromJson(Map<String, dynamic> json) {
    return PiscumPhotoModel(
      id: json["id"],
      author: json["author"],
      width: json["width"],
      height: json["height"],
      url: json["url"],
      downloadUrl: json["download_url"],
    );
  }

UI

UI 구조를 간단하게 살펴보면 API에서 받아온 PiscumPhotoModel을 리스트 뷰를 통해서 보여주며, 각 아이템은 왼쪽에 이미지를 보여주고 중앙에는 id, 작성자, 가로 세로 높이를 보여줄 것이다. 해당 아이템을 클릭하면 웹으로 연결할 수 있도록 launcher를 통해 웹을 오픈하는 구조이다.

body: NotificationListener<ScrollUpdateNotification>(
              onNotification: (ScrollUpdateNotification notification) {
                state.scrollListerner(notification);
                return false;
              },
              child: ListView.builder(
                  itemCount: state.photos.length,
                  itemBuilder: ((context, index) {
                    return Padding(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 20, vertical: 8),
                      child: Column(
                        children: [
                          SizedBox(
                            child: Row(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                SizedBox(
                                  width:
                                      MediaQuery.of(context).size.width * 0.2,
                                  height:
                                      MediaQuery.of(context).size.width * 0.2,
                                  child: ClipRRect(
                                    borderRadius: BorderRadius.circular(12),
                                    child: Image.network(
                                      state.photos[index].downloadUrl,
                                      fit: BoxFit.cover,
                                      frameBuilder: (BuildContext context,
                                          Widget child,
                                          int? frame,
                                          bool wasSynchronouslyLoaded) {
                                        return Container(
                                          decoration: BoxDecoration(
                                            borderRadius:
                                                BorderRadius.circular(12),
                                            color: const Color.fromRGBO(
                                                91, 91, 91, 1),
                                          ),
                                          child: child,
                                        );
                                      },
                                      loadingBuilder: (BuildContext context,
                                          Widget child,
                                          ImageChunkEvent? loadingProgress) {
                                        if (loadingProgress == null) {
                                          return child;
                                        }
                                        return Container(
                                          decoration: BoxDecoration(
                                            borderRadius:
                                                BorderRadius.circular(12),
                                            color: const Color.fromRGBO(
                                                91, 91, 91, 1),
                                          ),
                                          child: const Center(
                                            child: CircularProgressIndicator(
                                              color: Colors.amber,
                                            ),
                                          ),
                                        );
                                      },
                                    ),
                                  ),
                                ),
                                const SizedBox(width: 12),
                                Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    _content(
                                        url: state.photos[index].url,
                                        title: "ID : ",
                                        content: state.photos[index].id),
                                    _content(
                                        url: state.photos[index].url,
                                        title: "Author : ",
                                        content: state.photos[index].author),
                                    _content(
                                        url: state.photos[index].url,
                                        title: "Width : ",
                                        content:
                                            "${state.photos[index].width}"),
                                    _content(
                                        url: state.photos[index].url,
                                        title: "Height : ",
                                        content:
                                            "${state.photos[index].height}"),
                                  ],
                                )
                              ],
                            ),
                          ),
                          if (state.photos.length - 1 == index &&
                              state.isAdd) ...[
                            const SizedBox(
                              height: 100,
                              child: Center(
                                  child: CircularProgressIndicator(
                                color: Colors.deepOrange,
                              )),
                            ),
                          ],
                        ],
                      ),
                    );
                  })),
            ),
GestureDetector _content({
    required String title,
    required String content,
    required String url,
  }) {
    return GestureDetector(
      onTap: () async {
        if (await canLaunchUrlString(url)) {
          await launchUrlString(url, mode: LaunchMode.externalApplication);
        }
      },
      child: Row(
        children: [
          Text(
            title,
            style: const TextStyle(
              fontSize: 14,
              fontWeight: FontWeight.bold,
            ),
          ),
          Text(
            content,
            style: const TextStyle(
                fontSize: 14, color: Color.fromRGBO(215, 215, 215, 1)),
          ),
        ],
      ),
    );
  }

Provider

상태관리로 Provider를 사용하였다. 필요 상태로는 API를 통해서 받아온 PiscumPhotoModel 객체를 담고있는 리스트인 photos 상태가 있고, 현재 어떤 페이지를 호출한지에 대한 상태인 currentPageNo가 있다.
UI에 로딩을 표시할 isAdd 불리언 상태가 있다.

class HttpProvider extends ChangeNotifier {
	List<PiscumPhotoModel> photos = [];
	int currentPageNo = 1;
	bool isAdd = false;
    ...
  }

페이지에 진입시 API 호출을 통해서 데이터를 세팅하기 위해 만든 함수이다.

  Future<void> started() async {
    await _getPhotos();
  }

fetchPost 함수를 통해서 API로 부터 데이터를 받아와 photos 변수에 데이터를 넣어주는 부분이다.

currentPageNo는 현재 페이지 넘버를 의미하며, 1번 페이지를 호출한 이 후에 2번을 호출하여야 하기에 currentPageNo 변수를 2로 변경해 주었다.

 Future<void> _getPhotos() async {
    List<PiscumPhotoModel>? _data = await _fetchPost(pageNo: currentPageNo);
    photos = _data;
    currentPageNo = 2;
    logger.e(currentPageNo);
    notifyListeners();
  }

API를 호출하는 부분의 코드이다. 우선 위에서 추가한 http 라이브러리를 사용하기 위해서는 아래와 같이 http를 임포트 해주어야 한다.

fetchPost 함수를 살펴보면 정수형 pageNo를 필수 값으로 받아와 API 호출 페이지 넘버로 사용하고 있다.

API 통신은 비동기 통신이기 때문에 async-await 키워드를 넣어 비동기 처리를 해줘야 한다. http.get 으로 GET 방식의 Rest API 호출을 할 수 있다.
Uri.parse로 API호출 주소를 URI 방식으로 변경해주면 되는데, 물음표(?) 어노테이션 앞의 부분이 호출 주소에 해당하고 page, limit은 옵션 파라미터이다.
해당 API의 page는 1부터 호출되고 limit은 10개로 지정해 줬다.

API을 호출하면 응답 코드를 받게 되는데, 200 코드는 API 호출이 정상적으로 성공했을 때 성공 코드이고, 이 외의 코드는 각 API 레퍼런스를 확인하여야 한다.

리턴 받은 body 구조는 json 형태로 받을 수 있기에 위에서 데이터 모델을 만들 때 추가했었던 fromJson을 통해서 객체로 변환해주면 된다.

  import 'package:http/http.dart' as http;

  Future<List<PiscumPhotoModel>> _fetchPost({
    required int pageNo,
  }) async {
    try {
      http.Response _response = await http.get(
          Uri.parse("https://picsum.photos/v2/list?page=$pageNo&limit=10"));
      if (_response.statusCode == 200) {
        List<dynamic> _data = json.decode(_response.body);
        List<PiscumPhotoModel> _result =
            _data.map((e) => PiscumPhotoModel.fromJson(e)).toList();
        return _result;
      } else {
        return [];
      }
    } catch (error) {
      logger.e(error);
      return [];
    }
  }

무한 스크롤로 API데이터를 계속 호출해주기 위해서 스크롤 포지션 값을 가져오는 부분의 코드이다.

 void scrollListerner(ScrollUpdateNotification notification) {
    if (notification.metrics.maxScrollExtent * 0.85 <
        notification.metrics.pixels) {
      _morePhotos();
    }
  }

morePhotos는 무한 스크롤에서 호출하는 코드로 isAdd의 값을 통해서 해당 함수가 종료되기 전까지 중복으로 호출되지 않도록 하기 위한 기능이다.

API를 호출하는 기능인 fetchPost를 다시 호출해주면 되는데, 여기서 중요한 부분은 currentPageNo가 호출된 이 후로 1 페이지씩 증가한 상태로 호출하여야 한다는 것이다.
그래야 다음 페이지를 불러올 수 있다.

 Future<void> _morePhotos() async {
    if (!isAdd) {
      isAdd = true;
      notifyListeners();
      List<PiscumPhotoModel>? _data = await _fetchPost(pageNo: currentPageNo);
      Future.delayed(const Duration(milliseconds: 1000), () {
        photos.addAll(_data);
        currentPageNo = currentPageNo + 1;
        isAdd = false;
        notifyListeners();
      });
    }
  }

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/http/http

마무리

간단하게 http 라이브러리로 API 통신을 하고 무한 스크롤을 통해서 API를 리턴 데이터가 없을 때까지 계속 호출할 수 있도록 만드는 방법에 대해서도 간단하게 확인해 봤다.

다음 시간에는 Dio 라이브러리를 통해서 API를 호출하는 방법에 대해서 글을 작성하도록 하겠다.

profile
Flutter Developer

0개의 댓글