본문 바로가기
프로그래밍/Flutter

Flutter RestAPI 호출

by 코딩중독 2024. 6. 11.

목차

    1. 조회 api 만들기 (SpringBoot)

    @RequiredArgsConstructor
    @RequestMapping("/public-api/tenants")
    @RestController
    public class TenantPublicController {
    
        @GetMapping("")
        public ResponseEntity<TenantDTO.InfoList> getAllTenant(Pageable pageable) {
    
            TenantDTO.InfoList infoList = tenantService.getAllTenant(pageable);
    
            return ResponseEntity.ok(infoList);
    
        }
    
    }

     

    API 서버의 컨트롤러가 중요한 게 아니라 엔드포인트와 API가 반환하는 DTO 형태가 중요하다.

    스웨거 문서에서 볼 수 있는 /public-api/tenants 의 리턴 형태이다.

    count = info_list의 사이즈를 반환

    info_list 배열의 객체가 파싱할 데이터이다.

    {
      "count": 0,
      "info_list": [
        {
          "id": 0,
          "name": "string",
          "company_number": "string",
          "created_at": "2024-06-11T09:44:34.244Z",
          "updated_at": "2024-06-11T09:44:34.244Z"
        }
      ]
    }

     

    2. tenant_model.dart 만들기 (Flutter)

    API를 호출해서 전달받은 데이터를 담을 모델을 만든다.

    요청한 API의 형태는 TenantList로 받는다.

    count는 바로 값을 넣어주고 info_list의 json배열은 map을 사용해서 Tenant객체로 파싱한다.

    json의 키 값인 스네이크케이스 변수로 꺼내와야 한다.

    TenantsResponse가 최종 객체인데 API를 호출했을 때 response에 담기는 statusCode는 code에, 예외 메시지가 발생하면 message에 담기 위해 추가로 만들었다. 예외가 발생한다면 TenantList가 null일 상황을 대비했다.

    class Tenant {
      final int id;
      final String name, companyNumber;
      final DateTime createdAt, updatedAt;
    
      Tenant.fromJson(Map<String, dynamic> json)
          : id = json['id'],
            name = json['name'],
            companyNumber = json['company_number'],
            createdAt = DateTime.parse(json['created_at']),
            updatedAt = DateTime.parse(json['updated_at']);
    }
    
    class TenantList {
      final int count;
      final List<Tenant> tenantList;
    
      TenantList({required this.count, required this.tenantList});
    
      factory TenantList.fromJson(Map<String, dynamic> json) {
        final List<dynamic> infoList = json['info_list'];
        final List<Tenant> tenantList =
            infoList.map((item) => Tenant.fromJson(item)).toList();
        return TenantList(count: json['count'], tenantList: tenantList);
      }
    }
    
    class TenantsResponse {
      final int code;
      final String message;
      final TenantList? tenantList;
    
      TenantsResponse.of(this.code, this.message, Map<String, dynamic>? json)
          : tenantList = json != null ? TenantList.fromJson(json) : null;
    }

     

    3. api_service.dart 만들기 (Flutter)

    모델을 만들었으니 API를 호출한다.

    데이터를 받아와야 하니 비동기로 처리하기 위해 async / await를 사용한다.

    실제로 배포 중인 서버가 아닌 로컬에서 테스트를 진행하기 위해 base_url을 지정했다.

    로컬호스트 환경은 아이폰과 안드로이드 요청주소가 다르다.

    class ApiService {
    
      static String base_url =
      Platform.isIOS ? "http://localhost:8080" : "http://10.0.2.2:8080";
      static const String tenant_list = "public-api/tenants";
    
      static Future<TenantsResponse> getTenantsResponse() async {
        Uri url = Uri.parse("$base_url/$tenant_list");
        final response = await get(url);
        if (response.statusCode == 200) {
          return TenantsResponse.of(response.statusCode, "success",
              json.decode(utf8.decode(response.bodyBytes)));
        }
        return TenantsResponse.of(response.statusCode, response.body, null);
      }
    
    }

     

    4. tenant_provider.dart 만들기 (Flutter)

    생략

    다른 데이터는 변경사항에 따라 계속 상태관리가 필요한데, 지금 테스트하는 API는 회원가입 할 때 드롭다운버튼에 보여줄 목록이라 provider 적용을 하지 않은걸 지금 생각이 났다.....

    다음 글에서 provider 내용과 retrofit으로 변경하는 내용을 적어야겠...

     

    5. api 호출 (Flutter)

    위에서 만든 ApiService 클래스의 getTenantsResponse 메서드를 호출한다.

    FutureBuilder를 사용하고 타입을 지정해 준다.

    snapshot의 상태에 따라 처리한다.

    snapshot의 연결상태가 대기 중이면 프로그레스를 보여준다.

    snapshot의 상태가 에러도 없고 데이터도 포함한다면 드롭다운버튼 위젯을 만들고 데이터를 넣어준다.

    Future<TenantsResponse> tenantList = ApiService.getTenantsResponse();
    
    @override
      Widget build(BuildContext context) {
        return FutureBuilder<TenantsResponse>(
          future: tenantList,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const CircularProgressIndicator();
            } else if (snapshot.hasError) {
              return const Text('오류가 발생했습니다.');
            } else if (!snapshot.hasData ||
                snapshot.data!.tenantList!.tenantList.isEmpty) {
              return const Text('입주사 목록이 없습니다.');
            } else {
              return DropdownButton<Tenant>(
                isExpanded: true,
                value: selectedTenant,
                items: snapshot.data!.tenantList!.tenantList.map((Tenant item) {
                  return DropdownMenuItem<Tenant>(
                    value: item,
                    child: Text(
                      '${item.name} (${item.companyNumber})',
                      style: const TextStyle(
                        fontSize: 18,
                      ),
                    ),
                  );
                }).toList(),
                onChanged: (Tenant? value) {
                  setState(() {
                    selectedTenant = value;
                  });
                  widget.onTenantSelected(value!);
                },
                hint: const Text('입주사를 선택하세요'),
              );
            }
          },
        );
      }

     

    6. 결과

     

    이런 데이터가 하나뿐...