avatarsimbu

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

6596

Abstract

, <span class="hljs-string">"olive oil"</span> ], <span class="hljs-string">"thumbnail"</span>: <span class="hljs-string">"https://serpapi.com/searches/61c3369cc99903747c1e643b/images/bd928f9ef521c02bbdb2df96157011d6a02d619d06f8a6e0e62bb157f48e10e5.jpeg"</span> }, { <span class="hljs-string">"title"</span>: <span class="hljs-string">"Balsamic roast potatoes"</span>, <span class="hljs-string">"link"</span>: <span class="hljs-string">"https://www.taste.com.au/recipes/balsamic-roast-potatoes/1bcb6bd4-2efa-4f01-bc98-f2ce32eaa906"</span>, <span class="hljs-string">"source"</span>: <span class="hljs-string">"Taste"</span>, <span class="hljs-string">"rating"</span>: 4, <span class="hljs-string">"reviews"</span>: 2, <span class="hljs-string">"total_time"</span>: <span class="hljs-string">"1 hr 5 mins"</span>, <span class="hljs-string">"ingredients"</span>: [ <span class="hljs-string">"Balsamic vinegar"</span>, <span class="hljs-string">"kipfler potatoes"</span>, <span class="hljs-string">"olive oil"</span>, <span class="hljs-string">"garlic"</span> ], <span class="hljs-string">"thumbnail"</span>: <span class="hljs-string">"https://serpapi.com/searches/61c3369cc99903747c1e643b/images/bd928f9ef521c02bbdb2df96157011d625322c9b727d95f76706f0471bbaba31.jpeg"</span> }, { <span class="hljs-string">"title"</span>: <span class="hljs-string">"Balsamic roast potatoes"</span>, <span class="hljs-string">"link"</span>: <span class="hljs-string">"https://www.delicious.com.au/recipes/balsamic-roast-potatoes/1e2f30ad-d9b4-41bf-aa2e-16f1e2accfb7"</span>, <span class="hljs-string">"source"</span>: <span class="hljs-string">"Delicious"</span>, <span class="hljs-string">"rating"</span>: 5, <span class="hljs-string">"reviews"</span>: 1, <span class="hljs-string">"total_time"</span>: <span class="hljs-string">"1 hr 5 mins"</span>, <span class="hljs-string">"ingredients"</span>: [ <span class="hljs-string">"Balsamic vinegar"</span>, <span class="hljs-string">"kipfler potatoes"</span>, <span class="hljs-string">"olive oil"</span>, <span class="hljs-string">"garlic"</span> ], <span class="hljs-string">"thumbnail"</span>: <span class="hljs-string">"https://serpapi.com/searches/61c3369cc99903747c1e643b/images/bd928f9ef521c02bbdb2df96157011d6ebca3a2868589cb9feab32215a0ba5e8.jpeg"</span> } ] }</pre></div><p id="eee5">Added a list to return the results from the search.</p><figure id="14b0"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*HbqcQhRjCtV0ly9Jca16-w.png"><figcaption>The results from a recipe search for roast potatoes with balsamic vinegar</figcaption></figure><p id="460d">When a result is selected (onTap) we add it to our recipe digest.</p><h2 id="3a70">Ta Da</h2><figure id="3bab"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*S-c5ODW6iv4zZ7RpSAijaA.png"><figcaption>Shows the recipe digest after the new recipe is added.</figcaption></figure><h2 id="02fe">Back Burner</h2><h2 id="04c5">Saving the digest.</h2><p id="7c0c">For now we save the added recipe in memory, later we can persist it locally and remotely to our data stores.</p><h2 id="aba4">iOS Only — Click tab to reset</h2><p id="ba3a">Adding functionality to navigate back to the top level/state when tapping on the current tab item, this is a good iOS experience, a design that people expect.</p><h2 id="c94f">Animated Search Bar</h2><p id="3b5a">I decided to avoid implementing a more advance search bar similar to the one in Apple’s “App Store” application that has animation on focus.</p><figure id="9781"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*VctXKgaDC36mhA7FG1tq9Q.png"><figcaption>An example of a search bar with animation on focus used by Apple’s ‘App Store” app.</figcaption></figure><p id="365f">We can look at this and attempt to improve the search bar later on once the basics are in shape.</p><h2 id="bdc2">CupertinoSearchTextField</h2><p id="685d">I’m getting some library level errors from Flutter asking me to report a framework bug when hitting enter on the keyboard in the search field using a iOS emulator.</p><p id="5d38">For now I’ve abstracted it into a widget and intend to replace it with a custom version with more features.</p><p id="bd7a">I’ve also noticed that you need to enter twice on the keyboard in the emulator before the ‘OnTap’ event fires.</p><h2 id="0a20">XP</h2><h2 id="5103">In memory data</h2><p id="0bfc">We need to store and maintain a list of recipes to allow us to add new recipes.</p><p id="c667">At this point are using a memory repository class to manage this and injecting it as a ChangeNotifierProvider.</p><div id="0294"><pre>ChangeNotifierProvider<MemoryRepository>( create: <span class="hljs-function"><span class="hljs-params">(_)</span> =></span> MemoryRepository(), lazy: <span class="hljs-literal">false</span>, ),</pre></div><p id="98a1">It sounds complicated but it’s just a list of recipes in memory and some methods to add and retrieve them.</p><div id="02fb"><pre>List<Recipe> <span class="hljs-built_in">findAllRecipes</span>(); Recipe <span class="hljs-built_in">addRecipe</span>(Recipe recipe);</pre></div><p id="85eb">See the Flutter <a href="https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple">documentation</a> for more information on state management.</p><p id="53af">When we move to a data store e.g. a DB via an API we can improve this code.</p><h2 id="111f">List Item Reuse</h2><p id="e4b9">As part of displaying the search results we need to display a list again, so I abstracted the initial digest list item so we could reuse it and ensure our list items have the same look and feel across the app.</p><figure id="6e5a"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*XqpjA107JyVpNDZ0fmT-0w.png"><figcaption>Test results for the list item widget</figcaption></figure><figure id="1d2a"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*GDyE_RxWMdu-mO-wyLeBnA.png"><figcaption>Digest list item</figcaption></figure><div id="94f3"><pre>Widget <span class="hljs-title function_ invoke__">recipesList</span>() { <span class="hljs-keyword">final</span> repository = Provider.of<MemoryRepository>(context); <span class="hljs-keyword">var</span> recipes = repository.<span class="hljs-title function

