[Flutter] 스나이퍼팩토리 Flutter 도전하기 / 주간평가 (5주차)

GONG·2023년 4월 25일
0
post-thumbnail

도전하기

네이버 이메일 클론코딩


https://sfacassignmentchallenge-default-rtdb.europe-west1.firebasedatabase.app/.json

기능 목록

  1. 이메일의 목록을 사진 디자인과 같이 구현합니다.

  2. 이메일이 도착한 날이 오늘이라면 오늘, 어제라면 어제, 올해와 년도가 같다면 MM.dd , 년도가 다르다면 yyyy.MM.dd 로 보여져야합니다.

  3. 각 리스트는 swipe가 가능하며, 우측 스와이프로 삭제를 한다면, 휴지통으로 분류가 되며 현재 화면에는 보여지지 않습니다.

  4. 우측 하단 FAB은 작성하기 버튼이지만, 휴지통 아이콘을 넣어주세요. 클릭하면 휴지통에 추가된 리스트들이 보이는 화면을 구현해주세요.

  5. 우측상단에서 메일검색이 가능하며, 메일검색을 누르면 두번째 사진, 검색하는 화면이 등장하고 최근에 검색한 목록들이 등장합니다.

  6. 메일검색은 보낸사람을 기준으로 검색이 가능하게 합니다.

  7. 우측 상단 시계버튼을 누르면 메시지를 받은 최신순으로 정렬하게되고, 한번 더 누르게되면 오래된 순으로 보여지게 됩니다.

  8. pull to refresh를 한다면 이메일이 새로 업데이트가되며, 삭제한 이메일은 보이지 않습니다.

  9. 최상단 앱바를 클릭하면 자연스럽게 최상단으로 올라가게 됩니다.

