요구사항
- 다음의 공개된 API를 분석하고, 클래스를 활용하여 적용 후
연락처를 보여주는 앱을 다음과 같이 만드시오.
- https://jsonplaceholder.typicode.com/users
- 반드시 Profile 클래스를 만들고 Serialization을 진행할 수 있도록 하시오.
- 각 사람별 이미지를 CircleAvatar를 통해 보여주도록 한다.
- 이 때, 해당 API에는 프로필이미지가 없으므로 다음의 이미지 URL을 활용한다.
- https://xsgames.co/randomusers/assets/avatars/male/{번호}.jpg
- 위 URL에 들어가는 {번호}에는 유저ID를 넣어 사용할 수 있도록 한다.
- 애니메이션 효과를 적절히 사용하여 최대한 위 결과물과 비슷하도록 만드시오.
- 네트워크에 통신하여 데이터를 가져오는 것은 첫 페이지(리스트 보여주는 페이지)에만 할 수 있도록 한다.
서클아바타를 transform.translate로 -55만큼 이동시켰다.
잘린다.. appbar 뒤에 flexibleSpace를 두어서 이미지를 넣었는데, 스택을 사용해서 겹치도록 하는 방법밖에 없는 것 같다.
class UserPage extends StatelessWidget {
const UserPage({
super.key,
required this.user,
required this.imageUrl,
});
final User user;
final String imageUrl;
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(200),
child: AppBar(
title: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
),
],
),
backgroundColor: Colors.transparent,
elevation: 0,
// toolbarHeight: 200, 텍스트가 앱바 공간 중간에 위치하게 된다.
flexibleSpace: Container(
decoration: BoxDecoration(
image: DecorationImage(
//배경이미지 흐리게
fit: BoxFit.cover,
colorFilter: const ColorFilter.mode(
Colors.black45,
BlendMode.darken,
),
image: NetworkImage(imageUrl),
),
),
),
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 250,
),
Transform.translate(
offset: const Offset(0, -55),
child: CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(
imageUrl,
),
),
),
Text(
user.name,
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
const Divider(),
InformationTile(user: user),
const Divider(),
CompanyTile(user: user),
],
),
),
);
}
}
stack으로 감싸자마자 해결됐다...
class UserPage extends StatelessWidget {
const UserPage({
super.key,
required this.user,
required this.imageUrl,
});
final User user;
final String imageUrl;
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(200),
child: AppBar(
title: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
),
],
),
backgroundColor: Colors.transparent,
elevation: 0,
),
),
body: Stack(
children: [
Container(
height: 300,
decoration: BoxDecoration(
image: DecorationImage(
//배경이미지 흐리게
fit: BoxFit.cover,
colorFilter: const ColorFilter.mode(
Colors.black45,
BlendMode.darken,
),
image: NetworkImage(imageUrl),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 250,
),
CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(
imageUrl,
),
),
const SizedBox(height: 16),
Text(
user.name,
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
const Divider(),
InformationTile(user: user),
const Divider(),
CompanyTile(user: user),
],
),
),
],
),
);
}
}
List<User> users = [];
if(res.statusCode == 200) {
var data = List<Map<String, dynamic>>.from(res.data);
for (var userMap in data) {
users.add(User.fromMap(userMap));
};
}
요구사항
- 다음의 공개된 API를 분석하고, 클래스를 활용하여 적용 후
딕셔너리 앱을 다음과 같이 만드시오.
- https://api.dictionaryapi.dev/api/v2/entries/en/{검색어}
- 반드시 Dict 클래스를 만들고 Serialization을 진행할 수 있도록 하시오.
- 필요한 요소만을 클래스에 적용하는 것은 허용되지만,
최대한 많은 데이터를 가져올 수 있도록 한다.
- 이 때, 만약 검색어가 존재하지 않는 단어로 서버에서 정상적인 응답을 못받았을 경우는 아무 것도 출력되지 않도록 한다.
- 검색어를 입력하고 엔터를 누르면 (TextField의 onSubmitted) 주어진 API를 통해 검색하도록 한다.
- 이 때, 결과는 아래에 커스텀 위젯을 최대한 활용하여 보여줄 수 있도록한다.
- 커스텀 위젯은 최대한 분할되어 있을수록 좋다.
- 예) MeaningCard..
- 다음의 제공되는 코드를 사용할 수 있다.
class Dict {
String word;
String phonetic;
List<Phonetic> phonetics;
List<Meaning> meanings;
License license;
List<dynamic> sourceUrls;
Dict({
required this.word,
required this.phonetic,
required this.phonetics,
required this.meanings,
required this.license,
required this.sourceUrls,
});
... 생략
뭣모르고 다 타입을 지정했는데 meaning만 쓰더라... 머쓱
class Meaning {
String partOfSpeech;
List<Definition> definitions;
List<dynamic> synonyms;
List<dynamic> antonyms;
Meaning({
required this.partOfSpeech,
required this.definitions,
required this.synonyms,
required this.antonyms,
});
...생략
class Definition {
String definition;
List<dynamic> synonyms;
List<dynamic> antonyms;
Definition({
required this.definition,
required this.synonyms,
required this.antonyms,
});
... 생략
처음엔 List<dynamic>
대신 List<String>
이런식으로 했는데, 타입 에러가 나서 수정했다.
searchWord(String word) async {
Dio dio = Dio();
String url = 'https://api.dictionaryapi.dev/api/v2/entries/en/';
try {
var res = await dio.get(url + word);
var data = List<Map<String, dynamic>>.from(res.data); //1. MapList로 변경
dict = data.map((e) => Dict.fromMap(e)).toList(); //2. 직렬화
} catch (e) {
print(e);
}
}
이 부분이 핵심이다.
처음 데이터를 받아오면 List 메서드를 통해 Map<String, dynamic>으로 데이터타입을 바꿔준다.
그리고 Map 데이터를 내가 정의한 클래스를 사용해 직렬화한다.
위 데이터는 List라서 map을 써서 돌려준다.
FutureBuilder
로 void 함수를 연결했더니 나옴.
아마 다른 방법도 있을 것 같은데 아직 강의를 보지 못해서...
오프라인 강의를 나갔더니 자꾸 강의가 밀린다 주말에 몰아봐야겠다 흑흑..
class MainPage extends StatefulWidget {
const MainPage({super.key});
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<Dict>? dict = [];
String word = '';
searchWord(String word) async {
Dio dio = Dio();
String url = 'https://api.dictionaryapi.dev/api/v2/entries/en/';
try {
var res = await dio.get(url + word);
var data = List<Map<String, dynamic>>.from(res.data); //1. MapList로 변경
dict = data.map((e) => Dict.fromMap(e)).toList(); //2. 직렬화
} catch (e) {
print(e);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dictionary App'),
elevation: 0,
centerTitle: false,
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
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) {
dict = []; //초기화
word = value;
setState(() {});
},
),
),
),
),
],
),
FutureBuilder(
future: searchWord(word),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (dict!.isNotEmpty) {
return Column(
children: dict!
.map(
(singleDict) => MeaningCard(dict: singleDict),
)
.toList(),
);
} else {
return const Text('');
}
}
return const CircularProgressIndicator();
},
),
],
),
),
);
}
}
class MeaningCard extends StatelessWidget {
const MeaningCard({super.key, required this.dict});
final Dict dict;
Widget build(BuildContext context) {
List<Meaning> meanings = dict.meanings;
return Container(
margin: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Color.fromARGB(255, 57, 57, 57),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
dict.word,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: meanings.length,
itemBuilder: (BuildContext context, int index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
meanings[index].partOfSpeech,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
DefinitionTile(
definitions: meanings[index].definitions),
// const SizedBox(
// height: 200,
// ),
],
),
),
],
);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider();
},
),
],
),
);
}
}
class DefinitionTile extends StatelessWidget {
const DefinitionTile({super.key, required this.definitions});
final List<Definition> definitions;
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('- Definition'),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: definitions.map((e) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(e.definition),
],
);
}).toList(),
),
const Text('-Synonyms: '),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: definitions.map((e) {
if (e.antonyms.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(e.synonyms.join(', ')),
],
);
}
return Container();
}).toList(),
),
const Text('-Antonyms: '),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: definitions.map((e) {
if (e.antonyms.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(e.antonyms.join(', ')),
],
);
}
return Container();
}).toList(),
),
],
);
}
}
읭 definition에 example이라는 값이 들어오기도 한다.
String? example;
factory Dict.fromMap(Map<String, dynamic> map) {
return Dict(
word: map['word'] as String,
phonetic: map['phonetic'] as String,
phonetics: List<Phonetic>.from(
(map['phonetics'] as List<dynamic>).map<Phonetic>(
(x) => Phonetic.fromMap(x as Map<String, dynamic>),
),
),
meanings: List<Meaning>.from(
(map['meanings'] as List<dynamic>).map<Meaning>(
(x) => Meaning.fromMap(x as Map<String, dynamic>),
),
),
license: License.fromMap(map['license'] as Map<String, dynamic>),
sourceUrls: List<dynamic>.from((map['sourceUrls'] as List<dynamic>)),
);
}
factory Dict.fromMap(Map<String, dynamic> map) {
return Dict(
word: map['word'],
phonetic: map['phonetic'],
phonetics: List<Phonetic>.from(
map['phonetics'].map(
(x) => Phonetic.fromMap(x),
),
),
meanings: List<Meaning>.from(
map['meanings'].map(
(x) => Meaning.fromMap(x),
),
),
license: License.fromMap(map['license']),
sourceUrls: List<String>.from(['sourceUrls']),
);
}
필자는 그냥 List로 오는 걸 받아서 그대로 나타냈지만 강의에서는 .first를 사용해서 첫번째 객체만 불러왔다.
try-catch 사용
dict=null, setState해줌
Stack(
clipBehavior: Clip.none, //넘치는 부분 잘리지 않도록 함
children: [
Image.network(
url,
fit: Boxfit.cover,
widthL double.infinity,
height: 300,
),
Positioned(
bottom: -48,
child: CircleAvatar(
radius: 48,
backgroundImage: NetworkImage(url,),
)
)
]
)
마저 하기 위해 돌아왔당ㅎ
뭐 별거 아니지만...
데이터를 찍어보니 initState는 제대로 돼서 데이터가 찍힘. 그런데 ListView 내에서는 데이터가 찍히지 않는다.
전에도 비슷한 일이 있었어서 바로 해결방안을 생각해낼 수 있었음.
Future getAllUser() async {
var res = await dio.get(url);
var data = List<Map<String, dynamic>>.from(res.data);
users = data.map((e) => User.fromMap(e)).toList();
setState(() {});
}
initState에서 setState해도 소용없고, getAllUser 함수 내에서 setState()해야한다.
FadeInRight(
// delay: Duration(milliseconds: index * 100), not work!
from: index * 15,
child: ContactTile(
imageUrl: '$imageUrl/${users[index].id}.jpg',
user: users[index],
),
);