Options

_ invoke__">findAllRecipes</span>();

<span class="hljs-keyword">return</span> ListView.<span class="hljs-title function_ invoke__">builder</span>( <span class="hljs-attr">itemCount</span>: recipes.length, <span class="hljs-attr">itemBuilder</span>: (context, index) { <span class="hljs-keyword">var</span> listItemInfo = <span class="hljs-title function_ invoke__">ListItemInfo</span>( <span class="hljs-attr">id</span>: recipes[index].link, <span class="hljs-attr">author</span>: recipes[index].source, <span class="hljs-attr">title</span>: recipes[index].title, <span class="hljs-attr">titleImage</span>: Image.<span class="hljs-title function_ invoke__">network</span>(recipes[index].thumbnail), <span class="hljs-attr">showAddIcon</span>: <span class="hljs-literal">false</span>, );

  <span class="hljs-keyword">return</span> <span class="hljs-title function_ invoke__">ListItem</span>(listItemInfo);
},

); }</pre></div><figure id="446d"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*9uWraUbFzVfpmvkXmuSrXQ.png"><figcaption>Recipe search result list item</figcaption></figure><div id="9899"><pre>Widget <span class="hljs-title function_ invoke__">recipeSearchResultsList</span>() { <span class="hljs-keyword">var</span> searchResultRecipes = _searchResults.data.length > <span class="hljs-number">0</span> ? Recipe.<span class="hljs-title function_ invoke__">fromSerpApiResultJson</span>(_searchResults.data) : <Recipe>[];

<span class="hljs-keyword">final</span> repository = Provider.of<MemoryRepository>(context);