코드

  • main.dart
    import 'package:flutter/material.dart';
    
    import 'page/main_page.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      
      Widget build(BuildContext context) {
        return MaterialApp(
          home: MailPage(),
        );
      }
    }
  • Email.dart
    import 'package:intl/intl.dart';
    
    class Email {
      int emailNo;
      String from;
      String title;
      String detail;
      DateTime sendDate;
      bool isDeleted;
    
      Email({
        required this.emailNo,
        required this.from,
        required this.title,
        required this.detail,
        required this.sendDate,
        this.isDeleted = false,
      });
    
      factory Email.fromMap(Map<String, dynamic> map) {
        return Email(
          emailNo: map['emailNo'],
          from: map['from'],
          title: map['title'],
          detail: map['detail'],
          sendDate: DateFormat('yyyy.MM.dd').parse(map['sendDate']),
        );
      }
    
      
      String toString() => 'Mail($from, 삭제($isDeleted))';
    }
  • mail_item.dart
    import 'package:flutter/material.dart';
    import 'package:intl/intl.dart';
    
    import '../model/Email.dart';
    
    class MailItem extends StatefulWidget {
      const MailItem({Key? key, required this.email}) : super(key: key);
    
      final Email email;
    
      
      State<MailItem> createState() => _MailItemState();
    }
    
    class _MailItemState extends State<MailItem> {
    
      var now = DateTime.now();
    
      String dateText (DateTime dateTime) {
        if (now.difference(dateTime).inDays == 0) {
          return '오늘';
        } else if (now.difference(dateTime).inDays == 1) {
          return '어제';
        } else if (now.year == dateTime.year) {
          return DateFormat('MM.dd').format(dateTime);
        }
        return DateFormat('yyyy.MM.dd').format(dateTime);
      }
    
      
      Widget build(BuildContext context) {
    
        String sendDate = dateText(widget.email.sendDate);
    
        return Padding(
          padding: const EdgeInsets.only(top: 8.0, left: 8, right: 8),
          child: Container(
            color: Colors.white,
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 10.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Container(
                        width: 8,
                        child: CircleAvatar(
                          backgroundColor: Colors.green,
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(left: 8.0),
                        child: Text(widget.email.from, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),),
                      ),
                      Spacer(),
                      Padding(
                        padding: const EdgeInsets.only(right: 8.0),
                        child: Text(sendDate),
                      ),
                      Icon(Icons.star, color: Colors.grey,)
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.only(left: 16.0),
                    child: Row(
                      children: [
                        Container(
                          decoration: BoxDecoration(
                            color: Colors.grey,
                            borderRadius: BorderRadius.circular(30),
                          ),
                          child: Padding(
                            padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
                            child: Text('TO', style: TextStyle(color: Colors.white, fontSize: 14),),
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.only(left: 8.0),
                          child: Text(widget.email.title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                            overflow: TextOverflow.ellipsis,
                            maxLines: 1,),
                        ),
                      ],
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 16, right: 8),
                    child: Text(widget.email.detail, style: TextStyle(fontSize: 18, color: Colors.grey),
                      overflow: TextOverflow.ellipsis,
                      maxLines: 1,),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
  • main_page.dart
    import 'dart:developer';
    
    import 'package:dio/dio.dart';
    import 'package:flutter/material.dart';
    import 'package:pull_to_refresh/pull_to_refresh.dart';
    
    import 'bin_page.dart';
    import 'search_page.dart';
    import '../model/Email.dart';
    import '../widget/mail_item.dart';
    
    class MailPage extends StatefulWidget {
      const MailPage({Key? key}) : super(key: key);
    
      
      State<MailPage> createState() => _MailPageState();
    }
    
    class _MailPageState extends State<MailPage> {
      Dio dio = Dio();
      RefreshController refreshController = RefreshController(initialRefresh: false);
      ScrollController scrollController = ScrollController();
    
      List<Email> allEmails = [];        // 모든 메일 리스트
      List<Email> deletedEmails = [];    // 삭제된 메일 리스트
      List<Email> notDeletedEmails = []; // 삭제되지 않은 메일 리스트
      bool isSortAscending = false;
    
      
      void initState() {
        super.initState();
        getData();
      }
    
      // 데이터 가져오기
      void getData() async {
        final res = await dio.get('https://sfacassignmentchallenge-default-rtdb.europe-west1.firebasedatabase.app/.json');
        List<dynamic> emailList = res.data['emails'];
        allEmails = emailList.map((e) => Email.fromMap(e)).toList();
        notDeletedEmails = allEmails;
      }
    
      // 새로고침
      void onRefresh() async {
        setState(() {});
        refreshController.refreshCompleted();
      }
    
      // 시간순으로 정렬
      void sortEmails() {
        setState(() {
          isSortAscending = !isSortAscending;
          notDeletedEmails.sort((a, b) {
            if (isSortAscending) {
              return a.sendDate.compareTo(b.sendDate);
            } else {
              return b.sendDate.compareTo(a.sendDate);
            }
          });
        });
      }
    
      
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.grey[300],
          appBar: AppBar(
            backgroundColor: Colors.white,
            foregroundColor: Colors.black,
            centerTitle: true,
            title: GestureDetector(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('프로모션 '),
                  Container(
                    width: 8,
                    child: CircleAvatar(
                      backgroundColor: Colors.green,
                    ),
                  ),
                  Text(' ${notDeletedEmails.length}', style: TextStyle(color: Colors.green),),
                ],
              ),
              onTap: () {
                scrollController.animateTo(
                  0,
                  duration: Duration(milliseconds: 300),
                  curve: Curves.linear,
                );
              }
            ),
            leading: Icon(Icons.menu),
            actions: [
              IconButton(
                icon: Icon(Icons.access_time),
                onPressed: sortEmails,
              )
            ],
          ),
          body: FutureBuilder(
            future: dio.get('https://sfacassignmentchallenge-default-rtdb.europe-west1.firebasedatabase.app/.json'),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                log('deleted: $deletedEmails');
                log('notDeleted: $notDeletedEmails');
                log('all: $allEmails');
                return SmartRefresher(
                  controller: refreshController,
                  enablePullDown: true,
                  onRefresh: onRefresh,
                  header: WaterDropHeader(),
                  child: ListView.builder(
                    controller: scrollController,
                    itemCount: notDeletedEmails.length + 1,
                    itemBuilder: (context, index) {
                      // 검색
                      if (index == 0) {
                        return Padding(
                          padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
                          child: GestureDetector(
                            child: Container(
                              color: Colors.grey[350],
                              child: Padding(
                                padding: const EdgeInsets.all(8.0),
                                child: Row(
                                  mainAxisAlignment: MainAxisAlignment.center,
                                  children: [
                                    Icon(Icons.search, color: Colors.grey,),
                                    Text('메일 검색', style: TextStyle(color: Colors.grey),)
                                  ],
                                ),
                              )
                            ),
                            onTap: () {
                              Navigator.push(
                                context,
                                MaterialPageRoute(builder: (context) => SearchPage(emailList: notDeletedEmails)),
                              );
                            },
                          ),
                        );
                      }
                      Email email = notDeletedEmails[index-1];
                      print(email);
                      return Dismissible(
                          key: UniqueKey(),
                          background: Container(
                            color: Colors.red,
                            alignment: Alignment.centerRight,
                            padding: const EdgeInsets.all(8.0),
    
                            child: Padding(
                              padding: const EdgeInsets.symmetric(horizontal: 30.0),
                              child: Icon(Icons.delete_outline, color: Colors.white,),
                            ),
                          ),
                          onDismissed: (direction) {
                            email.isDeleted = true;
                            print('${email} : ${email.isDeleted}');
                            deletedEmails.add(email);
                            notDeletedEmails = allEmails.where((email) => !email.isDeleted).toList();
                          },
                        child: MailItem(email: email)
                      );
                    }
                  ),
                );
              } else {
                return Center(child: CircularProgressIndicator(),);
              }
            },
          ),
          floatingActionButton: FloatingActionButton(
            backgroundColor: Colors.green,
            child: Icon(Icons.delete_outline),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => BinPage(deletedEmails: deletedEmails)),
              );
            },
          ),
        );
      }
    }
  • bin_page.dart
    import 'package:first_app/homework/week5/week5challenge/widget/mail_item.dart';
    import 'package:flutter/material.dart';
    
    import '../model/Email.dart';
    
    class BinPage extends StatefulWidget {
      const BinPage({Key? key, required this.deletedEmails}) : super(key: key);
    
      final List<Email> deletedEmails;
    
      
      State<BinPage> createState() => _BinPageState();
    }
    
    class _BinPageState extends State<BinPage> {
      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.white,
            foregroundColor: Colors.black,
            elevation: 0,
            title: Text('휴지통'),
            centerTitle: true,
          ),
          body: ListView.builder(
            itemCount: widget.deletedEmails.length,
            itemBuilder: (context, index) {
              return MailItem(email: widget.deletedEmails[index]);
            }
          ),
        );
      }
    }
  • search_page.dart
    import 'package:first_app/homework/week5/week5challenge/page/search_result_page.dart';
    import 'package:flutter/material.dart';
    
    import '../model/Email.dart';
    
    class SearchPage extends StatefulWidget {
      const SearchPage({Key? key, required this.emailList}) : super(key: key);
    
      final List<Email> emailList;
    
      
      State<SearchPage> createState() => _SearchPageState();
    }
    
    class _SearchPageState extends State<SearchPage> {
      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.white,
            foregroundColor: Colors.black,
            elevation: 0,
            title: Text('메일 검색'),
            // TextField(
            //   decoration: InputDecoration(
            //     fillColor: Colors.grey[200],
            //     filled: true,
            //     border: OutlineInputBorder(
            //       borderSide: BorderSide.none,
            //     ),
            //     hintText: '메일 검색'
            //   ),
            // ),
            // actions: [
            //   TextButton(
            //     onPressed: (){},
            //     child: Text('상세'),
            //   )
            // ],
          ),
          body: ListView.builder(
            itemCount: widget.emailList.length,
            itemBuilder: (context, index) {
              return GestureDetector(
                child: ListTile(
                  title: Text('${widget.emailList[index].from}')
                ),
                onTap: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => SearchResultPage(email: widget.emailList[index]))
                  );
                },
              );
            }
          ),
        );
      }
    }
  • search_result_page.dart
    import 'package:first_app/homework/week5/week5challenge/widget/mail_item.dart';
    import 'package:flutter/material.dart';
    
    import '../model/Email.dart';
    
    class SearchResultPage extends StatelessWidget {
      const SearchResultPage({Key? key, required this.email}) : super(key: key);
    
      final Email email;
    
      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.white,
            foregroundColor: Colors.black,
            elevation: 0,
            title: Text('검색 결과'),
          ),
          body: Column(
            children: [
              MailItem(email: email)
            ],
          ),
        );
      }
    }

