一个会做饭的程序员如何每天给女朋友带不同的便当?-程序员宅基地

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

以前就想过要开发一个APP,来随机决定明天吃什么菜,然而世界上最痛苦的事情是:

我是一个 Android 开发崽,而女朋友用的是 iPhone!这难道就是世界上最遥远的距离吗?!

640?wx_fmt=jpeg

就在这时,Flutter 来了,它带着耀眼的光芒和风骚的话语:来啊!上我啊!

这不上还是男人?

APP 展示

APP基本上一个整天就开发完成了,后续进行了一系列的需求调整,先来看图:

640?wx_fmt=gif

640?wx_fmt=gif

640?wx_fmt=gif

640?wx_fmt=gif

菜品展示

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

简单放几个?

确定需求

从上面可以看到一共有四个功能:

1.随机选菜,并且可以单独随机某一个2.确认并保存截图到手机3.查看所有菜谱和菜谱使用的时间4.添加新的菜谱

还有一个功能没有体现出来,其实也是比较重要的功能:

七天之内不能有重复的菜出现。

代码实现

我们逐个功能来看,首先看一下首页随机选菜。

随机选菜功能

640?wx_fmt=gif

页面看似很简单,一个 Column 包裹住就 OK,但实际呢?

首先确定我们的需求,该功能就是一个随机选菜的功能,那逻辑如下:

1.先定义数据,然后点击选菜2.荤菜 素菜 全部随机 并附带随机效果

定义数据

该数据为个人所有会做的菜品,并且自己分类为 荤菜 还是 素菜。

640?wx_fmt=png

定义好数据后,因为考虑到后续有添加新菜的功能,使用 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毫秒就改变一下菜名,来达到随机的效果。

确认并保存截图到手机

该需求是女朋友后续提出来的,因为每次确认使用后,都需要手动保存图片,然后微信分享给我,所以添加了这个功能。

这样就不用每次都手动保存图片了。

640?wx_fmt=gif

该功能有如下三个小点:

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 方法来判断两个日期之间的差。

这样就能判断出来是否已经被使用过了。

查看所有菜谱和菜谱使用的时间

该功能主要为装逼所用,别人一看:卧槽,会做这么多菜,牛逼??。

640?wx_fmt=gif

该功能其实也有几个需要注意的点:

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()

添加新的菜谱

菜谱是自己写的,如果女朋友想吃别的菜怎么办?新增啊!

640?wx_fmt=gif

这里的弹出框使用的是 showModalBottomSheet,但是用过该方法的人都知道 BottomSheetDialog 有个 bug,那就是键盘弹出框不能顶起布局!

经过我不懈努力,终于,在网上找到了别人重写的 showModalBottomSheetApp

可以顺利弹起布局了。然后在点击保存时,调用 Scoped_Model 中增加菜谱方法。

总结

后续可能会对该APP进行一系列的功能优化,比如:

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

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

640?wx_fmt=jpeg

References

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


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_23756803/article/details/94691702

智能推荐

前端开发之vue-grid-layout的使用和实例-程序员宅基地

文章浏览阅读1.1w次,点赞7次,收藏34次。vue-grid-layout的使用、实例、遇到的问题和解决方案_vue-grid-layout

Power Apps-上传附件控件_powerapps点击按钮上传附件-程序员宅基地

文章浏览阅读218次。然后连接一个数据源,就会在下面自动产生一个添加附件的组件。把这个控件复制粘贴到页面里,就可以单独使用来上传了。插入一个“编辑”窗体。_powerapps点击按钮上传附件

C++ 面向对象(Object-Oriented)的特征 & 构造函数& 析构函数_"object(cnofd[\"ofdrender\"])十条"-程序员宅基地

文章浏览阅读264次。(1) Abstraction (抽象)(2) Polymorphism (多态)(3) Inheritance (继承)(4) Encapsulation (封装)_"object(cnofd[\"ofdrender\"])十条"

