一個會作飯的程序員如何天天給女友帶不一樣的便當?

做爲一個會作飯的程序員,天天給女友和本身帶飯是必須的,但是天天要吃什麼倒是一個世紀難題!程序員

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

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

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

這™不上仍是男人?markdown

APP 展現

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

隨機選菜

決定選擇

全部菜品

添加新菜

菜品展現

簡單放幾個🤣異步

肯定需求

從上面能夠看到一共有四個功能:async

  1. 隨機選菜,而且能夠單獨隨機某一個
  2. 確認並保存截圖到手機
  3. 查看全部菜譜和菜譜使用的時間
  4. 添加新的菜譜

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

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

代碼實現

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

隨機選菜功能

隨機選菜

頁面看似很簡單,一個 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進行一系列的功能優化,好比:

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

若是朋友們有什麼好的效果或者需求能夠找我呀,我來實現看看🌝

相關文章
相關標籤/搜索