결과 화면


주간평가

문제 1.

  1. 아래는 Nature의 웹사이트를 휴대폰으로 봤을 때의 화면이다.
    어떻게 클래스를 제작할 것인지 고민하고 플러터로 구현하도록 하시오.
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/61789c77-1673-4071-bc75-0dabc9eee19e/Untitled.png)
    
    - 7개 이상의 News Article을 포함하도록 하시오.
    - 이 때 사용되는 뉴스 제목을 포함한 모든 데이터는 다음과 같다.
        
        ```
        Journals adopt AI to spot duplicated images in manuscripts
        /articles/d41586-021-03830-7
        Smriti Mallapaty
        23 Dec 2021
        
        Fatal lab explosion in China highlights wider safety fears
        /articles/d41586-021-03589-x
        Andrew Silver
        22 Dec 2021
        
        Journals adopt AI to spot duplicated images in manuscripts
        /articles/d41586-021-03807-6
        Richard Van Noorden
        21 Dec 2021
        ```
        
        - 3개만 제공되며, 그 외에 본인이 원하는 기사는 다음의 URL 에서 수집할 수 있도록 한다.
            - [https://www.nature.com/nature/articles?type=news](https://www.nature.com/nature/articles?type=news&year=2021)

코드

  • main.dart
    import 'package:flutter/material.dart';
    
    import 'article_page.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      
      Widget build(BuildContext context) {
        return MaterialApp(
          home: ArticlePage()
        );
      }
    }
  • Article.dart
    class Article {
      String title;   // 제목
      String img;     // 이미지
      String url;     // 링크
      String author;  // 작성자
      DateTime createdAt;   // 작성일
      String type;    // 타입
    
      Article({
        required this.title,
        required this.url,
        required this.img,
        required this.author,
        required this.createdAt,
        required this.type});
    
      Article.fromMap(Map<String, dynamic> map)
        : title = map['title'],
          url = 'https://www.nature.com${map['url']}',
          img = map['img'],
          author = map['author'],
          createdAt = map['createdAt'],
          type = map['type'];
    }
  • data.dart
    List<Map<String, dynamic>> articleData = [
      {
        'title': 'Journals adopt AI to spot duplicated images in manuscripts',
        'url': '/articles/d41586-021-03830-7',
        'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03655-4/d41586-021-03655-4_19975876.jpg?as=webp',
        'author': 'Smriti Mallapaty',
        'createdAt': DateTime(2021, 12, 23),
        'type': 'News',
      },
      {
        'title': 'Fatal lab explosion in China highlights wider safety fears',
        'url': '/articles/d41586-021-03589-x',
        'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03830-7/d41586-021-03830-7_19972468.jpg?as=webp',
        'author': 'Andrew Silver',
        'createdAt': DateTime(2021, 12, 22),
        'type': 'News',
      },
      {
        'title': 'Journals adopt AI to spot duplicated images in manuscripts',
        'url': '/articles/d41586-021-03807-6',
        'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03589-x/d41586-021-03589-x_19914054.jpg?as=webp',
        'author': 'Richard Van Noorden',
        'createdAt': DateTime(2021, 12, 21),
        'type': 'News',
      },
      {
        'title': 'Webb telescope blasts off successfully — launching a new era in astronomy',
        'url': '/articles/d41586-021-03655-4',
        'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03807-6/d41586-021-03807-6_19969476.jpg?as=webp',
        'author': 'Alexandra Witze',
        'createdAt': DateTime(2021, 12, 20),
        'type': 'News',
      },
      {
        'title': 'Journals adopt AI to spot duplicated images in manuscripts',
        'url': '/articles/d41586-021-03807-6',
        'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03589-x/d41586-021-03589-x_19914054.jpg?as=webp',
        'author': 'Richard Van Noorden',
        'createdAt': DateTime(2021, 12, 20),
        'type': 'News',
      },
      {
        'title': 'Webb telescope blasts off successfully — launching a new era in astronomy',
        'url': '/articles/d41586-021-03655-4',
        'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03807-6/d41586-021-03807-6_19969476.jpg?as=webp',
        'author': 'Alexandra Witze',
        'createdAt': DateTime(2021, 12, 20),
        'type': 'News',
      },
      {
        'title': 'Journals adopt AI to spot duplicated images in manuscripts',
        'url': '/articles/d41586-021-03830-7',
        'img': 'https://media.springernature.com/w290h158/magazine-assets/d41586-021-03655-4/d41586-021-03655-4_19975876.jpg?as=webp',
        'author': 'Smriti Mallapaty',
        'createdAt': DateTime(2021, 12, 23),
        'type': 'News',
      },
    ];
  • article_page.dart
    import 'data.dart';
    import 'package:flutter/material.dart';
    import 'package:url_launcher/url_launcher_string.dart';
    
    import 'Article.dart';
    import 'article_item.dart';
    
    class ArticlePage extends StatefulWidget {
      const ArticlePage({Key? key}) : super(key: key);
    
      
      State<ArticlePage> createState() => _ArticlePageState();
    }
    
    class _ArticlePageState extends State<ArticlePage> {
      
      Widget build(BuildContext context) {
        List<Article> articleList = articleData.map((e) => Article.fromMap(e)).toList();
        print(articleList.toString());
    
        return Scaffold(
          body: SafeArea(
            child: ListView.separated(
              itemCount: articleList.length,
              itemBuilder: (context, index) {
                var article = articleList[index];
                return GestureDetector(
                  child: ArticleItem(article: article),
                  onTap: () {
                    launchUrlString(article.url);
                  },
                );
              },
              separatorBuilder: (context, index) {
                return Divider();
              },
            ),
          ),
        );
      }
    }
  • article_item.dart
    import 'package:intl/intl.dart';
    
    import 'Article.dart';
    import 'package:flutter/material.dart';
    
    class ArticleItem extends StatelessWidget {
      const ArticleItem({Key? key, required this.article}) : super(key: key);
    
      final Article article;
    
      
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Container(
                width: 240,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                        article.title,
                        style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, decoration: TextDecoration.underline,)
                    ),
                    Padding(
                      padding: const EdgeInsets.symmetric(vertical: 8.0),
                      child: Text(article.author),
                    ),
                    Row(
                      children: [
                        Text(article.type, style: TextStyle(fontWeight: FontWeight.bold),),
                        Text(' | ${DateFormat('dd MMM yyyy').format(article.createdAt)}'),
                      ],
                    )
                  ],
                ),
              ),
              Container(
                  width: 100,
                  child: Image.network(article.img)
              )
            ],
          ),
        );
      }
    }

