一個會做飯的程式設計師如何每天給女朋友帶不同的便當?

Flutter筆記發表於2019-07-04

作為一個會做飯的程式設計師,每天給女朋友和自己帶飯是必須的,可是每天要吃什麼卻是一個世紀難題!

以前就想過要開發一個APP,來隨機決定明天吃什麼菜,然而世界上最痛苦的事情是:

我是一個 Android 開發崽,而女朋友用的是 iPhone!這難道就是世界上最遙遠的距離嗎?!

一個會做飯的程式設計師如何每天給女朋友帶不同的便當?

就在這時,Flutter 來了,它帶著耀眼的光芒和風騷的話語:來啊!上我啊!

這™不上還是男人?

APP 展示

APP基本上一個整天就開發完成了,後續進行了一系列的需求調整,先來看圖:

隨機選菜

決定選擇

所有菜品

新增新菜

菜品展示

一個會做飯的程式設計師如何每天給女朋友帶不同的便當?

一個會做飯的程式設計師如何每天給女朋友帶不同的便當?

一個會做飯的程式設計師如何每天給女朋友帶不同的便當?

簡單放幾個?

確定需求

從上面可以看到一共有四個功能:

  1. 隨機選菜,並且可以單獨隨機某一個
  2. 確認並儲存截圖到手機
  3. 檢視所有菜譜和菜譜使用的時間
  4. 新增新的菜譜

還有一個功能沒有體現出來,其實也是比較重要的功能:

七天之內不能有重複的菜出現。

程式碼實現

我們逐個功能來看,首先看一下首頁隨機選菜。

隨機選菜功能

隨機選菜

頁面看似很簡單,一個 Column 包裹住就 OK,但實際呢?

首先確定我們的需求,該功能就是一個隨機選菜的功能,那邏輯如下:

  1. 先定義資料,然後點選選菜
  2. 葷菜 素菜 全部隨機 並附帶隨機效果

定義資料

該資料為個人所有會做的菜品,並且自己分類為 葷菜 還是 素菜。

一個會做飯的程式設計師如何每天給女朋友帶不同的便當?

定義好資料後,因為考慮到後續有新增新菜的功能,使用 SharedPreferences 儲存起來,

每次開啟APP的時候先判斷一下是否有快取,如果有快取則用快取,沒有則存入。

隨機選菜並附帶隨機效果

該功能我們也需要考慮一下,從上圖也可以看到,會多次隨機菜品,然後重新整理頁面,

那這個時候肯定不能用 setState(),因為 setState() 會多次 build 我們的頁面,這樣很不優雅。

BLoC模式

所以我決定使用 BLoC 模式,因為不需要在其他頁面使用,所以就定義了一個區域性的:

class RandomMenuBLoC {
  StreamController<String> _meatController;
  StreamController<String> _greenController;
  Random _random;

  RandomMenuBLoC() {
    _meatController = StreamController();
    _greenController = StreamController();
    _random = Random();
  }

  Stream<String> get meatStream => _meatController.stream;

  Stream<String> get greenStream => _greenController.stream;

  random(BuildContext context) async {
    var meatData = ScopedModel.of<DishModel>(context).meatData;
    var greenStuffData = ScopedModel.of<DishModel>(context).greenStuffData;
    for (int i = 0; i < 20; i++) {
      await Future.delayed(new Duration(milliseconds: 50), () {
        return "${meatData.length == 0 ? "暫無可用菜品" : meatData[_random.nextInt(meatData.length)].name}+${greenStuffData.length == 0 ? "暫無可用菜品" : greenStuffData[_random.nextInt(greenStuffData.length)].name}";
      }).then((s) {
        _meatController.sink.add(s.substring(0, s.indexOf("+")));
        _greenController.sink.add(s.substring(s.indexOf("+")+1));
      });

    }
  }

  randomMeat(BuildContext context) async{
    var meatData = ScopedModel.of<DishModel>(context).meatData;
    for (int i = 0; i < 20; i++) {
      await Future.delayed(new Duration(milliseconds: 50), () {
        return "${meatData.length == 0 ? "暫無可用菜品" : meatData[_random.nextInt(meatData.length)].name}";
      }).then((s) {
        _meatController.sink.add(s);
      });
    }
  }

  randomGreen(BuildContext context) async{
    var greenStuffData = ScopedModel.of<DishModel>(context).greenStuffData;
    for (int i = 0; i < 20; i++) {
      await Future.delayed(new Duration(milliseconds: 50), () {
        return "${greenStuffData.length == 0 ? "暫無可用菜品" : greenStuffData[_random.nextInt(greenStuffData.length)].name}";
      }).then((s) {
        _greenController.sink.add(s);
      });
    }
  }

  dispose() {
    _meatController.close();
    _greenController.close();
  }
}

