일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- db
- 애니메이션
- 마우이
- Firebase
- Binding
- Maui
- HTML
- Animation
- Flutter
- React JS
- spring boot
- 함수
- 바인딩
- AnimationController
- .NET
- 오류
- GitHub
- MSSQL
- 플러터
- 자바스크립트
- listview
- MS-SQL
- 파이어베이스
- page
- 닷넷
- 리엑트
- 깃허브
- typescript
- MVVM
- JavaScript
Archives
- Today
- Total
개발노트
[2024.06.28] API 서버에 만든 호출 Flutter 연결하기 본문
반응형
이번 포스팅은 Flutter에서 이전에 만들었던 DTO JSON 데이터를 받아오기위해 Flutter안에 API서버 호출을 만들어본다.
화면 시나리오
1. 모든 칵테일을 보여준다. (호출 1)
2. 원하는 칵테일을 클릭한다.
3. 해당 칵테일의 정보를 보여주는 화면이 나온다. (호출 2)
따라서 이렇게 두가지 호출을 만들어 줄 예정
- 호출 1은 메소드 쿼리를 이용하여 FindAll() 로 모든 칵테일을 보여준다.
- 호출 2는 클릭된 칵테일의 식별 ID를 파라미터로 받고 DTO를 이용하여 해당 칵테일 상세정보를 맞춤형으로 만들어서 클라이언트에 데이터를 보내준다.
이전 포스팅에서 호출1,2는 만들어졌다.
- 이전포스팅: https://mroh1226.tistory.com/223
Model 부분
호출 1과 호출 2를 CocktailsInfo 라는 Entity 하나로 같이 사용할 수 있도록 필요한 필드에 nullable을 설정하였다.
cocktailsInfo (칵테일 정보)
class CocktailsInfo {
final int cocktailId;
final String name;
final String nameEng;
final String detail;
final double sweetness;
final double acidity;
final double strength;
final int state;
final DateTime createdAt;
final int mixtypeId;
final String? mixtypeName;
final List<IngredientInfo>? ingredientList;
CocktailsInfo({
required this.cocktailId,
required this.name,
required this.nameEng,
required this.detail,
required this.sweetness,
required this.acidity,
required this.strength,
required this.state,
required this.createdAt,
required this.mixtypeId,
required this.mixtypeName,
required this.ingredientList,
});
factory CocktailsInfo.fromJson(Map<String, dynamic> json) {
return CocktailsInfo(
cocktailId: json['cocktailId'],
name: json['name'],
nameEng: json['nameEng'],
detail: json['detail'],
sweetness: json['sweetness'].toDouble(),
acidity: json['acidity'].toDouble(),
strength: json['strength'].toDouble(),
state: json['state'],
createdAt: DateTime.parse(json['createdAt']),
mixtypeId: json['mixtypeId'],
mixtypeName: json['mixtypeName'], //nullable로 표현했기 때문에 삼항연산자 안적어줌
ingredientList: json['ingredientDTOList'] != null
? (json['ingredientDTOList'] as List)
.map((i) => IngredientInfo.fromJson(i))
.toList()
: null,
);
}
Nullable 필드 처리:
- mixtypeName과 ingredientList 필드는 nullable로 설정되어 있어 JSON 데이터에서 해당 값들이 없을 경우를 처리
이는 데이터베이스나 API 응답에서 해당 필드가 null일 수 있기 때문에, 이를 고려하여 예외 처리할 수 있음 - mixtypeName 필드는 삼항 연산자를 사용하지 않아도 되는 이유는 이미 nullable로 선언되어 있어, JSON에서 해당 키가 없거나 null이면 null로 처리되기 때문
- ingredientList 필드의 경우, JSON 데이터에서 ingredientDTOList가 null이 아닌 경우에만 리스트로 변환하여 할당하고, null인 경우에는 null로 처리함
CocktailsInfo 안에 IngredientInfo 객체를 리스트로 두고, 레시피 재료 리스트와 양을 담는다.
IngredientInfo (재료 정보)
class IngredientInfo {
final double recipeAmount;
final int ingredientId;
final String ingredientName;
final String ingredientDetail;
final int ingredientState;
final int categoryId;
final String categoryName;
final String categoryUnit;
IngredientInfo({
required this.recipeAmount,
required this.ingredientId,
required this.ingredientName,
required this.ingredientDetail,
required this.ingredientState,
required this.categoryId,
required this.categoryName,
required this.categoryUnit,
});
factory IngredientInfo.fromJson(Map<String, dynamic> json) {
return IngredientInfo(
recipeAmount: json['recipeAmount'].toDouble(),
ingredientId: json['ingredientId'],
ingredientName: json['ingredientName'],
ingredientDetail: json['ingredientDetail'],
ingredientState: json['ingredientState'],
categoryId: json['categoryId'],
categoryName: json['categoryName'],
categoryUnit: json['categoryUnit'],
);
}
Map<String, dynamic> toJson() {
return {
'recipeAmount': recipeAmount,
'ingredientId': ingredientId,
'ingredientName': ingredientName,
'ingredientDetail': ingredientDetail,
'ingredientState': ingredientState,
'categoryId': categoryId,
'categoryName': categoryName,
'categoryUnit': categoryUnit,
};
}
}
ModelView 부분
Riverpod를 상태관리하는 프로젝트이기 때문에 AsyncNotifierProvider를 사용한다.
그 안에 아래와 같이 호출 2개를 만들어준다.
getCocktailList() 메소드 (호출 1: 칵테일 전체리스트 가져오기)
//호출 1 (칵테일 전체 리스트 가져오기)
Future<List<CocktailsInfo>> getCocktailList() async {
Response response = await get(Uri.parse('$baseUrl/all'));
if (response.statusCode == 200) {
final List cocktails = jsonDecode(utf8.decode(response.bodyBytes));
return cocktails.map((e) => CocktailsInfo.fromJson(e)).toList();
} else {
throw Exception();
}
}
- 엔드포인트 Uri를 파싱하여 Response를 받고 이를 .map으로 List에 담는다.
- 이때 받아오는 JSON 데이터에는 재료정보가 포함되어있지않아 CocktailsInfo 객체에서 설정해준대로 칵테일 정보만 생성된다.
getCocktailInfo(int cocktailId) 메소드 (호출 2: 선택된 칵테일 정보와 레시피 재료 정보 가져오기)
//호출 2 (선택된 칵테일의 정보와 레시피 재료 등 가져오기)
Future<CocktailsInfo> getCocktailInfo(int cocktailId) async {
Response response = await get(Uri.parse('$baseUrl/info/$cocktailId'));
if (response.statusCode == 200) {
final cocktailsInfo = jsonDecode(utf8.decode(response.bodyBytes));
return CocktailsInfo.fromJson(cocktailsInfo);
} else {
throw Exception();
}
}
- 파라미터로 cocktailId를 받게 되며, 이를 엔드포인트에 붙여주고 선택된 칵테일 정보와 레시피 재료정보를 받아온다.
- CocktailsInfo 객체에서 설정해준 것과 같이 IngedientInfo 리스트를 포함한다.
칵테일 전체를 보여주는 화면에 호출 1을 이용하여 리스트로 보여준다.
CocktailListWidget.dart (칵테일 전체 리스트를 보여주는 화면)
class CocktailListWidget extends ConsumerStatefulWidget {
const CocktailListWidget({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_CocktailListWidgetState();
}
class _CocktailListWidgetState extends ConsumerState<CocktailListWidget> {
late Future<List<CocktailsInfo>> cocktailList =
ref.read(searchProvider.notifier).getCocktailList();
void _onTap(int cocktailId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CocktailsDetailScreen(cocktailId: cocktailId),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.pink,
body: Center(
child: FutureBuilder(
future: cocktailList,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return GestureDetector(
onTap: () {
ref.read(searchProvider.notifier).getCocktailList();
},
child: const CircularProgressIndicator());
} else {
return GridView.builder(
itemCount: snapshot.data!.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 9 / 16,
crossAxisSpacing: 5,
mainAxisExtent: 250,
mainAxisSpacing: 0),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => _onTap(snapshot.data![index].cocktailId),
child: Container(
height: 350,
width: 200,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15)),
child: Stack(
children: [
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Image.network(
fit: BoxFit.fill,
"https://loremflickr.com/200/35${index % 9}/whisky,glass"),
),
Positioned.fill(
top: 200,
right: 35,
child: Column(
children: [
Text(
snapshot.data![index].name,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
backgroundColor:
Colors.grey.withOpacity(0.5)),
)
],
),
)
],
),
),
);
},
);
}
},
),
));
}
}
- ref.read() 를 통해 ViewModel에서 생성해준 호출1 메소드를 불러 값을 받아온다.
- FutureBuilder를 이용하여 snapshot에 호출성공 여부에 따라 리스트를 보여주거나 로딩 서클을 보여준다.
- 네비게이션을 이용하여 칵테일을 클릭했을 때 상세정보 화면으로 이동하게 된다.
*이미지는 랜덤으로 생성해주는 사이트를 이용함
선택된 칵테일Id를 호출 2을 이용하여 상세정보(레시피, 재료등)을 보여준다.
CocktailsDetailScreen.dart (칵테일 상세정보(레시피, 재료)를 보여주는 화면)
class CocktailsDetailScreen extends ConsumerStatefulWidget {
final int cocktailId;
const CocktailsDetailScreen({required this.cocktailId, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_CocktailsDetailScreenState();
}
class _CocktailsDetailScreenState extends ConsumerState<CocktailsDetailScreen>{
late Future<CocktailsInfo> cocktailsInfo;
@override
void initState() {
super.initState();
cocktailsInfo = _fetchCocktailInfo();
}
Future<CocktailsInfo> _fetchCocktailInfo() async {
return await ref
.read(searchProvider.notifier)
.getCocktailInfo(widget.cocktailId);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<CocktailsInfo>(
future: cocktailsInfo,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final cocktail = snapshot.data!;
return Center(
child: Column(
children: [
const SizedBox(
height: 100,
),
SizedBox(
width: DeviceSize.deviceWidth,
height: DeviceSize.deviceHeight / 2,
child: Column(
children: [
Text(cocktail.cocktailId.toString()),
Text(cocktail.nameEng),
Text(cocktail.name),
Text(cocktail.detail),
Text(cocktail.strength.toString()),
Text(cocktail.acidity.toString()),
Text(cocktail.sweetness.toString()),
],
),
),
Expanded(
child: ListView.builder(
itemCount: cocktail.ingredientList!.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
cocktail.ingredientList![index].ingredientName),
subtitle: Text(cocktail
.ingredientList![index].recipeAmount
.toString()),
);
},
),
),
],
),
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
}
*블로그에 올리기 위해 불러온 정보를 텍스트로 간단하게 표현함
- _fetchCocktailInfo() 메소드를 통해 선택된 cocktailId를 ViewModel에 만들어준 호출2 메소드 파라미터로 넘겨준다.
- 호출1 이 사용된 화면과 마찬가지로 FutureBuilder를 사용하여 데이터 수신 여부에 따라 위젯을 다르게 보여줌
이렇게 화면에 사용될 DTO를 만들고, 생성된 JSON 데이터를 Flutter 화면으로 가져오는 과정을 직접 구현해보았다.
이렇게 받아온 수치나, 정보를 에니메이션에 이용하거나 응용할 수 있으며, 실제로는 적용해둔 상태이다.
이렇게 앱 화면별로 사용될 데이터 목적에 따라 직접 JSON 데이터를 커스텀하고 호출하는 과정을 다시한번 따라가보는 시간이 되었다.
반응형
'1인 개발 일지' 카테고리의 다른 글
[2024.06.20] 화면에 필요한 JSON 데이터 만들기 (Spring Boot) (0) | 2024.06.20 |
---|---|
[2024.05.21] API 서버 DTO 작업중 (0) | 2024.05.21 |
[2024.04.19] DB, API Server, Client App 전부 연동 완료 (0) | 2024.04.19 |
[2024.04.12] API Server, DB 연동 완료 (0) | 2024.04.12 |
[2024.04.04] MySQL 스키마 1차 구성완료 (0) | 2024.04.04 |
Comments