As a programmer who can cook, it is necessary to bring food to my girlfriend and myself every day, but what to eat every day is a problem of a century!

I thought about developing an APP to randomly decide what to eat tomorrow, but the most painful thing in the world is:

I’m an Android developer and my girlfriend has an iPhone! Is this the furthest distance in the world? !

Just at this moment, Flutter comes, with dazzling light and coquely words: come! On me!

Is this ™ not good or a man?

The APP shows

Basically, the APP was developed in a whole day, followed by a series of requirements adjustment. Let’s take a look at the picture first:

Food show

Simply put a few 🤣

Determine the requirements

As you can see from the above, there are four functions:

  1. Pick dishes at random, and you can pick one of them individually
  2. Confirm and save the screenshot to your phone
  3. View all recipes and when they were used
  4. Add new recipes

There’s another feature that’s missing, and it’s actually quite important:

No repeat dishes for seven days.

Code implementation

Let’s look at each function, first look at the home page randomly selected dishes.

Random menu selection function

The page seems to be simple, wrapped in a Column and OK, but actually?

First determine our requirements, the function is a random selection of dishes, the logic is as follows:

  1. Define the data, then click on the menu
  2. Meat dishes and vegetarian dishes are all random with random effects

Define the data

The data is for all dishes that a person can cook, and they are classified as meat or vegetarian.

After defining the data, save it with SharedPreferences, considering that there will be the ability to add new dishes later.

Every time you open your APP, check whether there is a cache. If there is a cache, use it. If there is no cache, save it.

Random selection of dishes with random effects

We also need to consider this function, as can be seen from the above image, it will random dishes several times, and then refresh the page,

We can’t use setState() at this point, because setState() builds our page multiple times, which is not elegant.

BLoC mode

So I decided to use BLoC mode because I didn’t need to use it on other pages, so I defined a local:

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 ? "No dishes available." : meatData[_random.nextInt(meatData.length)].name}+${greenStuffData.length == 0 ? "No dishes available." : 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 ? "No dishes available." : 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 ? "No dishes available." : greenStuffData[_random.nextInt(greenStuffData.length)].name}"; }).then((s) { _greenController.sink.add(s); }); } } dispose() { _meatController.close(); _greenController.close(); }}Copy the code

First, we define two StreamControllers, one vegetarian and one meat, for the purpose of refreshing a single data.

Then this is a list of the random food method, through the Future, of for a 50 milliseconds of delay after the return of meat and vegetarian dishes random results, and then method invokes the streamController. Sink. Add to notify stream refreshes.

UI usage is as follows:

StreamBuilder(
  stream: _bLoC.greenStream,
  initialData: "Pick a dish.",
  builder: (context, snapshot) {
    _greenName = snapshot.data;
    return Text(
      _greenName,
      style: TextStyle(fontSize: 34, color: Colors.black87), ); },),Copy the code

This completes our above requirement, changing the dish name every 50 milliseconds to achieve a random effect.

Confirm and save the screenshot to your phone

This requirement was put forward by my girlfriend later, because every time I confirm to use it, I need to manually save the picture and then share it with me on wechat, so I added this function.

So you don’t have to manually save the image every time.

This feature has the following three small points:

  1. How to Save screenshots
  2. Show screenshots
  3. Save the screenshot to your mobile phone

How to Save screenshots

First of all, how to save screenshots. I also searched online for this function.

The address is fengy-flutter learning —- screen capture and Gaussian blur

Here I also briefly say, you can see the article:

The screenshots of the widget captured by Flutter are RepaintBoundary with the following code:

return RepaintBoundary(
  key: rootWidgetKey,
  child: Scaffold(),
);
Copy the code

The Scaffold is coated with a RepaintBoundary and given a globalKey, the screenshot can be taken:

The code is written by FengY
// Take a screenshot of boundary and return the binary data of the image.
Future<Uint8List> _capturePng() async {
  RenderRepaintBoundary boundary = globalKey.currentContext.findRenderObject();
  ui.Image image = await boundary.toImage();
  // Note: PNG is a compressed format. If you need raw pixel data for the image, use rawRgba
  ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
  Uint8List pngBytes = byteData.buffer.asUint8List();
  return pngBytes;
}
Copy the code

