{"id":306,"date":"2024-07-13T01:31:47","date_gmt":"2024-07-13T05:31:47","guid":{"rendered":"https:\/\/gerardomiranda.dev\/blog\/?p=306"},"modified":"2024-07-13T01:31:48","modified_gmt":"2024-07-13T05:31:48","slug":"creating-a-flutter-widget-part-1-should-be-easy-right","status":"publish","type":"post","link":"https:\/\/gerardomiranda.dev\/blog\/creating-a-flutter-widget-part-1-should-be-easy-right\/","title":{"rendered":"Creating a Flutter widget Part 1: Should be easy\u2026 right?"},"content":{"rendered":"\n<p><strong>Important:<\/strong> <em>Most of this post is written as I am writing the code as a mean to put my thoughts into words, so the style is not very polished.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p><\/p>\n\n\n\n<p>Alright, I have created a <a href=\"https:\/\/pub.dev\/packages\/flutter_dialog_helper\" target=\"_blank\" rel=\"noopener\" title=\"\">simple package<\/a> in the past, a Flutter package, but it was not a Flutter widget. And when I say simple it was REALLY simple. Less than 100 lines of code, just a couple helper methods.<\/p>\n\n\n\n<p>Every time I got some piece of code I thought could be a package, it was almost immediately shutdown because there was already a package published. Until I got an issue with editing a Duration property in an app I have been developing. I did find a package that could solve my problem this time. But it was not exactly what I needed. So this little problem became my excuse to start this little project.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Context<\/h4>\n\n\n\n<p>I had a form that needed to capture a Duration. This is very easily achievable using a couple of TextField&#8217;s but the result wasn&#8217;t that good. The <a href=\"https:\/\/pub.dev\/packages\/duration_picker\" target=\"_blank\" rel=\"noopener\" title=\"\">duration_picker<\/a> was an option, but I thought it would not fit my use case. I wanted something more like the <a href=\"https:\/\/pub.dev\/packages\/flutter_spinbox\" target=\"_blank\" rel=\"noopener\" title=\"\">flutter_spinbox<\/a>. And since that doesn&#8217;t exists, it was my chance to build it, and build it right.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Planning<\/h4>\n\n\n\n<p>I needed to take into account a few considerations. First that I only can dedicate a few hours a week to this project. Meaning the progress could be slow. Second, I don&#8217;t want the project to take forever so delivering incrementally is a must.<\/p>\n\n\n\n<p>This gives me some direction on how to approach the project:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Start small, to support only the most basic use case at first<\/li>\n\n\n\n<li>Think before adding any parameters to the widget. To avoid introducing breaking changes in the future as much as possible.<\/li>\n\n\n\n<li>This is a good opportunity to learn how to properly test in Flutter.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Starting point<\/h4>\n\n\n\n<p>Thinking about starting small, the initial plan is to create a widget that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Consists of a center Text representing the duration, and two icon buttons, to increase and decrease the value respectively.<\/li>\n\n\n\n<li>Receives a Duration as value<\/li>\n\n\n\n<li>Receives the value of the steps to add or subtract when pressing the buttons<\/li>\n\n\n\n<li>Only shows in the format <em>mm:ss<\/em>. So only showing minutes and seconds.<\/li>\n<\/ul>\n\n\n\n<p>Instead of starting from scratch I remembered there was a <a href=\"https:\/\/brickhub.dev\/bricks\/very_good_flutter_package\" target=\"_blank\" rel=\"noopener\" title=\"\">brick<\/a> I could use from our friends at Very Good Ventures. Has some basic CI stuff and templates for GitHub.<\/p>\n\n\n\n<p>Once the <a href=\"https:\/\/github.com\/gerardo-m\/duration_spinbox\" target=\"_blank\" rel=\"noopener\" title=\"\">repo<\/a> is created, the brick executed, added a Text, and 2 Icon Buttons, and a minimal functional example. This is enough for the <a href=\"https:\/\/github.com\/gerardo-m\/duration_spinbox\/commit\/e46a0f2488d44a6881e246a9489394d8b4a10a9f\" target=\"_blank\" rel=\"noopener\" title=\"\">initial commit.<\/a><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Basic functionality<\/h4>\n\n\n\n<p>Next day, started by implementing displaying the duration in a Text widget. Nothing fancy, just the parsing the minutes and the seconds, padding with zero&#8217;s and that&#8217;s it.<\/p>\n\n\n\n<p>Added a <em>StepUnit<\/em> enum to determine how much the duration will be increased or decreased. Then used the step unit and the step value to write the calculation method.<\/p>\n\n\n\n<p>Once <a href=\"https:\/\/github.com\/gerardo-m\/duration_spinbox\/commit\/76d697edcbc3746d08afa0bf03fd582213951d84\" target=\"_blank\" rel=\"noopener\" title=\"\">this was done<\/a>, I thought that was it and it was just a matter of adding some style, adding some test and it was ready. But just noticed something I didn&#8217;t quite like&#8230;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Optimizing already?<\/h4>\n\n\n\n<p>I am not &#8220;optimizing&#8221;, but when I was testing the widget I noticed that I could get negative values if I go low enough, I didn&#8217;t like this as a default behavior, so I decided to change it.<\/p>\n\n\n\n<p>That&#8217;s how I added the min and max values, both with of Duration type<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Dart\" data-shcb-language-slug=\"dart\"><span><code class=\"hljs language-dart\"><span class=\"hljs-keyword\">const<\/span> DurationSpinbox({\n  required <span class=\"hljs-keyword\">this<\/span>.value,\n  <span class=\"hljs-keyword\">this<\/span>.stepUnit = StepUnit.seconds,\n  <span class=\"hljs-keyword\">this<\/span>.stepValue = <span class=\"hljs-number\">1<\/span>,\n  <span class=\"hljs-keyword\">super<\/span>.key,\n  <span class=\"hljs-keyword\">this<\/span>.min = <span class=\"hljs-built_in\">Duration<\/span>.zero,\n  <span class=\"hljs-keyword\">this<\/span>.max,\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dart<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dart<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And of course added the verification to not allow this widget to be built if the max is less than the min<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"Dart\" data-shcb-language-slug=\"dart\"><span><code class=\"hljs language-dart\"><span class=\"hljs-meta\">@override<\/span>\n<span class=\"hljs-keyword\">void<\/span> initState() {\n  <span class=\"hljs-keyword\">super<\/span>.initState();\n  <span class=\"hljs-keyword\">final<\/span> minD = widget.min;\n  <span class=\"hljs-keyword\">final<\/span> maxD = widget.max;\n  <span class=\"hljs-keyword\">if<\/span> (minD != <span class=\"hljs-keyword\">null<\/span> &amp;&amp; maxD != <span class=\"hljs-keyword\">null<\/span>){\n    <span class=\"hljs-keyword\">if<\/span> (maxD &lt; minD) <span class=\"hljs-keyword\">throw<\/span> ArgumentError(<span class=\"hljs-string\">'max is less than min'<\/span>);\n  }\n  _minutes = widget.value.inSeconds ~\/ <span class=\"hljs-number\">60<\/span>;\n  _seconds = widget.value.inSeconds % <span class=\"hljs-number\">60<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dart<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dart<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>After that, I updated the calculations when the duration is changed to fall into the new boundaries.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"Dart\" data-shcb-language-slug=\"dart\"><span><code class=\"hljs language-dart\"><span class=\"hljs-built_in\">Duration<\/span> _getNewDuration(<span class=\"hljs-built_in\">int<\/span> millisDuration){\n  <span class=\"hljs-keyword\">final<\/span> minD = widget.min;\n  <span class=\"hljs-keyword\">final<\/span> maxD = widget.max;\n  <span class=\"hljs-keyword\">var<\/span> newMillisDuration = millisDuration;\n  <span class=\"hljs-keyword\">if<\/span> (minD != <span class=\"hljs-keyword\">null<\/span>){\n    newMillisDuration = max(minD.inMilliseconds, newMillisDuration);\n  }\n  <span class=\"hljs-keyword\">if<\/span> (maxD != <span class=\"hljs-keyword\">null<\/span>){\n    newMillisDuration = min(maxD.inMilliseconds, newMillisDuration);\n  }\n  <span class=\"hljs-keyword\">final<\/span> newDuration = <span class=\"hljs-built_in\">Duration<\/span>(milliseconds: newMillisDuration);\n  <span class=\"hljs-keyword\">return<\/span> newDuration;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Dart<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">dart<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h4 class=\"wp-block-heading\">Wrapping up our first approximation<\/h4>\n\n\n\n<p>Now it was time to add some color to our widget, and make sure the buttons are disabled when one of the limits was reached.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"380\" height=\"107\" src=\"https:\/\/gerardomiranda.dev\/blog\/wp-content\/uploads\/2024\/07\/image.png\" alt=\"\" class=\"wp-image-328\" style=\"width:838px;height:auto\" srcset=\"https:\/\/gerardomiranda.dev\/blog\/wp-content\/uploads\/2024\/07\/image.png 380w, https:\/\/gerardomiranda.dev\/blog\/wp-content\/uploads\/2024\/07\/image-300x84.png 300w\" sizes=\"auto, (max-width: 380px) 100vw, 380px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"375\" height=\"91\" src=\"https:\/\/gerardomiranda.dev\/blog\/wp-content\/uploads\/2024\/07\/image-1.png\" alt=\"\" class=\"wp-image-329\" style=\"width:841px;height:auto\" srcset=\"https:\/\/gerardomiranda.dev\/blog\/wp-content\/uploads\/2024\/07\/image-1.png 375w, https:\/\/gerardomiranda.dev\/blog\/wp-content\/uploads\/2024\/07\/image-1-300x73.png 300w\" sizes=\"auto, (max-width: 375px) 100vw, 375px\" \/><\/figure>\n\n\n\n<p>And with these <a href=\"https:\/\/github.com\/gerardo-m\/duration_spinbox\/commit\/7457e4ac9932e831682f9418dbb3f97deafaa34a\" target=\"_blank\" rel=\"noopener\" title=\"\">changes<\/a>, I&#8217;m kind of satisfied. Next step would be to write some tests and fix anything that was not addressed in this chapter. If you reached this point I encourage you to continue to follow along.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I found a flutter widget that could solve my problem, but it was not exactly what I needed. This little problem became my excuse to start this little project.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[20],"tags":[109],"class_list":["post-306","post","type-post","status-publish","format-standard","hentry","category-experience","tag-flutter-en"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/posts\/306","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/comments?post=306"}],"version-history":[{"count":10,"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/posts\/306\/revisions"}],"predecessor-version":[{"id":331,"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/posts\/306\/revisions\/331"}],"wp:attachment":[{"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/media?parent=306"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/categories?post=306"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/gerardomiranda.dev\/blog\/wp-json\/wp\/v2\/tags?post=306"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}