결과 화면


문제 2.

  1. 다음 JSON으로 받아오는 네트워크 데이터를 이름있는 생성자 (fromMap)을 만드시오.
    이 때, 제공되는 소스코드에서 빈 공간을 채워 다음의 화면을 구성할 수 있도록 하시오.

    - https://sfacassignment23-default-rtdb.europe-west1.firebasedatabase.app/.json
    - 해당 API에는 중복되는 네트워크 데이터가 포함되어있다.
    operator ==를 재정의하고 함수인 dismissDuplicatedData에 내용을 채워서
    중복된 값은 리스트에 출력되지 않게 하시오.
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e6237e69-dc7b-4693-a2ce-df969ce44fbb/Untitled.png)
    
    **lib/model/userdata.dart**
    
    ```dart
    class UserData {
    
    	///이곳 채우기.
    
    }
    ```
    
    **lib/assignment23_page.dart**
    
    ```dart
    class Assignment23 extends StatefulWidget {
    	const Assignment23({super.key});
    	@override
    	State<Assignment23> createState() => _Assignment23State();
    }
    
    class _Assignment23State extends State<Assignment23> {
    
    	Future<Map<String, dynamic>> getJsonData() async {
    
    		///이곳 채우기.
    
    	}
    
    	@override
    	Widget build(BuildContext context) {
    		return Scaffold(
    			appBar: AppBar(title: Text('23일차 과제')),
    			body: Center(
    				child: FutureBuilder(
    					future: getJsonData(),
    					builder: (context, snapshot) {
    						if (snapshot.connectionState == ConnectionState.waiting) {
    							return const CupertinoActivityIndicator();
    						}
    						if (!snapshot.hasData) return const Text("데이터가 없습니다");
    	
    						Map<String, dynamic> data = snapshot.data as Map<String, dynamic>;
    						List<dynamic> users = data['users'];
    						List<dynamic> dismissDuplicatedUsers = _dismissDuplicatedData(users);
    						return ListView.separated(
    							itemBuilder: (context, index) {
    								UserData userData = UserData.fromMap(dismissDuplicatedUsers[index]);
    								return _buildItemWidget(userData);
    							},
    							separatorBuilder: (context, index) {
    								return const Divider();
    							},
    							itemCount: dismissDuplicatedUsers.length,
    						);
    					}
    				)
    			),
    		);
    	}
    
    	Widget _buildItemWidget(UserData userData) {
    		return ListTile(
    			leading: Image.network(userData.imageUrl),
    			title: Text('${userData.firstName} ${userData.lastName}'),
    			subtitle: Text('${userData.phoneNumber}'),
    		);
    	}
    
    	List<dynamic> _dismissDuplicatedData(List<dynamic> data) {
    
    		///이곳 채우기.
    
    	}
    }
    ```