This method returns a Future

object, which can then be displayed using the image.memory method.

Show screenshots

As can be seen from GIF, after the screenshot, a small chrysanthemum will be displayed first, and then the current image will pop up, which will disappear after a while. Here, showDialog and FutureBuilder are used.

There is no reason not to use FutureBuilder because screenshots have a delay and return a Future. If you are not familiar with FutureBuilder, please check out my article on Flutter FutureBuilder

The approximate code is as follows:

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), ); }}); });Copy the code

Save the screenshot to your mobile phone

This functionality uses the image_gallery_saver library, which is implemented by calling native methods. Since you want to save the picture, you must add read and write permission to the picture on your phone.

It’s also easy to use in one line of code:

_saveImage(Uint8List img) async {
  await ImageGallerySaver.save(img);
}
Copy the code

No repeat dishes for seven days

This function is also added as a follow-up, because after all, no one wants to repeat ordering on the software every day: I had braised pork in brown sauce yesterday, still eat today?

There are a few snags with this feature:

  1. SharedPreferencesUnable to store objects
  2. How can you tell if seven days have passed?

SharedPreferences cannot store objects

Initially, we only store the name of the dish, not whether the dish is already in use, so we need to define an object to store data,

SharedPreferences can’t store objects, so you have no choice but to convert json:

class Food { String name; String time; bool isUsed; Food(this.name, {this.time, // confirm the time to eat, for 7 days automatic expiration 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']; }}Copy the code

Since this is a small project, we use jsonDecode/jsonEncode directly. When using this method, we must define fromJson/toJson, otherwise we will get an error.

How can you tell if seven days have passed

Dart has a DateTime class with a number of methods.

The logic to determine if seven days have passed is: get the current date, get the use date of the stored dish, subtract whether greater than 6

So we can determine when we initialize the dish, loop all the dishes, if the dish is already in use, then determine:

_meatData.forEach((f) {
  if (f.isUsed) {
    if (timeNow.difference(DateTime.parse(f.time)).inDays > 6) {
      f.time = null;
      f.isUsed = false; }}});Copy the code

First check whether the item has been used. If it has been used, use the datetime. difference method to determine the difference between the two dates.

So you can tell if it’s already been used.

View all recipes and when they were used

The function is mainly used for loading force, others a look: God, will do so many dishes, niubi 🐂🍺.

There are a few caveats to this feature:

  1. How to display vegetarian and meat dishes
  2. How to update used/added dishes in real time?

How to display vegetarian and meat dishes

Here, I’ve chosen ExpansionPanelList, which would be perfect for implementation.

If you haven’t covered ExpansionPanelList yet, I recommend reading my article on Flutter ExpansionPanelList

The rest is simple, using the data to determine whether or not to show the used logo and how long it was used.

The simple code is as follows:

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));Copy the code

How to update used/added dishes in real time?

This functionality requires what we call state management, and HERE I’m using Scoped_Model.

This function can be used on both the home page and this page. When a dish is already used, all dishes should be updated in real time, as well as when new dishes are added.

Use the following dish codes:

/// Confirm the use of the food
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('Used successfully and saved to album',
            textStyle: TextStyle(fontSize: 20),
            textPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            position: ToastPosition(align: Alignment.bottomCenter),
            radius: 30,
            backgroundColor: Colors.grey[400]);
  notifyListeners();
}
Copy the code

The code is simple, consisting of two loops of searches followed by notifyListeners().

Add new recipes

I wrote the recipe myself. What if my girlfriend wants something else? New!

The popup here uses the showModalBottomSheet, but anyone who has used that method knows that BottomSheetDialog has a bug that the keyboard popup doesn’t pop up with the layout!

After my unremitting efforts, I finally found the showModalBottomSheetApp rewritten by others on the Internet.

I’m ready to bounce the layout. Then, when I click Save, I call the add recipe method in Scoped_Model.

conclusion

A series of functional optimizations may be carried out for the APP in the future, such as:

  • Write a background storage recipe
  • Add pictures of dishes
  • Optimizing random effects?

If friends have any good effect or demand can find me ah, I will realize see 🌝