做爲一個會作飯的程序員,天天給女友和本身帶飯是必須的,但是天天要吃什麼倒是一個世紀難題!程序員
之前就想過要開發一個APP,來隨機決定明天吃什麼菜,然而世界上最痛苦的事情是:json
我是一個 Android 開發崽,而女友用的是 iPhone!這難道就是世界上最遙遠的距離嗎?!緩存
就在這時,Flutter 來了,它帶着耀眼的光芒和風騷的話語:來啊!上我啊!微信
這™不上仍是男人?markdown
APP基本上一個成天就開發完成了,後續進行了一系列的需求調整,先來看圖:dom
簡單放幾個🤣異步
從上面能夠看到一共有四個功能:async
還有一個功能沒有體現出來,其實也是比較重要的功能:佈局
七天以內不能有重複的菜出現。post
咱們逐個功能來看,首先看一下首頁隨機選菜。
頁面看似很簡單,一個 Column 包裹住就 OK,但實際呢?
首先肯定咱們的需求,該功能就是一個隨機選菜的功能,那邏輯以下:
該數據爲我的全部會作的菜品,而且本身分類爲 葷菜 仍是 素菜。
定義好數據後,由於考慮到後續有添加新菜的功能,使用 SharedPreferences
保存起來,
每次打開APP的時候先判斷一下是否有緩存,若是有緩存則用緩存,沒有則存入。
該功能咱們也須要考慮一下,從上圖也能夠看到,會屢次隨機菜品,而後刷新頁面,
那這個時候確定不能用 setState()
,由於 setState()
會屢次 build 咱們的頁面,這樣很不優雅。
因此我決定使用 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毫秒就改變一下菜名,來達到隨機的效果。
該需求是女友後續提出來的,由於每次確認使用後,都須要手動保存圖片,而後微信分享給我,因此添加了這個功能。
這樣就不用每次都手動保存圖片了。
該功能有以下三個小點:
首先說如何保存截圖,關於該功能,我也是網上查找資料所得,
地址爲: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);
}
複製代碼
該功能也是後續添加的,由於畢竟誰也不想天天在軟件上點菜都有重複:我昨天吃紅燒肉了,今天還吃?
該功能也有幾個小難點:
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
方法來判斷兩個日期之間的差。
這樣就能判斷出來是否已經被使用過了。
該功能主要爲裝逼所用,別人一看:臥槽,會作這麼多菜,牛逼🐂🍺。
該功能其實也有幾個須要注意的點:
這裏我選用的是 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進行一系列的功能優化,好比:
若是朋友們有什麼好的效果或者需求能夠找我呀,我來實現看看🌝