iT邦幫忙

2022 iThome 鐵人賽

DAY 18
0
Mobile Development

Flutter 30: from start to store系列 第 18

Flutter介紹:新增別人寫好的酷炫功能 - flutter package

  • 分享至 

  • xImage
  •  

前面已經透過pubspec.yaml加入http, provider等功能了,我們已經對於新增套件不陌生

今天要來介紹UI相關的套件,先閱讀官方文件,一起嘗試在專案的「月曆頁」中加入月曆套件並且改成我們需要的樣子

好的,那我們就開始吧!


table_calendar套件

  1. 首先將table_calendar加入pubspec.yaml, 並下載套件

    $ flutter pub add table_calendar
    
    $ flutter pub get
    
  2. 在文件中我們看到要引用TableCalendar至少需要三個參數
    因此我們在CalendarPage先使用TableCalendar並輸入必要參數

    class _CalendarPageState extends State<CalendarPage> {
      @override
      Widget build(BuildContext context) {
        DateTime current = DateTime.now();
        return Center(
            child: TableCalendar(
          firstDay: DateTime.utc(current.year - 2, 01, 01),
          lastDay: DateTime.utc(current.year + 2, 12, 31),
          focusedDay: DateTime.now(),
        ));
      }
    }
    

    目標是距離現今前後兩年的每日圖片

  3. 接著加入一些互動的元素:畫面會依照我們選取的日期互動

    class _CalendarPageState extends State<CalendarPage> {
      DateTime _selectedDay = DateTime.now();
      DateTime _focusedDay = DateTime.now();
      @override
      Widget build(BuildContext context) {
        DateTime current = DateTime.now();
        return Center(
            child: TableCalendar(
          firstDay: DateTime.utc(current.year - 2, 01, 01),
          lastDay: DateTime.utc(current.year + 2, 12, 31),
          focusedDay: _focusedDay,
          selectedDayPredicate: (day) {
            return isSameDay(_selectedDay, day);
          },
          onDaySelected: (selectedDay, focusedDay) {
            setState(() {
              _selectedDay = selectedDay; // 選取的日期
              _focusedDay = focusedDay; // 頁面停留的日期
            });
          },
       ));
    }
    
  4. 現在頁面可以依照我們的點選而變化,接著我們要客製化月曆上的日期UI,顯示日期的同時也要顯示一些標示。為此使用此套件的calendarBuilder功能,更多詳細設定見 CalendarBuilders class

      calendarBuilders: CalendarBuilders(
        defaultBuilder: (context, day, focusedDay) =>
            Center(child: Text(day.day.toString())),
      ),
    

    如上例的設定,會讓客製化的UI跟目前的月曆完全一樣

  5. 我們要讓日期稍微偏上,並在下方加入一個小圓點,紅色代表未查看過當日的圖片,綠色代表已查看。我們先將每日的方格組件客製化,挖好空格準備依照資料給予property

    class CalendarDayCell extends StatefulWidget {
      const CalendarDayCell(
          {super.key,
          required this.day,
          this.decoration,
          this.hasDot = false,
          this.dotColor = Colors.red});
    
      final DateTime day;
      final Decoration? decoration;
      final bool hasDot;
      final Color dotColor;
    
      @override
      State<CalendarDayCell> createState() => _CalendarDayCellState();
    }
    
    class _CalendarDayCellState extends State<CalendarDayCell> {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Column(children: [
            Container(
              width: 35.0,
              height: 35.0,
              padding: const EdgeInsets.all(5.0),
              decoration: widget.decoration,
              child: Center(
                child: Text(
                  widget.day.day.toString(),
                  style: const TextStyle(fontSize: 16),
                ),
              ),
            ),
            const SizedBox(
              height: 2,
            ),
            widget.hasDot
                ? Container(
                    width: 5.0,
                    height: 5.0,
                    decoration: BoxDecoration(
                      color: widget.dotColor,
                      shape: BoxShape.circle,
                    ),
                  )
                : Container(),
          ]),
        );
      }
    }
    
  6. 在tableCalendar中的calendarBuilder加入剛才的客製化組件

    calendarBuilders: CalendarBuilders(
        defaultBuilder: (context, day, focusedDay) =>
            Center(child: CalendarDayCell(day: day)),
        selectedBuilder: (context, day, focusedDay) => Center(
            child: CalendarDayCell(
                day: day,
                decoration: BoxDecoration(
                  color: Colors.blue[300],
                  shape: BoxShape.circle,
                ))),
        todayBuilder: (context, day, focusedDay) => CalendarDayCell(
            day: day,
            decoration: BoxDecoration(
              color: Colors.blue[100],
              shape: BoxShape.circle,
            )))));
    

    https://ithelp.ithome.com.tw/upload/images/20221005/201522340OY2H9cI3A.png

  7. 加入長按功能:長按之後要導頁到對應的每日圖片頁

    onDayLongPressed: ((selectedDay, focusedDay) {
      // 長按日期時導向該日的天文圖片頁面
      Navigator.push(context, MaterialPageRoute(builder: (context) {
        return Scaffold(
            appBar: AppBar(
                title: Text(
                    '${selectedDay.year}-${selectedDay.month}-${selectedDay.day}')),
            body: const AstroPicture(
                title: '',
                pictureUrl: '',
                desc: '',
                note: '',
                isFavorite: false));
      }));
    
    
  8. call API 取得整月份資料,再依照長按的日期注入資料

    class CalendarPage extends StatefulWidget {
      const CalendarPage({super.key});
    
      @override
      State<CalendarPage> createState() => _CalendarPageState();
    }
    
    class _CalendarPageState extends State<CalendarPage> {
      DateTime _selectedDay = DateTime.now();
      DateTime _focusedDay = DateTime.now();
      DateTime current = DateTime.now();
      bool _isLoading = false;
    
      final Map<String, ApodData> _dailyApodMap = {};
    
      @override
      void initState() {
        super.initState();
        String firstDayofMonth =
            formatDate(DateTime(current.year, current.month, 1));
        _fetchDailyApodData(firstDayofMonth, formatDate(current));
      }
    
      _fetchDailyApodData(String startDate, String endDate) async {
        if (_dailyApodMap[startDate] != null && _dailyApodMap[endDate] != null) {
          // 已經前後時間的資料,代表當月都抓過了,不必再call api抓一次
          return;
        }
        setState(() {
          _isLoading = true;
        });
        Uri url = Uri.parse(
            '$apodEndpoint?api_key=$apiKey&thumbs=true&start_date=$startDate&end_date=$endDate');
        final response = await http.get(url, headers: {
          'Content-type': 'application/json',
          'Accept': 'application/json',
        });
    
        final parsedResponse = json.decode(response.body) as List<dynamic>;
        List<Map<String, dynamic>> dataList =
            parsedResponse.map((data) => data as Map<String, dynamic>).toList();
        for (var data in dataList) {
          _dailyApodMap[data['date']] = ApodData.fromJson(data);
        }
        setState(() {
          _isLoading = false;
        });
      }
    
      String formatDate(DateTime datetime) {
        return '${datetime.year}-${datetime.month < 10 ? '0${datetime.month}' : datetime.month}-${datetime.day < 10 ? '0${datetime.day}' : datetime.day}';
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
            child: _isLoading
                ? const CircularProgressIndicator()
                : TableCalendar(
                    headerStyle: const HeaderStyle(formatButtonVisible: false),
                    firstDay: DateTime.utc(current.year - 2, 01, 01),
                    lastDay: DateTime.now(),
                    focusedDay: _focusedDay,
                    selectedDayPredicate: (day) {
                      return isSameDay(_selectedDay, day);
                    },
                    onPageChanged: (focusedDay) {
                      setState(() {
                        _focusedDay = focusedDay;
                      });
                      if (focusedDay.month <= current.month) {
                        String lastDayOfMonth = formatDate(
                            DateTime(focusedDay.year, focusedDay.month + 1, 0));
                        _fetchDailyApodData(
                            formatDate(focusedDay),
                            DateTime(focusedDay.year, focusedDay.month + 1, 0)
                                    .isAfter(current)
                                ? formatDate(current)
                                : lastDayOfMonth);
                      }
                    },
                    onDaySelected: (selectedDay, focusedDay) {
                      setState(() {
                        _selectedDay = selectedDay; // 選取的日期
                        _focusedDay = focusedDay; // 頁面停留的日期
                      });
                    },
                    onDayLongPressed: ((selectedDay, focusedDay) {
                      String date = formatDate(focusedDay);
                      // 長按日期時導向該日的天文圖片頁面
                      Navigator.push(context, MaterialPageRoute(builder: (context) {
                        return Scaffold(
                            appBar: AppBar(title: Text(formatDate(selectedDay))),
                            body: AstroPicture(
                                title: _dailyApodMap[date]?.title ?? '',
                                pictureUrl: _dailyApodMap[date]?.url ?? '',
                                desc: _dailyApodMap[date]?.desc ?? '',
                                note: '',
                                isFavorite: false));
                      }));
                    }),
                    calendarBuilders: CalendarBuilders(
                        defaultBuilder: (context, day, focusedDay) =>
                            Center(child: CalendarDayCell(day: day)),
                        selectedBuilder: (context, day, focusedDay) => Center(
                            child: CalendarDayCell(
                                day: day,
                                decoration: BoxDecoration(
                                  color: Colors.blue[300],
                                  shape: BoxShape.circle,
                                ))),
                        todayBuilder: (context, day, focusedDay) => CalendarDayCell(
                            day: day,
                            decoration: BoxDecoration(
                              color: Colors.blue[100],
                              shape: BoxShape.circle,
                            )))));
      }
    }
    
  • 長按日期可以導向該日的圖片頁面
    https://ithelp.ithome.com.tw/upload/images/20221005/20152234TrsfVVR3Em.png

Recap

今天示範了如何應用第三方的UI套件,未來如果要做各式各樣的APP,在pub.dev上搜尋一下有沒有別人寫好的套件,說不定可以省下大量的時間,避免重複造輪子。

不過,使用時要先詳閱該套件的license,避免違法和侵權的行為喔!

明天一起來看看如何將寫好的資料存在手機裡吧!


上一篇
Flutter介紹:App狀態管理 - app state management
下一篇
Flutter介紹:把資料存進手機 - app database
系列文
Flutter 30: from start to store30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
elvisyip
iT邦新手 5 級 ‧ 2023-07-26 15:59:41

想問一下 在 7. 加入長按功能:長按之後要導頁到對應的每日圖片頁

body: const AstroPicture(
            title: '',
            pictureUrl: '',
            desc: '',
            note: '',
            isFavorite: false));

AstroPicture 不是應該傳入 apodData嗎?

我要留言

立即登入留言