複製程式碼

首先因為考慮到會單獨重新整理某一個資料,所以定義了兩個 streamController,一個素菜,一個葷菜。

然後下面就是隨機菜品的方法,通過 Future.delayed來進行一個50毫秒的延時後返回葷菜和素菜隨機的結果,並且在 then 方法中呼叫 streamController.sink.add 來通知 stream 重新整理。

UI使用如下:

StreamBuilder(
  stream: _bLoC.greenStream,
  initialData: "選個菜吧",
  builder: (context, snapshot) {
    _greenName = snapshot.data;
    return Text(
      _greenName,
      style: TextStyle(fontSize: 34, color: Colors.black87),
    );
  },
),
複製程式碼

這樣就完成了我們上圖的需求,每隔50毫秒就改變一下菜名,來達到隨機的效果。

確認並儲存截圖到手機

該需求是女朋友後續提出來的,因為每次確認使用後,都需要手動儲存圖片,然後微信分享給我,所以新增了這個功能。

這樣就不用每次都手動儲存圖片了。

決定選擇

該功能有如下三個小點:

  1. 如何儲存截圖
  2. 顯示截圖
  3. 儲存截圖到手機

如何儲存截圖

首先說如何儲存截圖,關於該功能,我也是網上查詢資料所得,

地址為:FengY - Flutter學習 ---- 螢幕截圖和高斯模糊

這裡我也簡單說一下,具體可以檢視該文章:

Flutter 獲取 widget 的截圖 使用到的是 RepaintBoundary,程式碼如下:

return RepaintBoundary(
  key: rootWidgetKey,
  child: Scaffold(),
);
複製程式碼

通過 RepaintBoundary 包裹住 Scaffold,然後給定一個 globalKey,這樣就可以進行截圖了:

// 程式碼為 FengY 所寫
// 截圖boundary,並且返回圖片的二進位制資料。
Future<Uint8List> _capturePng() async {
  RenderRepaintBoundary boundary = globalKey.currentContext.findRenderObject();
  ui.Image image = await boundary.toImage();
  // 注意:png是壓縮後格式,如果需要圖片的原始畫素資料,請使用rawRgba
  ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
  Uint8List pngBytes = byteData.buffer.asUint8List();
  return pngBytes;
}
複製程式碼

呼叫該方法後,返回的就是一個 Future<Uint8List> 物件了,後續使用 Image.memory 方法即可顯示該圖片。

顯示截圖

從 gif 可以看到,在截圖以後會先顯示一個小菊花,然後彈出當前所截圖片,一會以後會消失,這裡使用的是 showDialog 配合 FutureBuilder

因為截圖會有一定的延時,並且返回值為一個 Future ,那我們沒有理由不用 FutureBuilder,如有不瞭解 FutureBuilder 的,可以檢視我的這篇文章:Flutter FutureBuilder 非同步UI神器

大概程式碼如下:

showDialog(
  context: context,
  builder: (context) {
    return FutureBuilder<Uint8List>(
      future: _future,
      builder: (BuildContext context,
                AsyncSnapshot snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.none:
          case ConnectionState.active:
          case ConnectionState.waiting:
            return Center(
              child: CupertinoActivityIndicator());
          case ConnectionState.done:
            _saveImage(snapshot.data);

            Future.delayed(
              Duration(milliseconds: 1500), () {
                Navigator.of(context,rootNavigator: true).pop();
              });
            return Container(
              margin:
              EdgeInsets.symmetric(vertical: 50),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.all(
                  Radius.circular(18)),
                color: Colors.transparent,
              ),
              child: Image.memory(snapshot.data),
            );
        }
      },
    );
  });
複製程式碼

儲存截圖到手機

該功能使用的是 image_gallery_saver 庫,該庫通過呼叫原生方法來實現。由於要儲存圖片,所以必須要新增手機圖片讀寫許可權。

使用方法也很簡單,一行程式碼就搞定:

_saveImage(Uint8List img) async {
  await ImageGallerySaver.save(img);
}
複製程式碼

七天之內不能出現重複菜品

該功能也是後續新增的,因為畢竟誰也不想每天在軟體上點菜都有重複:我昨天吃紅燒肉了,今天還吃?

該功能也有幾個小難點:

  1. SharedPreferences 不能儲存物件
  2. 如何判斷已經過了七天?

SharedPreferences 不能儲存物件

最開始的時候只是儲存了菜名,並沒有該菜是否已經使用,所以要定義一個物件來儲存資料,

後來發現SharedPreferences 不能儲存物件,那沒辦法,只能轉 json 了:

class Food {
  String name;
  String time;
  bool isUsed;

  Food(
    this.name, {
    this.time, // 確認吃的時間,用於七天自動過期
    this.isUsed = false,
  });

