28일차에서도 데이터 Serialization을 연습한다. 직렬화를 사용하여 딕셔너리 앱을 만들었다.
학습한 내용
- 딕셔너리 앱 만들기
Mixin 이란 Dart의 공식 문서에 "여러 클래스 계층에서 클래스 코드를 재사용하는 방법입니다."라고 설명되어 있다.
또한 extends
를 사용하면 다중 상속이 불가능한데 Mixin은 with
키워드와 함께 다중 상속을 가능하게 한다.
예시를 들어보면 우선 아래는 extends
를 사용하여 클래스를 상속 받는 코드이다.
class Player {
void play() {
print("경기하는 중...");
}
}
class BasketBallPlayer extends Player{
void play() {
super.play();
print("농구경기 하는 중...");
}
}
//경기하는 중...
//농구경기 하는 중...
이렇게 extends
는 단일 상속만 가능한데 Mixin을 사용하면 다중 상속이 가능하다. 예를 들어 아래 코드처럼 운동 기능을 다른 클래스로 나누고, 축구 선수, 농구 선수에게 다중 상속을 할 수 있다.
mixin CanRun {
void run() {
print("뛰는 중...");
}
}
mixin CanJump {
void jump() {
print("점프하는 중...");
}
}
mixin CanShoot {
void shoot() {
print("슈팅하는 중...");
}
}
class Striker extends FootBallPlayer with CanRun,CanJump,CanShoot {
void play() {
run();
shoot();
jump();
}
}
//뛰는 중...
//슈팅하는 중...
//점프하는 중...
class Pitcher extends BaseBallPlayer with CanRun,CanJump {
void play() {
run();
jump();
}
}
//뛰는 중...
//점프하는 중...
- 딕셔너리 앱 만들기
- 추가 사항
공개된 API를 분석하고, 클래스를 활용하여 적용 후 딕셔너리 앱을 만들고자 한다.
- 아래의 공개된 API에서 데이터를 받아온다.
- 반드시 Dict 클래스를 만들고 Serialization을 진행한다.
- 필요한 요소만 클래스에 적용해도 되지만, 최대한 많은 데이터를 가져올 수 있도록 한다.
- 만약 검색어가 존재하지 않는 단어로 서버에서 정상적인 응답을 받지 못한 경우 아무것도 출력되지 않도록 한다.
- 검색어를 입력하고 엔터를 누르면 (TextField의 onSubmitted) 주어진 API를 통해 검색하도록 한다.
- 이 때, 결과는 커스텀 위젯을 최대한 활용하여 보여줄 수 있도록 한다.
- 커스텀 위젯은 최대한 분할되어 있을수록 좋다.
- 예) MeaningCard
- 제공되는 코드의 예시를 활용한다.
// 추가 코드를 작성할 것. 본 소스는 디자인만 작성되어 있으며
// 이 기본 틀을 통하여 과제에 필요한 소스코드를 추가적으로 구현할 것.
import 'package:flutter/material.dart';
class MainPage extends StatefulWidget {
const MainPage({super.key});
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dictionary App'),
elevation: 0,
centerTitle: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
decoration: const InputDecoration(
hintText: "Search",
suffixIcon: Icon(Icons.search),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
),
onSubmitted: (value) {},
),
),
),
],
),
],
),
),
);
}
}
-pubspec.yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dio: ^5.0.0
pubspec.yaml
에 필요한 패키지를 설치했다.
import 'package:flutter/material.dart';
import 'package:my_app/page/main_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// root Widget
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: MainPage(), // 메인 페이지 호출
);
}
}
main.dart
에서는 MainPage
를 호출한다.
그리고, 데이터 Serialization을 하기 위해 아래 코드들 처럼 총 5개의 모델을 만들었다. 각각 Dict
, Phonetic
, Meaning
, Definition
, License
이며, 모델에 대한 설명은 생략했다.
import 'package:my_app/model/license.dart';
import 'package:my_app/model/meaning.dart';
import 'package:my_app/model/phonetic.dart';
class Dict {
String word; //단어
String? phonetic; //발음
List<Phonetic> phonetics; //발음 리스트
List<Meaning> meanings; //의미 리스트
License license; //라이센스
List<String> sourceUrls; //소스 URL
Dict({
required this.word,
required this.phonetic,
required this.phonetics,
required this.meanings,
required this.license,
required this.sourceUrls,
});
factory Dict.fromMap(Map<String, dynamic> map) {
return Dict(
word: map['word'],
phonetic: map['phonetic'],
phonetics:
List<Phonetic>.from(map['phonetics'].map((e) => Phonetic.fromMap(e))),
meanings:
List<Meaning>.from(map['meanings'].map((e) => Meaning.fromMap(e))),
license: License.fromMap(map['license']),
sourceUrls: List<String>.from(map['sourceUrls']),
);
}
}
import 'package:my_app/model/license.dart';
class Phonetic {
String? text; //발음 텍스트
String? audio; //발음 오디오
String? sourceUrl; //소스 URL
License? license; //발음 라이센스
Phonetic({
required this.text,
required this.audio,
required this.sourceUrl,
required this.license,
});
factory Phonetic.fromMap(Map<String, dynamic> map) {
return Phonetic(
text: map['text'],
audio: map['audio'],
sourceUrl: map['sourceUrl'],
license: map['license'] != null ? License.fromMap(map['license']) : null,
);
}
}
import 'package:my_app/model/definition.dart';
class Meaning {
String partOfSpeech; //품사
List<Definition> definitions; //정의 리스트
List<String> synonyms; //유의어 리스트
List<String> antonyms; //반대어 리스트
Meaning({
required this.partOfSpeech,
required this.definitions,
required this.synonyms,
required this.antonyms,
});
factory Meaning.fromMap(Map<String, dynamic> map) {
return Meaning(
partOfSpeech: map['partOfSpeech'],
definitions: List<Definition>.from(
map['definitions'].map((e) => Definition.fromMap(e))),
synonyms: List<String>.from(map['synonyms']),
antonyms: List<String>.from(map['antonyms']),
);
}
}
class Definition {
String definition; //정의
List<String> synonyms; //유의어
List<String> antonyms; //반대어
String? example; //예시
Definition({
required this.definition,
required this.synonyms,
required this.antonyms,
required this.example,
});
factory Definition.fromMap(Map<String, dynamic> map) {
return Definition(
definition: map['definition'],
synonyms: List<String>.from(map['synonyms']),
antonyms: List<String>.from(map['antonyms']),
example: map['example'],
);
}
}
class License {
String name; //라이센스 이름
String url; //라이센스 URL
License({
required this.name,
required this.url,
});
factory License.fromMap(Map<String, dynamic> map) {
return License(
name: map['name'],
url: map['url'],
);
}
}
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:my_app/model/dict.dart';
import 'package:my_app/widget/meaning_card.dart';
class MainPage extends StatefulWidget {
const MainPage({super.key});
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
Dio dio = Dio(); //dio객체
String url = 'https://api.dictionaryapi.dev/api/v2/entries/en/'; //데이터 요청 URL
Dict? currentWord; //현재 검색 단어
//검색한 단어의 정보 가져오기
getData(String word) async {
try {
var response = await dio.get(url + word); //url에 입력한 단어를 더해 데이터 요청
//데이터 요청 성공
if (response.statusCode == 200) {
currentWord = Dict.fromMap(response.data.first);
}
} on DioError {
//데이터 요청 실패(잘못된 단어인 경우)
currentWord = null;
} finally {
setState(() {}); //화면 새로고침
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dictionary App'),
elevation: 0,
centerTitle: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
//단어 검색 창
child: TextField(
decoration: const InputDecoration(
hintText: "Search",
suffixIcon: Icon(Icons.search),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
),
onSubmitted: (value) {
//검색후 제출하면 네트워크에 단어 정보 요청
getData(value);
},
),
),
),
],
),
//현재 검색한 단어의 정보가 있는 경우
if (currentWord != null) MeaningCard(word: currentWord!),
],
),
),
);
}
}
MainPage
는 주어진 코드를 사용했다.
여기에 dio
를 사용해서 네트워크에 데이터를 요청하는 getData()
메소드를 작서했다. 해당 메소드는 입력한 단어를 매개변수로 전달받아 URL에 더해 데이터를 요청한다. 정상적으로 데이터를 받은 경우 currentWord
변수에 저장하고, 만약 데이터를 받지 못한 경우 null을 저장한다. 마지막으로 setState()
를 호출하여 화면을 새로고침 한다.
이 메소드는 TextField
의 onSubmitted
이벤트가 발생하면 실행되도록 한다.
결과를 화면에 출력하기 위해 Column
안에서 currentWord
가 null이 아닐 경우 커스텀 위젯인 MeaningCard
를 출력했다.
import 'package:flutter/material.dart';
import 'package:my_app/model/dict.dart';
class MeaningCard extends StatelessWidget {
const MeaningCard({super.key, required this.word});
final Dict word; //검색한 단어 정보
Widget build(BuildContext context) {
return Expanded(
child: ListView(
physics: const BouncingScrollPhysics(),
children: [
Card(
color: Colors.white12,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//단어
Text(
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
),
word.word,
),
//단어의 뜻 리스트 만큼 반복
for (var meaning in word.meanings)
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//품사
Text(
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
),
meaning.partOfSpeech,
),
const SizedBox(height: 8),
//유의어
const Text('- Synonyms:'),
for (var synonym in meaning.synonyms) Text(synonym),
const SizedBox(height: 8),
//정의
const Text('- Definition:'),
for (var def in meaning.definitions)
Text(def.definition),
const SizedBox(height: 8),
//반대어
const Text('- Antonyms:'),
for (var antonym in meaning.antonyms) Text(antonym),
],
),
),
],
),
),
),
],
),
);
}
}
MeaningCard
는 검색한 단어를 전달받아 단어의 품사와, 유의어, 정의, 반대어를 출력한다.
단어의 유사어와 정의, 반대어는 for문을 사용해 모두 출력했다.
과제를 수행하고 난 뒤 강의를 듣고 아쉬웠던 점을 정리했다.
검색한 단어의 의미에 대한 내용을 for문을 사용하여 출력했는데 ListView.builder
를 사용했으면 코드가 더 간결할 것 같다.
전체 MeaningCard
가 스크롤이 가능하도록 ListView
로 만들어졌기 때문에 단어의 뜻을 출력하는 ListView.builder
에서는 스크롤이 되지 않도록 physics
를 설정하고, Column
안에서 에러가 발생하지 않도록 shrinkWrap
을 true로 설정해야 한다.
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: ...
),
이번주도 모든 과제를 마무리했다. 이번주에는 일주일 동안 데이터 Serialization만 계속 연습했다. 데이터가 연결되어 쉽게 사용할 수 있게 되는게 뭔가 신기하면서 점점 적응이 되고 있는 것 같다. ㅋㅋㅋㅋ 추가 내용 정리는 데이터 직렬화만 연습하고 있어서 따로 학습하는 내용이 없기 때문에 이번주는 계속 클래스에 대해 하나씩 쓰고 있다. 오늘은 mixin에 대해 간단히 정리했는데 다중 상속을 필요로 할 때 사용된다고 하고, 이해도 모두 했지만, 직접 써보지는 않아서 크게 와닿지는 않는다. 그건 그렇고 오늘 코드에서 MeaningCard를 만들 때 뭔가 반복문을 많이 쓴거 같아서 효율적으로 잘 짜지 못한 것 같다...ㅠㅠ 다른 방법이 있을라나..?