코드

  • main.dart
    import 'package:flutter/material.dart';
    
    import 'assignment23_page.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      
      Widget build(BuildContext context) {
        return MaterialApp(
            home: Assignment23()
        );
      }
    }
  • userdata.dart
    class UserData {
      int userId;
      String firstName;
      String lastName;
      String imageUrl;
      String phoneNumber;
    
      UserData({
        required this.userId,
        required this.firstName,
        required this.lastName,
        required this.imageUrl,
        required this.phoneNumber,
      });
    
      UserData.fromMap(Map<String, dynamic> map)
        : userId = map['userId'],
          firstName = map['firstName'],
          lastName = map['lastName'],
          imageUrl = map['imageUrl'],
          phoneNumber = map['phoneNumber'];
    
      Map<String, dynamic> toMap() => {
        'userId': userId,
        'firstName': firstName,
        'lastName': lastName,
        'imageUrl': imageUrl,
        'phoneNumber': phoneNumber,
      };
    
      
      bool operator ==(Object other) => other is UserData && userId == other.userId;
    }
  • assignment23.dart
    import 'package:dio/dio.dart';
    import 'package:flutter/material.dart';
    import 'userdata.dart';
    
    class Assignment23 extends StatefulWidget {
      const Assignment23({super.key});
      
      State<Assignment23> createState() => _Assignment23State();
    }
    
    class _Assignment23State extends State<Assignment23> {
    
      Future<Map<String, dynamic>> getJsonData() async {
        Dio dio = Dio();
        String url = 'https://sfacassignment23-default-rtdb.europe-west1.firebasedatabase.app/.json';
    
        return await dio.get(url).then((value) => value.data);
      }
    
      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('23일차 과제')),
          body: Center(
              child: FutureBuilder(
                  future: getJsonData(),
                  builder: (context, snapshot) {
                    if (snapshot.connectionState == ConnectionState.waiting) {
                      return const CircularProgressIndicator();
                    }
                    if (!snapshot.hasData) return const Text("데이터가 없습니다");
    
                    Map<String, dynamic> data = snapshot.data as Map<String, dynamic>;
                    List<dynamic> users = data['users'];
                    List<dynamic> dismissDuplicatedUsers = _dismissDuplicatedData(users);
                    return ListView.separated(
                      itemBuilder: (context, index) {
                        UserData userData = UserData.fromMap(dismissDuplicatedUsers[index]);
                        return _buildItemWidget(userData);
                      },
                      separatorBuilder: (context, index) {
                        return const Divider();
                      },
                      itemCount: dismissDuplicatedUsers.length,
                    );
                  }
              )
          ),
        );
      }
    
      Widget _buildItemWidget(UserData userData) {
        return ListTile(
          leading: Image.network(userData.imageUrl),
          title: Text('${userData.firstName} ${userData.lastName}'),
          subtitle: Text('${userData.phoneNumber}'),
        );
      }
    
      List<dynamic> _dismissDuplicatedData(List<dynamic> data) {
        // 전체 UserData 리스트
        List<UserData> userList = data.map((e) => UserData.fromMap(e)).toList();
    
        // 중복 제거 UserData 리스트
        List<UserData> result = [];
    
        for (var user in userList) {
          // result에 user랑 같은 UserData 없으면 result에 user 추가
          if (!result.any((e) => e == user)) {
            result.add(user);
          }
        }
    
        return result.map((e) => e.toMap()).toList();
      }
    }

결과 화면


5주차 끝.........

도전하기 코드가 좀 더러운 것 같습니다..........................................
구현이 덜 된 5번 검색기능은 나중에 시간 날 때 꼭!!! 다시 해결해보겠ㅅ브니다..........

profile
우와재밋다

0개의 댓글