<span class="hljs-keyword">return</span> ListView.<span class="hljs-title function_ invoke__">builder</span>( <span class="hljs-attr">itemCount</span>: searchResultRecipes.length, <span class="hljs-attr">itemBuilder</span>: (context, index) { <span class="hljs-keyword">var</span> listItemInfo = <span class="hljs-title function_ invoke__">ListItemInfo</span>( <span class="hljs-attr">id</span>: searchResultRecipes[index].link, <span class="hljs-attr">author</span>: searchResultRecipes[index].source, <span class="hljs-attr">title</span>: searchResultRecipes[index].title, <span class="hljs-attr">titleImage</span>: Image.<span class="hljs-title function_ invoke__">network</span>(searchResultRecipes[index].thumbnail), <span class="hljs-attr">showAddIcon</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">onTap</span>: () => { repository.<span class="hljs-title function_ invoke__">addRecipe</span>(searchResultRecipes[index]), Navigator.<span class="hljs-title function_ invoke__">pop</span>(context) }, );

  <span class="hljs-keyword">return</span> <span class="hljs-title function_ invoke__">ListItem</span>(listItemInfo);
},

); }</pre></div><h2 id="755d">Scope Creep (Beware — YUTNI Your unlikely to need it)</h2><p id="c29f">For me this was the most tempting time to try and over engineer the project as it is still missing a number of key things.</p><p id="ec8a">I wanted to do more work on:</p><ul><li>data access, including batched api requests</li><li>design much better widgets with nice animation</li><li>add a theme and a flavour</li><li>logging</li><li>Automated acceptance testing.</li><li>Improve the design for Android</li><li>Look at how it would work as website</li><li>bring in some state management libraries</li><li>etc..</li></ul><p id="5bf3">But experience is telling me to resist the urge to get side tracked, even some initial research on google, has slowed this post down considerably.</p><p id="52e6">In the end I went back to the demo mentality of get it working, balanced with advice from Uncle Bob, that once it’s working that’s only 50%, you now need to get it right.</p><p id="c3fa">That equated to some more time extracting methods and tidying up.</p><p id="83cc">As long as we give each class, method a single responsibility in its own file, we can easily improve them independently later on, as we move from concept to production ready.</p><p id="c5b3">Often the scope creep comes in when I get bored with the discipline of the task at hand as disciplines feel slow.</p><h2 id="44f6">Links</h2><ul><li><a href="https://serpapi.com/recipes-results">SerpApi Recipe results</a></li></ul><figure id="a2e3"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*VBdU52sh43j3EaHTPrq9Tw.png"><figcaption>SerpAPI Recipe results</figcaption></figure><figure id="83f3"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*wvQc1YxoP1Rwi1rRG1yrCg.jpeg"><figcaption>Serp API Setup</figcaption></figure><ul><li><a href="https://readmedium.com/add-a-tab-bar-and-navigation-bar-with-ios-style-in-your-next-flutter-app-bf97b1e27e3a">Add a Tab Bar and Navigation Bar with iOS style in your next Flutter app</a></li><li><a href="https://www.youtube.com/watch?v=9Z_yzIuqxjo">Add elevation to any widget</a></li></ul><p id="62b2">A big thanks to <a href="https://medium.com/@info_4766?source=post_page-----bf97b1e27e3a-----------------------------------">M4trix Dev</a> as his article on the iOS Tab Bar was invaluable and allowed me to quickly fix the problems I had with my first implementation.</p><h2 id="0723">People</h2><p id="d63f">“The thing is, it’s very easy to be different, but very difficult to be better.”</p><p id="6269"><b>Jony Ive</b></p><figure id="f813"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*2PkTNmcQTqADwwgMFAi8ng.png"><figcaption>Picture of Jony Ive</figcaption></figure><h2 id="d62e">Sound & Vision</h2><figure id="6589"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*XwUqmw4GfFyuhUOj8rbqRQ.png"><figcaption>The Bonny — Gerry Cinnamon</figcaption></figure><figure id="0fd4"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*koB16ALOMpgyvC6kMt6w4Q.png"><figcaption>Summary of BBC drama Around the world in 80 days</figcaption></figure><figure id="6eb2"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*dr3RB8tMsT3l6BBLpcRxww.png"><figcaption>Title image from BBC drama around the world in 80 days.</figcaption></figure><h2 id="56af">One more thing</h2><p id="796a">“I have looked in the mirror every morning and asked myself: “If today were the last day of my life, would I want to do what I am about to do today?” And whenever the answer has been “No” for too many days in a row, I know I need to change something.”</p><p id="66ff"><b>Steve Jobs</b></p></article></body>

Join Medium to view all my articles.

Flutter for iOS-Adding an Item

Each year we try to make an improvement to our dinner on Christmas day, this year the focus is on better roasties.

Let’s look at how we can find and add a new roast potato recipe to our digest.

The acceptance for this requirement is to be able to:

  • Search for a new recipe
  • Save it to our digest:
  • Title
  • Author
  • Thumbnail image

To get started I added a top navigation bar with an action that allows us to add a recipe.

@override
Widget build(BuildContext context) {
  return CupertinoPageScaffold(
    navigationBar: const CupertinoNavigationBar(
      heroTag: "RecipeTabHeroTag",
      transitionBetweenRoutes: false,
      middle: Text(
        "Recipies",
      ),
      trailing: Icon(CupertinoIcons.add)
    ),
    child: Container(
      color: CupertinoColors.activeGreen,
      child: recipiesList(),
    ),
  );
}
Shows the Cupertino scafold and App bar used to display our recipes