修改node_modules源码,并保存,使用patch-package打补丁,git提交代码后,所有人可以用到修改后的_修改 node_modules-程序员宅基地

文章浏览阅读133次。删除node_modules,重新npm install看是否成功。在 package.json 文件中的 scripts 中加入。修改你的第三方库的bug等。然后目录会多出一个目录文件。_修改 node_modules

【】kali--password:su的 Authentication failure问题,&sudo passwd root输入密码时Sorry, try again._password: su: authentication failure-程序员宅基地

文章浏览阅读883次。【代码】【】kali--password:su的 Authentication failure问题,&sudo passwd root输入密码时Sorry, try again._password: su: authentication failure

整理5个优秀的微信小程序开源项目_微信小程序开源模板-程序员宅基地

文章浏览阅读1w次,点赞13次,收藏97次。整理5个优秀的微信小程序开源项目。收集了微信小程序开发过程中会使用到的资料、问题以及第三方组件库。_微信小程序开源模板

随便推点

Centos7最简搭建NFS服务器_centos7 搭建nfs server-程序员宅基地

文章浏览阅读128次。Centos7最简搭建NFS服务器_centos7 搭建nfs server

Springboot整合Mybatis-Plus使用总结(mybatis 坑补充)_mybaitis-plus ruledataobjectattributemapper' and '-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏3次。前言mybatis在持久层框架中还是比较火的,一般项目都是基于ssm。虽然mybatis可以直接在xml中通过SQL语句操作数据库,很是灵活。但正其操作都要通过SQL语句进行,就必须写大量的xml文件,很是麻烦。mybatis-plus就很好的解决了这个问题。..._mybaitis-plus ruledataobjectattributemapper' and 'com.picc.rule.management.d

EECE 1080C / Programming for ECESummer 2022 Laboratory 4: Global Functions Practice_eece1080c-程序员宅基地

文章浏览阅读325次。EECE 1080C / Programming for ECESummer 2022Laboratory 4: Global Functions PracticePlagiarism will not be tolerated:Topics covered:function creation and call statements (emphasis on global functions)Objective:To practice program development b_eece1080c

洛谷p4777 【模板】扩展中国剩余定理-程序员宅基地

文章浏览阅读53次。被同机房早就1年前就学过的东西我现在才学,wtcl。设要求的数为\(x\)。设当前处理到第\(k\)个同余式,设\(M = LCM ^ {k - 1} _ {i - 1}\) ,前\(k - 1\)个的通解就是\(x + i * M\)。那么其实第\(k\)个来说,其实就是求一个\(y\)使得\(x + y * M ≡ a_k(mod b_k)\)转化一下就是\(y * M ...

android 退出应用没有走ondestory方法,[Android基础论]为何Activity退出之后,系统没有调用onDestroy方法?...-程序员宅基地

文章浏览阅读1.3k次。首先,问题是如何出现的?晚上复查代码,发现一个activity没有调用自己的ondestroy方法我表示非常的费解,于是我检查了下代码。发现再finish代码之后接了如下代码finish();System.exit(0);//这就是罪魁祸首为什么这样写会出现问题System.exit(0);////看一下函数的原型public static void exit (int code)//Added ..._android 手动杀死app,activity不执行ondestroy

SylixOS快问快答_select函数 导致堆栈溢出 sylixos-程序员宅基地

文章浏览阅读894次。Q: SylixOS 版权是什么形式, 是否分为<开发版税>和<运行时版税>.A: SylixOS 是开源并免费的操作系统, 支持 BSD/GPL 协议(GPL 版本暂未确定). 没有任何的运行时版税. 您可以用她来做任何 您喜欢做的项目. 也可以修改 SylixOS 的源代码, 不需要支付任何费用. 当然笔者希望您可以将使用 SylixOS 开发的项目 (不需要开源)或对 SylixOS 源码的修改及时告知笔者.需要指出: SylixOS 本身仅是笔者用来提升自己水平而开发的_select函数 导致堆栈溢出 sylixos

推荐文章

热门文章

相关标签