Flutter에서 Dio를 사용하여 HTTP 통신하기
              
          2024. 4. 3. 22:08ㆍ카테고리 없음
Flutter에서 Dio를 사용하여 HTTP 통신하기 : 시작하기 전에
Flutter 애플리케이션에서 HTTP 통신을 효율적으로 처리하기 위해 Dio 라이브러리를 사용하는 방법을 알아보기 전에, Dio를 프로젝트에 추가하고 설정하는 몇 가지 필수 단계를 알아보자
1. Dio 패키지 추가하기
// terminal에 입력
flutter pub add dio
2. 기본 Dio 인스턴스 설정
- Dio 인스턴스를 생성하고, 기본 설정을 적용하는 것으로 시작. 이 설정에는 기본 URL, 타임아웃, 헤더 등이 포함될 수 있음
 
import 'package:dio/dio.dart';
void main() async {
  Dio dio = Dio();
  dio.options = BaseOptions(
    baseUrl: "https://api.example.com", // 요청의 기본 URL
    connectTimeout: 5000, // 연결 시간 초과 (밀리초)
    receiveTimeout: 3000, // 응답 시간 초과 (밀리초)
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json",
    },
  );
}
- baseUrl : 모든 요청에 대한 기본 URL로 각 요청의 상대 경로와 결합된다
 - connectTimeout, receiveTimeout: 네트워크 연결 및 응답에 대한 타임아웃 설정,(밀리초단위)
 - headers: 모든 요청에 공통으로 적용되는 HTTP 헤더
 