I renamed the tab back to ‘Digests’ now we have a title of ‘Recipes’ in the top navigation bar.

Now for another quick decision we are going to slide up the screen when the add button is clicked and show a search bar to allow us to find new recipes.

Shows the recipe search bar

With that out the way we just need to harness Google search to find some new recipes.

I turned to SerpAPI for this and grabbed some data from a recipe search for roast potatoes with balsamic vinegar.

https://serpapi.com/search.json?q=Roast+potatoes+with+balsamic&location=United+Kingdom&hl=en&gl=uk&google_domain=google.co.uk&api_key=secret_api_key

{
    "search_metadata": {
      "id": "61c3369cc99903747c1e643b",
      "status": "Success",
      "json_endpoint": "https://serpapi.com/searches/bdb5db8a7f4c5593/61c3369cc99903747c1e643b.json",
      "created_at": "2021-12-22 14:30:52 UTC",
      "processed_at": "2021-12-22 14:30:52 UTC",
      "google_url": "https://www.google.co.uk/search?q=Roast+potatoes+with+balsamic&oq=Roast+potatoes+with+balsamic&uule=w+CAIQICIOVW5pdGVkIEtpbmdkb20&hl=en&gl=uk&sourceid=chrome&ie=UTF-8",
      "raw_html_file": "https://serpapi.com/searches/bdb5db8a7f4c5593/61c3369cc99903747c1e643b.html",
      "total_time_taken": 1.99
    },
    "search_parameters": {
      "engine": "google",
      "q": "Roast potatoes with balsamic",
      "location_requested": "United Kingdom",
      "location_used": "United Kingdom",
      "google_domain": "google.co.uk",
      "hl": "en",
      "gl": "uk",
      "device": "desktop"
    },
    "search_information": {
      "organic_results_state": "Results for exact spelling",
      "query_displayed": "Roast potatoes with balsamic",
      "total_results": 12800000,
      "time_taken_displayed": 0.49
    },
    "recipes_results": [
      {
        "title": "Balsamic potatoes",
        "link": "https://www.jamieoliver.com/recipes/potato-recipes/balsamic-potatoes/",
        "source": "Jamie Oliver",
        "total_time": "2 hrs 20 mins",
        "ingredients":
        [
          "Balsamic vinegar",
          "maris piper potatoes",
          "red onions",
          "rocket",
          "olive oil"
        ],
        "thumbnail": "https://serpapi.com/searches/61c3369cc99903747c1e643b/images/bd928f9ef521c02bbdb2df96157011d6a02d619d06f8a6e0e62bb157f48e10e5.jpeg"
      },
      {
        "title": "Balsamic roast potatoes",
        "link": "https://www.taste.com.au/recipes/balsamic-roast-potatoes/1bcb6bd4-2efa-4f01-bc98-f2ce32eaa906",
        "source": "Taste",
        "rating": 4,
        "reviews": 2,
        "total_time": "1 hr 5 mins",
        "ingredients":
        [
          "Balsamic vinegar",
          "kipfler potatoes",
          "olive oil",
          "garlic"
        ],
        "thumbnail": "https://serpapi.com/searches/61c3369cc99903747c1e643b/images/bd928f9ef521c02bbdb2df96157011d625322c9b727d95f76706f0471bbaba31.jpeg"
      },
      {
        "title": "Balsamic roast potatoes",
        "link": "https://www.delicious.com.au/recipes/balsamic-roast-potatoes/1e2f30ad-d9b4-41bf-aa2e-16f1e2accfb7",
        "source": "Delicious",
        "rating": 5,
        "reviews": 1,
        "total_time": "1 hr 5 mins",
        "ingredients":
        [
          "Balsamic vinegar",
          "kipfler potatoes",
          "olive oil",
          "garlic"
        ],
        "thumbnail": "https://serpapi.com/searches/61c3369cc99903747c1e643b/images/bd928f9ef521c02bbdb2df96157011d6ebca3a2868589cb9feab32215a0ba5e8.jpeg"
      }
    ]
}

Added a list to return the results from the search.

The results from a recipe search for roast potatoes with balsamic vinegar

When a result is selected (onTap) we add it to our recipe digest.

Ta Da

Shows the recipe digest after the new recipe is added.

Back Burner

Saving the digest.

For now we save the added recipe in memory, later we can persist it locally and remotely to our data stores.

iOS Only — Click tab to reset

