作为一个会做饭的程序员,每天给女朋友和自己带饭是必须的,可是每天要吃什么却是一个世纪难题!

以前就想过要开发一个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学习 ---- 屏幕截图和高斯模糊[1]

这里我也简单说一下,具体可以查看该文章:

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进行一系列的功能优化,比如:

•写个后台存储菜谱•增加菜品图片•优化随机效果?

如果朋友们有什么好的效果或者需求可以找我呀,我来实现看看?

References

[1] FengY - Flutter学习 ---- 屏幕截图和高斯模糊: https://juejin.im/post/5b03ea7e51882565bd2594b0

一个会做饭的程序员如何每天给女朋友带不同的便当?相关推荐

  1. 臻好黄金百香果苗做一个有脑子的程序员

    程序员是最理性的一个群人,除非面对电子产品的时. 程序员是一群高智商的群体,唯一的缺点就是发际线总是很难防守. 程序员是一群情商比较低的人群,常常看到程序员仅仅因为对技术的理解不同而大吵起来. 程序员 ...

  2. 争取做一个良性循环的程序员

    争取做一个良性循环的程序员,莫让恶性循环上身. 以下阐述仅仅的是个人的想法和意见!觉得有说的不对的地方您老人家可以随手关掉页面,顺便可以嘀咕一句(太水了,简直就是胡诌)!^_^ 一:需求与概要 一点1 ...

  3. python官方推荐的三本书-一个合格的python程序员,应该从这三本书入手

    pytho官方推荐的三本入门书籍 python是一门新起的老的编程语言,为什么这么说呢?因为在很早就出现了,最初被用于编写自动化脚本,随着版本不断更新,越来越用于独立的大型项目,在17年广泛进入入门的 ...

  4. python好学吗 老程序员-今天面试了一个34岁大龄程序员,有感而发

    原标题:今天面试了一个34岁大龄程序员,有感而发 " 昨天,我面试了一个34岁的大龄程序员--我给人事的建议是P4,结果人事说:那直接让他走吧. " 我一直以为他们在开玩笑! 结果 ...

  5. 用php写一个可以抽取随机数的工具一次只抽四个怎么实现?_面试了一个32岁的程序员,场面一度很尴尬。...

    招人背景 首先说一下朋友的公司招人背景,公司招聘PHP高级岗位,负责公司的B2B项目研发.并发问题的处理和解决.领导给了他两个要求:(接下来的讲述我会以朋友的第一人称来进行) (1)技术比较好 (2) ...

  6. 面试阿里挂了却拿到网易、点我达offer,一个三年经验Java程序员的面试总结

    转载自  面试阿里挂了却拿到网易.点我达offer,一个三年经验Java程序员的面试总结 前言 15年毕业到现在有三年多了,最近去面试了阿里集团(菜鸟网络,蚂蚁金服).网易.滴滴.点我达,最终收到点我 ...

  7. 30分钟,让你成为一个更好的程序员

    我相信激励是非常重要的.这也是为什么我常常把时间管理(这些书激励我不管改进我的时间管理方法)的书和软件开发拿出来看看.我最近刚看完一本 书,"Apprenticeship Patterns: ...

  8. 我是一个来自泰兴的程序员,我喜欢C++

    我们学习了一个感人的故事,我是一个来自泰兴的程序员,我喜欢C++ 还有一条短短地小尾巴,它有一副坚硬的龟壳,腿脚落下了残疾,每天晚上我做家庭作业时,不愿意让同学看到她走路的姿势,婆婆更爱我,离开,一缕 ...

  9. 如何从一个吊丝男成为一个合格的高级程序员

    2019独角兽企业重金招聘Python工程师标准>>> 首先要成为一个合格的高级程序员,那么你必须精通前端和至少一项后端编程语言.你可以选择java,php,python,ruby等 ...

最新文章

  1. android studio 编译报错:download fastutil-7.2.0.jar
  2. 小型Web应用扫描工具Grabber
  3. High Logic MainType 10中文版
  4. 12.figure/subplot多窗口技巧
  5. 笔记-项目配置管理-配置项
  6. MyEclipes+JSP+SSH+MySQL实现一个文章发布系统
  7. Docker JFrog Artifactory 7.27.10 maven私服(搭建篇)
  8. 容器编排技术 -- Kubernetes Deployment
  9. 互联网核心应用(搜索/推荐/广告)算法峰会
  10. 小程序中canvas绘制网络图片
  11. 百度网盘下载提速,推荐3种亲测有效的方法
  12. android编程歌词显示,Android 音乐播放器实现歌词显示
  13. 使用百度网盘链接分享学习资料 防止链接失效
  14. simulink电子节气门控制模型发动机电子节气门控制模型,有说明文档,教程。
  15. 我的CV实习工作总结
  16. 网易涉暴力裁员引众怒 5大争议背后是否违法?
  17. SQL 完整的实体性—联系图
  18. 给zabbix更换nagios图标
  19. GBASE 8s UDR内存管理_04_mi_zalloc
  20. 想加薪怎么和领导谈?学会这四招轻松涨薪

热门文章

  1. 高老师的架构设计_隽语集(BB_0751)
  2. 掷骰子问题--动态规划
  3. 学计算机需要笔记本嘛,大学生入学后,新生有必要买电脑吗?听完学长的建议再做打算...
  4. 关于我18岁这一年的概括加总结
  5. BiFinance币慧交易所推进大中华区布局,新增垂直媒体战略合作
  6. hdoj 悼念512汶川大地震遇难同胞——一定要记住我爱你 2186 (模拟)
  7. 图形库编程学习笔记(一)
  8. 【python】id()函数
  9. CC00053.pbpositions——|HadoopPB级数仓.V11|——PB数仓.v11|核心交易分析|DIM层建表|加载数据|
  10. python 读取Excel文件(包括后缀为.xls与.xlsx)