一个会作饭的程序员如何天天给女友带不一样的便当?

做为一个会作饭的程序员,天天给女友和本身带饭是必须的,但是天天要吃什么倒是一个世纪难题!程序员

之前就想过要开发一个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进行一系列的功能优化,好比:

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

若是朋友们有什么好的效果或者需求能够找我呀,我来实现看看🌝

相关文章
相关标签/搜索