[flutter] day14 과제일지: 직렬화, class, animate_do

KoEunseo·2023년 10월 12일
0

flutter

목록 보기
22/45

과제2(연락처 앱)

요구사항

  1. 다음의 공개된 API를 분석하고, 클래스를 활용하여 적용 후
    연락처를 보여주는 앱을 다음과 같이 만드시오.

    - https://jsonplaceholder.typicode.com/users
  • 반드시 Profile 클래스를 만들고 Serialization을 진행할 수 있도록 하시오.
  • 각 사람별 이미지를 CircleAvatar를 통해 보여주도록 한다.
  • 애니메이션 효과를 적절히 사용하여 최대한 위 결과물과 비슷하도록 만드시오.
  • 네트워크에 통신하여 데이터를 가져오는 것은 첫 페이지(리스트 보여주는 페이지)에만 할 수 있도록 한다.

user_page.dart

서클아바타를 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),
          ],
        ),
      ),
    );
  }
}

after

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),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

시연화면

tip: map으로 데이터 가공하기

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));
  };
}

과제3 사전 앱

요구사항

  1. 다음의 공개된 API를 분석하고, 클래스를 활용하여 적용 후
    딕셔너리 앱을 다음과 같이 만드시오.

    - https://api.dictionaryapi.dev/api/v2/entries/en/{검색어}
    - 반드시 Dict 클래스를 만들고 Serialization을 진행할 수 있도록 하시오.
    - 필요한 요소만을 클래스에 적용하는 것은 허용되지만,
    최대한 많은 데이터를 가져올 수 있도록 한다.
    - 이 때, 만약 검색어가 존재하지 않는 단어로 서버에서 정상적인 응답을 못받았을 경우는 아무 것도 출력되지 않도록 한다.
    - 검색어를 입력하고 엔터를 누르면 (TextField의 onSubmitted) 주어진 API를 통해 검색하도록 한다.
    - 이 때, 결과는 아래에 커스텀 위젯을 최대한 활용하여 보여줄 수 있도록한다.
    - 커스텀 위젯은 최대한 분할되어 있을수록 좋다.
    - 예) MeaningCard..
    - 다음의 제공되는 코드를 사용할 수 있다.

model

Dict 외 class

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> 이런식으로 했는데, 타입 에러가 나서 수정했다.

main_page.dart

직렬화하기

  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을 써서 돌려준다.

웬걸 setState를 해도 바로바로 화면이 바뀌지 않았다.

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();
              },
            ),
          ],
        ),
      ),
    );
  }
}

meaining_card.dart

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();
            },
          ),
        ],
      ),
    );
  }
}

definition_tile.dart

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;

vscode 익스텐션이 만들어준 fromMap

  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>)),
    );
  }

강의에서 만든 fromMap

  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를 사용해서 첫번째 객체만 불러왔다.

  • 앗 setState를 get하는 함수 내에서 실행시키지 않아서 문제가 됐었던 것 같다!!

스크롤

  1. ListView를 중복으로 사용함.
    리스트뷰가 스크롤할 수 없도록 지정함.
  2. 메인페이지에서 DictCard를 Expanded, SingleChildScrollView로 감쌈

404에러일때

try-catch 사용
dict=null, setState해줌

Stack 짤리는 문제-clipBehavior: Clip.none

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,),
      )
    )
  ]
)

애니메이션 추가하는 것을 까먹었다...

마저 하기 위해 돌아왔당ㅎ
뭐 별거 아니지만...

error: 근데 켜보니까 처음에 흰 화면만 나오고 컨텐츠가 나오지 않는 것을 발견했다.

데이터를 찍어보니 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()해야한다.

다시 애니메이션으로 돌아가서,

  1. animate_do 설치
  2. 각 ContactTile이 순차적으로 들어올 수 있도록 애니메이션 적용
    딜레이를 주었는데 어쩐일인지 생각대로 안돼서 from을 주었다.
    다시 보니 밀리세컨즈가 아니라 마이크로세컨즈라서 안된 거였다.
FadeInRight(
// delay: Duration(milliseconds: index * 100), not work!
  from: index * 15,
  child: ContactTile(
  imageUrl: '$imageUrl/${users[index].id}.jpg',
  user: users[index],
  ),
);
profile
주니어 플러터 개발자의 고군분투기

0개의 댓글