Adding functionality to navigate back to the top level/state when tapping on the current tab item, this is a good iOS experience, a design that people expect.

Animated Search Bar

I decided to avoid implementing a more advance search bar similar to the one in Apple’s “App Store” application that has animation on focus.

An example of a search bar with animation on focus used by Apple’s ‘App Store” app.

We can look at this and attempt to improve the search bar later on once the basics are in shape.

CupertinoSearchTextField

I’m getting some library level errors from Flutter asking me to report a framework bug when hitting enter on the keyboard in the search field using a iOS emulator.

For now I’ve abstracted it into a widget and intend to replace it with a custom version with more features.

I’ve also noticed that you need to enter twice on the keyboard in the emulator before the ‘OnTap’ event fires.

XP

In memory data

We need to store and maintain a list of recipes to allow us to add new recipes.

At this point are using a memory repository class to manage this and injecting it as a ChangeNotifierProvider.

ChangeNotifierProvider<MemoryRepository>(
	create: (_) => MemoryRepository(),
    lazy: false,
),

It sounds complicated but it’s just a list of recipes in memory and some methods to add and retrieve them.

List<Recipe> findAllRecipes();
Recipe addRecipe(Recipe recipe);

See the Flutter documentation for more information on state management.

When we move to a data store e.g. a DB via an API we can improve this code.

List Item Reuse

As part of displaying the search results we need to display a list again, so I abstracted the initial digest list item so we could reuse it and ensure our list items have the same look and feel across the app.

Test results for the list item widget
Digest list item
Widget recipesList() {
  final repository = Provider.of<MemoryRepository>(context);
  var recipes = repository.findAllRecipes();

  return ListView.builder(
    itemCount: recipes.length,
    itemBuilder: (context, index) {
      var listItemInfo = ListItemInfo(
        id: recipes[index].link,
        author: recipes[index].source,
        title: recipes[index].title,
        titleImage: Image.network(recipes[index].thumbnail),
        showAddIcon: false,
      );

      return ListItem(listItemInfo);
    },
  );
}
Recipe search result list item
Widget recipeSearchResultsList() {
  var searchResultRecipes = _searchResults.data.length > 0
    ? Recipe.fromSerpApiResultJson(_searchResults.data)
    : <Recipe>[];

  final repository = Provider.of<MemoryRepository>(context);

  return ListView.builder(
    itemCount: searchResultRecipes.length,
    itemBuilder: (context, index) {
      var listItemInfo = ListItemInfo(
        id: searchResultRecipes[index].link,
        author: searchResultRecipes[index].source,
        title: searchResultRecipes[index].title,
        titleImage: Image.network(searchResultRecipes[index].thumbnail),
        showAddIcon: true,
        onTap: () => {
          repository.addRecipe(searchResultRecipes[index]),
          Navigator.pop(context)
        },
      );

      return ListItem(listItemInfo);
    },
  );
}

Scope Creep (Beware — YUTNI Your unlikely to need it)

For me this was the most tempting time to try and over engineer the project as it is still missing a number of key things.

I wanted to do more work on:

  • data access, including batched api requests
  • design much better widgets with nice animation
  • add a theme and a flavour
  • logging
  • Automated acceptance testing.
  • Improve the design for Android
  • Look at how it would work as website
  • bring in some state management libraries
  • etc..

But experience is telling me to resist the urge to get side tracked, even some initial research on google, has slowed this post down considerably.

In the end I went back to the demo mentality of get it working, balanced with advice from Uncle Bob, that once it’s working that’s only 50%, you now need to get it right.

That equated to some more time extracting methods and tidying up.

As long as we give each class, method a single responsibility in its own file, we can easily improve them independently later on, as we move from concept to production ready.

Often the scope creep comes in when I get bored with the discipline of the task at hand as disciplines feel slow.

Links

SerpAPI Recipe results
Serp API Setup

A big thanks to M4trix Dev as his article on the iOS Tab Bar was invaluable and allowed me to quickly fix the problems I had with my first implementation.

People

“The thing is, it’s very easy to be different, but very difficult to be better.”

Jony Ive_

Picture of Jony Ive

Sound & Vision

The Bonny — Gerry Cinnamon
Summary of BBC drama Around the world in 80 days
Title image from BBC drama around the world in 80 days.

One more thing

“I have looked in the mirror every morning and asked myself: “If today were the last day of my life, would I want to do what I am about to do today?” And whenever the answer has been “No” for too many days in a row, I know I need to change something.”

Steve Jobs_

Programming
Flutter
Dart
Mobile
iOS
Recommended from ReadMedium