  Map toJson() {
    return {'name': this.name, 'time': this.time, 'isUsed': this.isUsed};
  }

  Food.fromJson(Map<String, dynamic> json) {
    this.name = json['name'];
    this.time = json['time'];
    this.isUsed = json['isUsed'];
  }
}
複製程式碼

由於是個小專案,直接就用的 jsonDecode / jsonEncode,使用該方法的時候必須定義 fromJson / toJson,否則會報錯。

如何判斷已經過了七天

經過查詢資料,發現 dart 中有一個 DateTime 類,該類的方法確實不少。

判斷過了七天的邏輯就是:獲取當前日期,獲取儲存的菜的使用日期,相減是否大於6

那我們在初始化菜的時候就可以判斷,迴圈所有的菜品,如果該菜品已經被使用,那麼則去判斷:

_meatData.forEach((f) {
  if (f.isUsed) {
    if (timeNow.difference(DateTime.parse(f.time)).inDays > 6) {
      f.time = null;
      f.isUsed = false;
    }
  }
});
複製程式碼

首先判斷該菜品是否被使用過,如果已經被使用過,則使用 DateTime.difference 方法來判斷兩個日期之間的差。

這樣就能判斷出來是否已經被使用過了。

檢視所有菜譜和菜譜使用的時間

該功能主要為裝逼所用,別人一看:臥槽,會做這麼多菜,牛逼??。

所有菜品

該功能其實也有幾個需要注意的點:

  1. 如何展示素菜和葷菜
  2. 如何實時更新已經使用過/新增的菜?

如何展示素菜和葷菜

這裡我選用的是 ExpansionPanelList,用它來實現最合適不過。

如果你還沒有了解過 ExpansionPanelList,那麼我建議讀我的這篇文章:Flutter ExpansionPanel 超級實用展開控制元件

剩下的就很簡單了,通過資料來判斷是否展示 已使用標識 和 已使用時間。

簡單程式碼如下:

return Padding(
  child: Row(
    children: <Widget>[
      data.isUsed
      ? Icon(
        Icons.done,
        color: Colors.red,
      )
      : Container(),
      Expanded(
        child: Padding(
          padding:
          const EdgeInsets.symmetric(horizontal: 12.0),
          child: Text(
            data.name,
            style: TextStyle(fontSize: 16),
          ),
        ),
      ),
      data.isUsed
      ? Text(
        data.time.substring(0, data.time.indexOf('.')))
      : Container(),
    ],
  ),
  padding: EdgeInsets.all(20),
);
複製程式碼

如何實時更新已經使用過/新增的菜?

該功能就需要用到我們所說的狀態管理,這裡我使用的是 Scoped_Model

在首頁和該頁都會使用到該功能,當已經使用一個菜的時候,所有菜品裡應實時更新,新增菜品的時候也應如此。

使用菜品程式碼如下:

/// 確認使用該食物
useFood(String greenName, String meatName) {
  var time = DateTime.now();

  for (int i = 0; i < _greenStuffData.length; i++) {
    if (_greenStuffData[i].name == greenName) {
      _greenStuffData[i].isUsed = true;
      _greenStuffData[i].time = time.toString();
      break;
    }
  }

  for (int i = 0; i < _meatData.length; i++) {
    if (_meatData[i].name == meatName) {
      _meatData[i].isUsed = true;
      _meatData[i].time = time.toString();
      break;
    }
  }

  updateData('greenStuffData', _greenStuffData);
  updateData('meatData', _meatData);
  showToast('使用成功並儲存至相簿',
            textStyle: TextStyle(fontSize: 20),
            textPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            position: ToastPosition(align: Alignment.bottomCenter),
            radius: 30,
            backgroundColor: Colors.grey[400]);
  notifyListeners();
}
複製程式碼

程式碼很簡單,就是兩個迴圈查詢,然後 notifyListeners()

新增新的菜譜

菜譜是自己寫的,如果女朋友想吃別的菜怎麼辦?新增啊!

新增新菜

這裡的彈出框使用的是 showModalBottomSheet,但是用過該方法的人都知道 BottomSheetDialog 有個 bug,那就是鍵盤彈出框不能頂起佈局!

經過我不懈努力,終於,在網上找到了別人重寫的 showModalBottomSheetApp

可以順利彈起佈局了。然後在點選儲存時,呼叫 Scoped_Model 中增加菜譜方法。

總結

後續可能會對該APP進行一系列的功能優化,比如:

  • 寫個後臺儲存菜譜
  • 增加菜品圖片
  • 優化隨機效果?

如果朋友們有什麼好的效果或者需求可以找我呀,我來實現看看?

一個會做飯的程式設計師如何每天給女朋友帶不同的便當?

相關文章