- [Flutter] "Do it! 플러터 앱 프로그래밍" - 네트워크를 이용해 통신하기 | 카카오 API 이용한 책 정보 받아오기, 이미지 파일 내려받기2022년 02월 14일 12시 28분 22초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
"조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 7장을 실습하였다. 네트워크를 연결하여 웹에서 정보를 받아와 화면에 표시하는 기능과 파일을 다운로드하는 기능을 실습하였다. 특별히 "스크롤로 책 정보 가져오기"에서 깨알 같은 오타로 인해 발생한 에러로 크게 애를 먹었다. 콘솔창의 메시지만 진지하게 읽었더라도 덜 고생하고 더 빨리 해결할 수 있었던 에러다!
HTTP 통신 실습
http 패키지 설치
pub.dev에서 http 외부 패키지를 찾는다(https://pub.dev/packages/http/install). http 패키지에 대한 설치 방법 및 관련 정보를 확인할 수 있다. cmd 창을 열고 http 패키지를 설치한다.
flutter pub add http
GET 방식 접속
import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; // http 패키지 불러오기 void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: HttpApp(), ); } } class HttpApp extends StatefulWidget { @override State<StatefulWidget> createState() => _HttpApp(); } class _HttpApp extends State<HttpApp> { String result = ''; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Http Example"), ), body: Container( child: Center( child: Text('$result'), ), ), floatingActionButton: FloatingActionButton( onPressed: () async { var url = 'http://www.google.com'; var response = await http.get(Uri.parse(url)); // GET 방식으로 URL에 접속하는 코드 setState(() { result = response.body; }); }, child: Icon(Icons.file_download), ), ); } }
카카오 API를 이용해 책 정보 받아오기
책 정보 가져오기 / 카드 위젯 꾸미기
카카오 API 키를 이용한다(https://developers.kakao.com/). 앱 키로 REST API 키를 사용한다.
// main.dart import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; // http 패키지 불러오기 import 'dart:convert'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: HttpApp(), ); } } class HttpApp extends StatefulWidget { @override State<StatefulWidget> createState() => _HttpApp(); } class _HttpApp extends State<HttpApp> { String result = ''; List? data; // 사이트로 부터 받아온 데이터를 담을 data 리스트 @override void initState() { super.initState(); // initState에서 data 변수를 초기화해야 한다. data = new List.empty(growable: true); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Http Example"), ), body: Container( child: Center( // 데이터가 0이면 '데이터가 없습니다.', 서버로 부터 데이터를 받으면 ListView.builder로 표시하는 삼항 연산자 child: data!.length == 0 ? Text( '데이터가 없습니다.', style: TextStyle(fontSize: 20), textAlign: TextAlign.center, ) : ListView.builder( itemBuilder: (context, index) { return Card( child: Container( child: Column( children: <Widget>[ // 카드 위젯 꾸지미기 전 // Text(data![index]['title'].toString()), // Text(data![index]['author'].toString()), // Text(data![index]['sale_price'].toString()), // Text(data![index]['status'].toString()), Image.network( data![index]['thumbnail'], height: 100, width: 100, fit: BoxFit.contain, ), // 카드 위젯 꾸미기 Column( children: <Widget>[ Container( width: // MediaQuery.of(context).size는 현 스마트폰의 화면 크기를 의미한다. MediaQuery.of(context).size.width - 150, // overflow 방지 child: Text( data![index]['title'].toString(), textAlign: TextAlign.center, ), ), Text( '저자 : ${data![index]['authors'].toString()}'), Text( '가격 : ${data![index]['sale_price'].toString()}'), Text( '판매중 : ${data![index]['status'].toString()}'), ], ) ], mainAxisAlignment: MainAxisAlignment.start, ), ), ); }, itemCount: data!.length), ), ), floatingActionButton: FloatingActionButton( onPressed: () async { getJSONData(); }, child: Icon(Icons.file_download), ), ); } Future<String> getJSONData() async { // https://dapi.kakao.com/v3/search/book? // 요청할 도메인 // target=title& // 도메인에 요청할 파라미터 // query=doit // query 파라미터에 검색어 doit 전달 var url = 'https://dapi.kakao.com/v3/search/book?target=title&query=doit'; var response = await http.get(Uri.parse(url), headers: { "Authorization": "KakaoAK ##REST API 키 넣기##" }); // KakaoAK 대소문자 구분 잘해야 한다. // print(response.body); // 데이터를 List 형태의 data 변수에 넣는다. setState(() { var dataConvertedToJSON = json.decode(response.body); List result = dataConvertedToJSON['documents']; data!.addAll(result); }); return "Successful"; } }
검색 기능 추가하기 / 스크롤로 책 정보 가져오기
검색 기능의 추가로 플로팅 버튼을 누를 때마다 검색어가 포함된 총 10권의 책을 받아왔다. 이를 개선해 플로팅 버튼을 한 번만 누른 후 스크롤을 내릴 때마다 이어서 새로운 책을 받아서 표시하도록 했다.
// main.dart import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; // http 패키지 불러오기 void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: HttpApp(), ); } } class HttpApp extends StatefulWidget { @override State<StatefulWidget> createState() => _HttpApp(); } class _HttpApp extends State<HttpApp> { String result = ''; TextEditingController? _editingController; // 검색 기능 추가용 ScrollController? _scrollController; // 스크롤 내릴 때 마다 새로운 책을 받아서 표시하는 기능 추가용 List? data; // 사이트로 부터 받아온 데이터를 담을 data 리스트 int page = 1; // 스크롤 내릴 때 마다 새로운 책을 받아서 표시하는 기능 추가용 @override void initState() { super.initState(); // initState에서 data 변수를 초기화해야 한다. data = new List.empty(growable: true); _editingController = new TextEditingController(); // 검색 기능 추가용 // 스크롤 내릴 때 마다 새로운 책을 받아서 표시하는 기능 추가용 _scrollController = new ScrollController(); // addListener 함수를 이용해 스크롤할 때 이벤트를 받도록 처리한다. // offset은 목록의 현재 위치를 double형 변수로 나타낸다. // 스크롤할 때마다 offset을 검사해 maxScrollExtent보다 크거나 같고 스크롤 컨트롤러의 position에 정의된 범위를 넘어가지 않으면 목록의 마지막이라고 인식한다. // 그러면 page를 1만큼 증가한 후 getJSONData 함수를 호출한다. _scrollController!.addListener(() { if (_scrollController!.offset >= _scrollController!.position.maxScrollExtent && !_scrollController!.position.outOfRange) { print('bottom'); // 리스트의 마지막일 때 실행 page++; getJSONData(); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( // 앱바에 검색 기능 추가 title: TextField( controller: _editingController, style: TextStyle(color: Colors.white), keyboardType: TextInputType.text, decoration: InputDecoration(hintText: '검색어를 입력하세요'), ), ), body: Container( child: Center( // 데이터가 0이면 '데이터가 없습니다.', 서버로 부터 데이터를 받으면 ListView.builder로 표시하는 삼항 연산자 child: data!.length == 0 ? Text( '데이터가 없습니다.', style: TextStyle(fontSize: 20), textAlign: TextAlign.center, ) : ListView.builder( itemBuilder: (context, index) { print(data![index]['thumbnail']); return Card( child: Container( child: Row( children: <Widget>[ // 카드 위젯 꾸지미기 전 // Text(data![index]['title'].toString()), // Text(data![index]['author'].toString()), // Text(data![index]['sale_price'].toString()), // Text(data![index]['status'].toString()), if (data?[index]['thumbnail'] != '') Image.network( data![index]['thumbnail'], height: 100, width: 100, fit: BoxFit.contain, ) else Container( height: 100, width: 100, ), // 카드 위젯 꾸미기 Column( children: <Widget>[ Container( width: // MediaQuery.of(context).size는 현 스마트폰의 화면 크기를 의미한다. MediaQuery.of(context).size.width - 150, // overflow 방지 child: Text( data![index]['title'].toString(), textAlign: TextAlign.center, ), ), Text( '저자 : ${data![index]['authors'].toString()}'), Text( '가격 : ${data![index]['sale_price'].toString()}'), Text( '판매중 : ${data![index]['status'].toString()}'), ], ) ], mainAxisAlignment: MainAxisAlignment.start, ), ), ); }, itemCount: data!.length, // 스크롤 내릴 때 마다 새로운 책을 받아서 표시하는 기능 추가 controller: _scrollController, ), ), ), floatingActionButton: FloatingActionButton( // 버튼을 누를 때마다 기존 내용을 지우고 페이지를 1로 초기화 onPressed: () { page = 1; data!.clear(); getJSONData(); }, child: Icon(Icons.search), ), ); } Future<String> getJSONData() async { // https://dapi.kakao.com/v3/search/book? // 요청할 도메인 // target=title& // 도메인에 요청할 파라미터 // query=doit // query 파라미터에 검색어 doit 전달 // var url = 'https://dapi.kakao.com/v3/search/book?target=title&query=doit'; // 검색 기능 추가 // 검색어가 포함된 총 10권의 책을 서버로 부터 받아서 표시한다. // var url = 'https://dapi.kakao.com/v3/search/book?' // 'target=title&query=${_editingController!.value.text}'; // 스크롤을 내릴 때 마다 이어서 새로운 책을 받아서 표시하기 위해 page파라미터 추가 var url = 'https://dapi.kakao.com/v3/search/book?target=title&page=$page&query=${_editingController!.value.text}'; // KakaoAK 대소문자 구분 잘해야 한다. var response = await http.get(Uri.parse(url), headers: {"Authorization": "KakaoAK ##REST API 키 넣기##"}); print(response.body); // 검색 결과 로그창으로 확인 // 데이터를 List 형태의 data 변수에 넣는다. setState(() { var dataConvertedToJSON = json.decode(response.body); List result = dataConvertedToJSON['documents']; data!.addAll(result); }); // return "Successful"; return response.body; } }
실습 중 만난 에러
[그림 4]는 실습 중 만난 에러이다. 구글링도 해보고, 저자의 깃허브 코드와 비교도 해봤지만 좀처럼 잡히지 않았다.
콘솔창의 메시지에서 힌트를 얻어 query parameter를 다시 찬찬히 확인했더니 query를 qurey라고 쳐 놓았다. 깨알 같은 오타다!
I/flutter (18383): {"errorType":"MissingParameter","message":"query parameter required"}
이미지 파일 내려받기
dio, path_provider 패키지 설치
pub.dev에서 dio, path_provider 외부 패키지를 찾아 설치한다. (https://pub.dev/packages/http/install). dio는 파일을 내려받는 데 도움을 주는 패키지이고, path_provider는 내부 저장소를 이용하는 패키지이다.
flutter pub add dio
flutter pub add path_provider
내려받기 진행 상황 표시하기
용량이 큰 파일을 내려받아야 할 때 화면에 아무런 정보가 없으면 사용자는 앱이 멈춘 것으로 착각한다(3초 이상이라면). 화면에 진행 상황을 표시하여 UX를 향상한다.
// main.dart import 'package:flutter/material.dart'; import 'largeFileMain.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: LargeFileMain(), ); } }
// largeFileMain.dart import 'package:file/memory.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; // 파일 입출력을 돕는 io 패키지 class LargeFileMain extends StatefulWidget { @override State<StatefulWidget> createState() => _LargeFileMain(); } class _LargeFileMain extends State<LargeFileMain> { // 내려받을 이미지 주소 final imgUrl = 'https://images.pexels.com/photos/5019409/pexels-photo-5019409.jpeg' '?auto=compress'; bool downloading = false; // 지금 내려받는 중인지 확인하는 변수 var progressString = ""; // 현재 얼마나 내려받았는지 표시하는 변수 String file = ""; // 내려받은 파일 @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Large File Example'), ), body: Center( child: downloading ? Container( height: 120.0, width: 200.0, child: Card( color: Colors.black, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ CircularProgressIndicator(), SizedBox( height: 20.0, ), Text( 'Downloading File: $progressString', style: TextStyle( color: Colors.white, ), ), ], ), ), ) : FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState .none: // FutureBuilder.future가 null일 때 print('none'); return Text('데이터 없음'); case ConnectionState .waiting: // 연결되기 전(FutureBuilder.future에서 데이터를 반환받지 않았을 때) print('waiting'); return CircularProgressIndicator(); case ConnectionState.active: // 하나 이상의 데이터를 반환받을 때 print('active'); return CircularProgressIndicator(); case ConnectionState.done: // 모든 데이터를 받아서 연결이 끝날 때 print('done'); if (snapshot.hasData) { return snapshot.data as Widget; } } print('end process'); return Text('데이터 없음'); }, future: downloadWidget(file), )), floatingActionButton: FloatingActionButton( onPressed: () { downloadFile(); }, child: Icon(Icons.file_download), ), ); } // downloadWidget은 이미지 파일이 있는지 확인해서 있으면 이미지를 화면에 보여주는 위젯을 반환하고, 없으면 'No Data'라는 텍스트를 출력한다. Future<Widget> downloadWidget(String filePath) async { File file = File(filePath); bool exist = await file.exists(); new FileImage(file).evict(); // 캐시 초기화하기, evict()로 캐시를 비우면 같은 이름이라도 갱신한다. if (exist) { return Center( child: Column( children: <Widget>[Image.file(File(filePath))], ), ); } else { return Text('No Data'); } } Future<void> downloadFile() async { // dio를 선언한 후 내부 디렉터리를 가져온다. Dio dio = Dio(); try { // getApplicationDocumentsDirectory는 path_provider 패키지가 제공하며 플러터 앱의 내부 디렉터리를 가져오는 역할을 한다. var dir = await getApplicationDocumentsDirectory(); // dio.download를 이용해 url에 담긴 주소의 파일 내려 받는다. await dio.download(imgUrl, '${dir.path}/myimage.jpg', onReceiveProgress: (rec, total) { // onReceiveProgress함수를 실행해 진행 상황을 표시한다. print('Rec: $rec, Total:$total'); file = '${dir.path}/myimage.jpg'; setState(() { downloading = true; // 내려받기 시작 progressString = ((rec / total) * 100).toStringAsFixed(0) + '%'; // 얼마나 내려 받았는지 계산 }); }); } catch (e) { print(e); } setState(() { downloading = false; // 내려받기 끝 progressString = 'Completed'; }); print('Download completed'); } }
URL을 직접 입력해 내려받기
[그림6]에서 URL 입력할 때 텍스트필드를 더블 클릭하면 복사/붙여넣기가 가능하다.
// largeFileMain.dart import 'package:file/memory.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; // 내부 저장소를 이용하는 패키지 import 'dart:io'; // 파일 입출력을 돕는 io 패키지 class LargeFileMain extends StatefulWidget { @override State<StatefulWidget> createState() => _LargeFileMain(); } class _LargeFileMain extends State<LargeFileMain> { // 내려받을 이미지 주소 // final imgUrl = // 'https://images.pexels.com/photos/5019409/pexels-photo-5019409.jpeg' // '?auto=compress'; bool downloading = false; // 지금 내려받는 중인지 확인하는 변수 var progressString = ""; // 현재 얼마나 내려받았는지 표시하는 변수 String file = ""; // 내려받은 파일] TextEditingController? _editingController; // URL 직접 입력해 내려받기 // URL 직접 입력해 내려받기 @override void initState() { super.initState(); _editingController = new TextEditingController( text: 'https://images.pexels.com/photos/5019409/pexels-photo-5019409.jpeg' '?auto=compress'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( // title: Text('Large File Example'), // URL 직접 입력해 내려받기 TextField title: TextField( controller: _editingController, style: TextStyle(color: Colors.white), keyboardType: TextInputType.text, decoration: InputDecoration(hintText: 'url 입력하세요'), ), ), body: Center( child: downloading ? Container( height: 120.0, width: 200.0, child: Card( color: Colors.black, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ CircularProgressIndicator(), SizedBox( height: 20.0, ), Text( 'Downloading File: $progressString', style: TextStyle( color: Colors.white, ), ), ], ), ), ) : FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState .none: // FutureBuilder.future가 null일 때 print('none'); return Text('데이터 없음'); case ConnectionState .waiting: // 연결되기 전(FutureBuilder.future에서 데이터를 반환받지 않았을 때) print('waiting'); return CircularProgressIndicator(); case ConnectionState.active: // 하나 이상의 데이터를 반환받을 때 print('active'); return CircularProgressIndicator(); case ConnectionState.done: // 모든 데이터를 받아서 연결이 끝날 때 print('done'); if (snapshot.hasData) { return snapshot.data as Widget; } } print('end process'); return Text('데이터 없음'); }, future: downloadWidget(file), )), floatingActionButton: FloatingActionButton( onPressed: () { downloadFile(); }, child: Icon(Icons.file_download), ), ); } // downloadWidget은 이미지 파일이 있는지 확인해서 있으면 이미지를 화면에 보여주는 위젯을 반환하고, 없으면 'No Data'라는 텍스트를 출력한다. Future<Widget> downloadWidget(String filePath) async { File file = File(filePath); bool exist = await file.exists(); new FileImage(file).evict(); // 캐시 초기화하기, evict()로 캐시를 비우면 같은 이름이라도 갱신한다. if (exist) { return Center( child: Column( children: <Widget>[Image.file(File(filePath))], ), ); } else { return Text('No Data'); } } Future<void> downloadFile() async { // dio를 선언한 후 내부 디렉터리를 가져온다. Dio dio = Dio(); try { // getApplicationDocumentsDirectory는 path_provider 패키지가 제공하며 플러터 앱의 내부 디렉터리를 가져오는 역할을 한다. var dir = await getApplicationDocumentsDirectory(); // dio.download를 이용해 url에 담긴 주소의 파일 내려 받는다. // dio.download() 함수의 첫 번째 인자로 사용자가 입력한 주소(_editingController)를 전달 await dio .download(_editingController!.value.text, '${dir.path}/myimage.jpg', onReceiveProgress: (rec, total) { // onReceiveProgress함수를 실행해 진행 상황을 표시한다. print('Rec: $rec, Total:$total'); file = '${dir.path}/myimage.jpg'; setState(() { downloading = true; // 내려받기 시작 progressString = ((rec / total) * 100).toStringAsFixed(0) + '%'; // 얼마나 내려 받았는지 계산 }); }); } catch (e) { print(e); } setState(() { downloading = false; // 내려받기 끝 progressString = 'Completed'; }); print('Download completed'); } }
728x90반응형'언어·프레임워크 > Flutter' 카테고리의 다른 글
다음글이 없습니다.이전글이 없습니다.댓글