3. Flutter - Dio : 인터셉터 추가
- Dio는 요청/응답 인터셉터를 추가하여 HTTP 요청 및 응답을 가로채고, 
필요한 경우 수정하거나 추가 처리를 할 수 있게 해준다
ex) 요청 전에 로그를 찍기, 응답을 받은 후 데이터를 전처리하기 등 
dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    // 요청 전처리
    return handler.next(options); // 요청 계속 진행
  },
  onResponse: (response, handler) {
    // 응답 처리
    return handler.next(response); // 응답 계속 진행
  },
  onError: (DioError e, handler) {
    // 에러 처리
    return handler.next(e); // 에러 계속 진행
  },
));
- Dio 인스턴스 설정을 완료한 후에는 이를 사용하여 다양한 HTTP 요청을 보낼 수 있다 
인스턴스의 설정은 애플리케이션의 요구 사항에 따라 조정될 수 있으며,
이를 통해 효율적인 네트워크 통신을 구현할 수 있다 
4. HTTP 통신 : GET 요청 보내기, 서버로부터의 응답 처리, 에러 핸들링
✓ URL의 Query String을 통해 서버에 데이터를 전달한다
✓ URL에 전송할 데이터가 포함되어 있기 때문에 길이에 제한이 있을 수 있다
✓ 브라우저 기록, 캐싱 등에 사용되므로 민감한 데이터를 전송하기엔 적합하지 않다
import 'package:dio/dio.dart';
Future<void> sendGetRequest() async {
  var dio = Dio();
  
  try {
    // GET 요청
    var response = await dio.get(
      'https://example.com/data', 
      queryParameters: {'id': 12, 'name': 'xx'}
    );
    
    if (response.statusCode == 200) {
      // 성공 처리
      print('Response data: ${response.data}');
    } else {
      // 에러 처리
      print('Error: ${response.statusCode}');
    }
  } catch (e) {
    // 예외 처리
    print('Exception occurred: $e');
  }
}
- dio.get(url, queryParameters: params): 
url : 요청을 보낼 서버의 주소.
queryParameters : URL에 추가될 쿼리 매개변수를 담은 맵, 이 맵의 키와 값은 URL의 쿼리 스트링으로 변환된다. - response : 서버로부터의 응답을 포함하는 객체 
response.data는 서버가 보낸 데이터를 포함하고,
response.statusCode는 HTTP 응답 코드를 포함한다. - try-catch : 네트워크 요청 중 발생할 수 있는 예외 처리 네트워크 연결 문제, 타임아웃, 서버에서 반환된 에러 등을 적절히 처리할 수 있으며, 
response.statusCode를 검사해서 HTTP 상태 코드에 따라 다른 처리를 할 수 있다. 
예제 서버로 부터 목록을 가져오자
요구사항 : 성능을 위해 최초에는 목록을 소량만 가져오고 스크롤을 내리면 통신시 보내는 index를 통해 리스트를 계속해서 불러와라
// controller
final Repository repo = Repository();
//repository의 getList()에는 위의 설명을 통해 만들어진 서버와 통신하는 코드가 있다
RxList<ExampleModel> exampleList = <ExampleModel>[].obs;
RxBool maxMore = false.obs;
var requestStatus = RequestStatus.EMPTY.obs;
late int index;
// GET_LIST
Future<bool> getList() async {
  maxMore.value = false;
  requestStatus.value = RequestStatus.LOADING;
  exampleList.clear();
  index = 0;
  try {
    Map<String, dynamic>? response = await repo.getList(index);
    if (response?["list"] != null) {
      exampleList.addAll(response!["list"].map((i) => ExampleModel.fromJson(i)));
      if (response["list"].length < 10) {
        maxMore.value = true;
      }
      requestStatus.value = RequestStatus.SUCCESS;
    } else {
      requestStatus.value = RequestStatus.SUCCESS;
    }
    return true;
  } catch (e) {
    requestStatus.value = RequestStatus.ERROR;
    return false;
  }
}
// GET_MORE_LIST
Future<void> getMoreList() async {
  requestStatus.value = RequestStatus.LOADINGMORE;
  index++;
  try {
    Map<String, dynamic>? response = await repo.getNoti(index);
    if (response != null && response["totalPage"] >= index) {
      var newList = response["list"] as List;
      
      if (newList.isNotEmpty) {
        exampleList.addAll(newList.map((i) => ExampleModel.fromJson(i)));
        if (newList.length < 10) {
          maxMore.value = true;
        }
      }
    }
    requestStatus.value = RequestStatus.SUCCESS;
  } catch (e) {
    requestStatus.value = RequestStatus.ERROR;
    // 에러 메시지 처리 혹은 사용자에게 피드백 제공
  }
}
// list page
//...
final Controller controller = Get.find<Controller>();
final ScrollController _scrollController = ScrollController(); 
@override
void initState() {
	
	// 초기값 설정
  alarmController.getList();
  
	// 스크롤 추적해서 loading more
  _scrollController.addListener(_handleScrollEvent);
  super.initState();
}
@override
void dispose() {
  _scrollController.removeListener(_handleScrollEvent);
  _scrollController.dispose();
  super.dispose();
}
void _handleScrollEvent() {
  if (_isCloseToBottom(_scrollController) && _shouldLoadMore()) {
    controller.getMoreList();
  }
}
bool _isCloseToBottom(ScrollController controller) {
  return controller.position.pixels >= controller.position.maxScrollExtent - 350;
}
bool _shouldLoadMore() {
  return controller.requestStatus.value != RequestStatus.LOADINGMORE &&
         controller.requestStatus.value != RequestStatus.LOADING &&
         controller.requestStatus.value != RequestStatus.EMPTY &&
         controller.maxMore.value == false;
}
//...
child: ListView.builder(
  itemCount: controller.exampleList.length,
  shrinkWrap: true,
  controller: _scrollController,
  physics: BouncingScrollPhysics(),
  itemBuilder: (BuildContext context, int index) {
      return exampleList[index]['example']
    }
  )
- 설명
- 초기 데이터 로드: initState에서 controller.getList()를 호출하여 초기 데이터를 로드한다
 - 스크롤 리스너 설정: initState에서 _scrollController에 스크롤 리스너 _handleScrollEvent를 추가한다. 
이 리스너는 사용자가 리스트의 끝에 도달할 때 추가 데이터를 불러오는 기능을 담당한다. - 스크롤 리스너 해제 및 컨트롤러 정리: dispose 메서드에서 스크롤 리스너를 제거하고 _scrollController를 정리한다.
 - 스크롤 이벤트 처리: _handleScrollEvent 메서드는 스크롤 위치를 확인하고 _isCloseToBottom 및 _shouldLoadMore 조건을 평가하여 추가 데이터를 불러올지 결정한다
 - ListView.builder 구성: ListView.builder는 controller.exampleList의 데이터를 기반으로 항목을 동적으로 생성한다.
 
 - fromJson(i)에 대한 설명은 다음 포스팅에서 설명한다 (JSON 직렬화)