<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Pierre Monier on Medium]]></title>
        <description><![CDATA[Stories by Pierre Monier on Medium]]></description>
        <link>https://medium.com/@pmonier?source=rss-de40fb673fd9------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*lUo7DjQ2jYOQn-zTd685wg.jpeg</url>
            <title>Stories by Pierre Monier on Medium</title>
            <link>https://medium.com/@pmonier?source=rss-de40fb673fd9------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 05 Jun 2026 14:30:57 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@pmonier/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Secure Firebase Cloud Function with an API key]]></title>
            <link>https://pmonier.medium.com/secure-firebase-cloud-function-with-an-api-key-37028dee39e0?source=rss-de40fb673fd9------2</link>
            <guid isPermaLink="false">https://medium.com/p/37028dee39e0</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[firebase]]></category>
            <dc:creator><![CDATA[Pierre Monier]]></dc:creator>
            <pubDate>Sun, 09 Apr 2023 12:13:55 GMT</pubDate>
            <atom:updated>2023-04-09T12:18:28.303Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U_V2C_pdq6jNlmP7Zgw1cw.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@growtika?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Growtika</a> on <a href="https://unsplash.com/photos/8zB4P0eafrs?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption></figure><h3>Introduction</h3><p>Firebase Cloud Functions is a serverless compute platform that enables developers to create backend logic in JavaScript or TypeScript. It is built on top of Google Cloud Functions, providing a scalable and flexible backend infrastructure. With Cloud Functions, developers can focus on building features and let Firebase handle the underlying infrastructure, making it an efficient and scalable way to build backend services. Cloud Functions are seamlessly integrated with Firebase, enabling developers to add value to their project directly and quickly deploy code</p><p>Firebase Cloud Functions are built on top of Google Cloud Functions, but they provide a simpler and more streamlined interface, with minimal configuration required. This is because Firebase aims to make it easy for developers to deploy backend logic without worrying about infrastructure management.</p><p>However, there are times when additional configuration is necessary, such as when restricting access to Cloud Functions. By default, Firebase Cloud Functions are public, but it’s possible to restrict access to specific users or entities. In this article, we’ll explore how to protect Cloud Functions with an API key, so that only authorized users with a valid key can call the function.</p><h3>Where should we check that a user have correct authorization ?</h3><p>There are various methods to secure Cloud Functions, and one approach is to check whether the user has the appropriate role within the function. While this can be effective, it may not be the most secure solution. Allowing unauthorized users to enter the function, even if they don’t have permission to call it, could pose a risk. Malicious users could attempt to access sensitive data or execute unauthorized actions.</p><p>To mitigate this risk, it’s best to have a reverse proxy in front of the Cloud Function to filter authorized users. This approach helps to ensure that only authorized users can access the function, and provides an additional layer of security against malicious attacks.</p><p>Instead of doing that :</p><figure><img alt="in code auth check" src="https://cdn-images-1.medium.com/max/1024/0*4VuyvCoR3CLA5i4j" /></figure><p>We want to do that :</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RdSu97vRhzJUaKS-" /></figure><blockquote>This article primarily focuses on securing calls that are not made from your app’s frontend. If you’re looking to secure Firebase resources in this context, consider using the App Check Firebase service.</blockquote><h3>Why use API keys to secure Firebase Cloud Functions?</h3><p>In a reverse proxy, there are multiple techniques available to verify user authorization. One such technique is using an API key, which is relatively easy to implement. However, if we want to enhance security, we can use JWT tokens instead. This approach provides more security because JWT tokens have a limited lifetime, meaning that Cloud Functions can only be called within a specific time range. This approach helps to prevent malicious actors from using a stolen JWT token for an extended period. However, using JWT tokens requires creating and managing them, which can be a bit more complex.</p><p>On the other hand, API key authorization requires fewer steps. However, if an API key is stolen, the malicious actor can use it to call Cloud Functions as long as the key is valid. For this reason, it’s important to use API key authorization for non-critical Cloud Functions with a low level of risk. For sensitive Cloud Functions, it’s better to use an authentication method with a limited lifetime to enhance security.</p><p>Now that we have all the information we need, let’s move on to the implementation.</p><h3>Implementation</h3><h4>Disabled public access</h4><p>I have my Cloud Function, define by the following code :</p><pre>export const securedHelloWorld =<br>    functions.https.onRequest((request, response) =&gt; {<br>        response.send(&quot;You are authorized to receive this Hello from Firebase!&quot;);<br>    });</pre><p>Currently, it is not very secure because anyone can call it with a curl command, for example :</p><pre>curl https://myfirebaseproject.cloudfunctions.net/securedHelloWorld</pre><p>you will receive :</p><pre>“You are authorized to receive this Hello from Firebase!”</pre><p>The first step in securing our Cloud Function is to block public calls. We can do this by going to the GCP interface. When you create a Firebase project, it automatically creates a GCP project. To access your GCP project, the easiest way is to click on “Detailed usage stat.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9mus4hO0qHRhvAjo" /></figure><p>This will open your function details in GCP. Now, we need to remove the “allUsers” group from the Cloud Invoker role. This is what allows anyone to call our function. Essentially, it translates to “Ok GCP, let anyone call this function.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/812/0*bKhmO1YHsUeASJOL" /></figure><p>If we try to call our function again with curl, we will receive the following response:</p><pre>&lt;html&gt;&lt;head&gt;<br>&lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html;charset=utf-8&quot;&gt;<br>&lt;title&gt;403 Forbidden&lt;/title&gt;<br>&lt;/head&gt;<br>&lt;body text=#000000 bgcolor=#ffffff&gt;<br>&lt;h1&gt;Error: Forbidden&lt;/h1&gt;<br>&lt;h2&gt;Your client does not have permission to get URL &lt;code&gt;/securedHelloWorld&lt;/code&gt; from this server.&lt;/h2&gt;<br>&lt;h2&gt;&lt;/h2&gt;<br>&lt;/body&gt;&lt;/html&gt;</pre><p>Now, our function is secured from public calls!</p><h4>Create a service account</h4><p>Before creating our API Gateway, we need to give permission to something to call our cloud function. Then we will associate this permission with our API Gateway to let our authorized users call the cloud function. In GCP, we do this with Service Accounts. Service Accounts are non-human accounts on your GCP project used to access specific resources. The best practice is to be as granular as possible and give as little permission as possible to each Service Account. One Service Account should only do one thing.</p><blockquote>If you’ve never heard of Service Accounts before, this video is a good resource: <a href="https://www.youtube.com/watch?v=xXk1YlkKW_k">What are Service Accounts?</a></blockquote><p>In our case, we will need to have the right to call cloud functions, and we need to let the user who requests our function to act as this Service Account. For this, we will use the “Cloud Function Invoker” role and the “Service Account User” role.</p><p>Let’s configure this from the GCP interface:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/762/0*nCVaDQTXki5bs0bJ" /></figure><h4>Create the API config</h4><p>Now, let’s create a reverse proxy using GCP.</p><p>GCP provides a service called “API Gateway” that enables us to create an API gateway, which is a specific type of reverse proxy.</p><blockquote>If you’re interested in learning more about the difference between a reverse proxy and an API Gateway, check out this StackOverflow post: <a href="https://stackoverflow.com/questions/35756663/api-gateway-vs-reverse-proxy">https://stackoverflow.com/questions/35756663/api-gateway-vs-reverse-proxy</a></blockquote><p>The GCP API Gateway service work like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/630/0*bb-eYQq_RVjXbmr8" /></figure><p>We have an API with a configuration that determines how the API behaves and a gateway that serves as an endpoint to call it. To secure our Firebase Cloud Function with an API key, we need to create a YAML file that describes our API configuration. This YAML file follows the OpenAPI standard and specifies that we want our API to redirect only authorized users, who have the correct API key, to our cloud function. This configuration can be translated into the following YAML file:</p><pre>swagger: &quot;2.0&quot;<br>info:<br>  title: secured-api<br>  description: Secured API to let only authorized users to access the cloud function<br>  version: 1.0.0<br>schemes:<br>  - https<br>produces:<br>  - application/json<br>paths:<br>  /hello:<br>    get:<br>      summary: Receive a hello world message<br>      operationId: hello<br>      x-google-backend:<br>        address: https://myfirebaseproject.cloudfunction.net/securedHelloWorld<br>        protocol: h2<br>      security:<br>      - api_key: []<br>      responses:<br>        &quot;202&quot;:<br>          description: Messages received, will be treated<br>          schema:<br>            type: string<br>        &quot;400&quot;:<br>          description: Bad request<br>          schema:<br>            type: string<br>securityDefinitions:<br>  api_key:<br>    type: &quot;apiKey&quot;<br>    name: &quot;key&quot;<br>    in: &quot;query&quot;</pre><p>That file is quite verbose, let’s summarize what it’s all about.</p><p>We can see that this YAML file defines a title and description, which are used to set the name and description of the API in GCP. The paths section specifies the endpoint that we can call. In this case, we have a configuration for the /hello path that allows for an HTTP GET request. The security section specifies that requests require an API key defined by the key api_key. The securityDefinitions section defines the type of security as an apiKey, and specifies that the API key needs to be provided in the query as the value of the key parameter. This YAML file can be used to create a secure API that only allows authorized users to access the Cloud Function.</p><p>Now that our API configuration is complete, we can proceed to create it on GCP using the following command:</p><pre>gcloud api-gateway api-configs create CONFIG_ID \<br>  --api=API_ID --openapi-spec=path-to-yaml-file \<br>  --project=projectid --backend-auth-service-account=service-account-email</pre><p>This command requires several arguments. The CONFIG_ID and API_ID are unique identifiers for the API and its corresponding configuration. Note that if you haven’t created an API first, it will automatically create one for you using the given ID.</p><blockquote>It is important to note that the IDs must follow a specific format, and you should avoid using special characters or keeping them too long.</blockquote><p>You also need to provide the path to your configuration file, your project ID, and the identifier of your service account that was created earlier. This identifier corresponds to the email address visible from the GCP console.</p><p>Finally, we need to enable the managed service associated with the API by running the following command:</p><pre>gcloud services enable managed-service</pre><p>You can find your managed service name at the landing page of your API Gateway’s.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/766/0*rKWmpHMfNKX7V-G5" /></figure><h4>Deploy your API to a Gateway</h4><p>Now that we have set up everything for our API, the last thing to do is to actually deploy it on a gateway, to let users call it. To do that, we need to run the following command:</p><pre> gcloud api-gateway gateways create GATEWAY_ID \<br>  --api=API_ID --api-config=API_CONFIG_ID \<br>  --location=GCP_LOCATION --project=PROJECT_ID</pre><p>Here, the GATEWAY_ID is how you want to name the Gateway. API_ID and API_CONFIG_ID are IDs that we have defined before. The project ID is still the ID of your GCP project. You also need to define a location. Here is the list of available locations:</p><pre>* asia-northeast1<br>* australia-southeast1<br>* europe-west1<br>* europe-west2<br>* us-east1<br>* us-east4<br>* us-central1<br>* us-west2<br>* us-west3<br>* us-west4</pre><p>And now, if you go back to GCP, you will see your API Gateway, and you can copy/paste the URL endpoint to try it!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/766/0*4gBWjarhTCzl5jh4" /></figure><h4>Create the API Key</h4><p>Now, if you try to call your endpoint with a curl command without including an API key, you will receive an error response since you need to include the API key to access the resource. Let’s create an API Key with GCP, which can be easily done in the console:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/766/0*isv8ntrGILKX-c3k" /></figure><p>Pretty easy, right? But we should go a step further for security purposes. We need to restrict the scope of this API key to limit its power as much as possible. We must restrict its usage to only the API we defined earlier. This can also be easily done using the GCP console:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/766/0*SFeO-8ZyESm1ujPt" /></figure><h4>Testing and troubleshooting API key security</h4><p>Everything is done! Good job! Now we have to test that everything works correctly. Here is the checklist that you need to verify:</p><ul><li>Calling your Firebase Cloud Function directly must fail</li><li>Calling your API Gateway without an API key must fail</li><li>Calling your API Gateway with the wrong API key must fail</li><li>Calling your API Gateway with the correct API key must succeed</li></ul><p>If these steps are valid, you have successfully secured your Firebase Cloud Function with an API key! Congrats!</p><p>To go a step further, you could monitor strange usage of your API key in order to detect if someone has stolen it. This will not be covered here, but maybe in another article we’ll see</p><h4>Conclusion</h4><p>That’s all for this article, I hope you have learned something and had a good time reading it. Don’t forget that an API key might not be the perfect way to secure a Firebase Cloud Function. As always, it depends on the context. If you have any ways to improve this tutorial, feel free to share them in the comments.</p><p>👏 Please clap this article if you found it useful 👏</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=37028dee39e0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Run Go in Dart VM with dart:ffi]]></title>
            <link>https://pmonier.medium.com/run-go-in-dart-vm-with-dart-ffi-7d3575fe9c0b?source=rss-de40fb673fd9------2</link>
            <guid isPermaLink="false">https://medium.com/p/7d3575fe9c0b</guid>
            <category><![CDATA[golang]]></category>
            <category><![CDATA[dartlang]]></category>
            <category><![CDATA[dart]]></category>
            <dc:creator><![CDATA[Pierre Monier]]></dc:creator>
            <pubDate>Wed, 04 May 2022 21:18:26 GMT</pubDate>
            <atom:updated>2022-05-06T11:07:08.131Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/955/1*G-ykpeek5QKqTaxD3zGw3g.png" /></figure><h3>Introduction</h3><p>There is a really cool feature with Dart named dart:ffi. It’s the ability of the dart VM to run code that isn’t written in Dart. Basically, you can execute nearly everything that compiles to a shared library. A shared library is a file that is intended to be used across multiple programs. It takes the form of a <em>dll</em> file on the windows platform or a <em>.so</em> file on the Linux platform.</p><p>To glue the shared library with our Dart code, we use the <a href="https://api.dart.dev/stable/2.16.2/dart-ffi/dart-ffi-library.html">dart:ffi</a> package. It will load the shared library and get the exported functions from it. We can write our shared library code with C, C++, Rust, and theoretically anything that can be built to a shared library. In this demo, we are going to use Go.</p><p>We are going to learn how to use low level code in our Dart application.</p><h3>Demo</h3><p>To try this out, we are going to write a function in Go, that sums two numbers. Then we will call this function from our Dart code and print the result. It’s pretty easy, but we can learn a lot just with this little demo. Final code can be found here</p><p><a href="https://github.com/Pierre-Monier/dart_ffi_go_demo">GitHub - Pierre-Monier/dart_ffi_go_demo</a></p><h3>The Go part</h3><p>We need a function that makes the sum of two numbers and returns the result. In Go, it looks like this :</p><pre>func Sum(a int, b int) int {<br>  return a + b<br>}</pre><p>Very simple. But there is a problem, our Dart app can’t use this function like this. The reason is that Dart doesn’t know anything about the Golang <em>int</em> type. We must use a type system that is understandable by Dart.</p><p>To solve this, we use the C type system. It will be our common type interface. Dart knows about it (thanks to dart:ffi). So let’s rewrite our function like this :</p><pre>import (<br>  “C”<br>)</pre><pre>func Sum(a C.int, b C.int) C.int {<br>  return a + b<br>}</pre><p>We have just replaced types from <em>int</em> to <em>C.int</em>.</p><p>The last step is to export the function from the shared library. To do this, we just add a comment on top of our function :</p><pre>//export Sum<br>func Sum(a C.int, b C.int) C.int {<br> return a + b<br>}</pre><p>And voilà, our function is ready to be used by Dart. Before that we must actually build our Go code to a shared library. It’s quite easy, just use the following command line</p><pre>go build -buildmode=c-shared -o lib.a lib.go</pre><p>It builds a shared library named lib.a with our file lib.go.</p><blockquote>You can check that your function is well exported in the shared library by using the command <em>nm -gU lib.a</em></blockquote><p>Ok, perfect! We are all set up on the Go part. Now it’s time to go to the Dart side.</p><h3>The Dart part</h3><p>In this part we are going to do three things. First we will load the shared library, then we will pick up our function in it, finally we will call it.</p><p>Let’s create a class named <em>FFIBridge</em> that will be responsible for calling methods from the shared library.</p><pre>class FFIBridge {}</pre><p>Ok, so remember, the first thing is to load the shared library. We can do it, either statically or dynamically.</p><p>Statically means that our shared library will be bundled directly in our Dart program and loaded when it starts. The process is platform specific. You can find more info on <a href="https://docs.flutter.dev/development/platform-integration/c-interop">this documentation</a>. For the sake of simplicity, we are going to load our library dynamically.</p><p>Dynamically means that we will lazily load the shared library, when we want to use it. In order to do it, we must use the `DynamicLibrary` object, and give it the path to our shared library. Let’s add a private method named <em>_getDynamicLibrary</em> :</p><pre>ffi.DynamicLibrary _getDynamicLibrary() {<br> final libraryPath = path.join(Directory.current.path, ‘go’, ‘lib.a’);</pre><pre>return ffi.DynamicLibrary.open(libraryPath);<br> }</pre><p>Perfect, we have loaded our library like professionals. Next thing, pick up functions from it.</p><p>There is an attribute named <em>lookup</em> that is used to lookup for a specific function in our shared library. We are going to use it. Let’s add a <em>_lookup</em> attribute to the <em>FFIBridge</em> class, and pass it the value of the `lookup` attribute of our shared library.</p><pre>class FFIBridge {<br> /// Holds the symbol lookup function.<br>late final ffi.Pointer&lt;T&gt; Function&lt;T extends ffi.NativeType&gt;(<br> String symbolName) _lookup;</pre><pre>FFIBridge() {<br> _lookup = _getDynamicLibrary().lookup;<br> }<br>…<br>}</pre><p>Now we lookup for functions in the shared library. It’s a good idea to create a method in our bridge to call the shared library one. Let’s add it to the class :</p><pre>int sum(int a, int b) {<br> final sumFunctionInLibrary = _lookup&lt;<br> ffi.NativeFunction&lt;ffi.Int32 Function(ffi.Int32 a, ffi.Int32 b)&gt;&gt;(<br> ‘Sum’);</pre><pre>final convertedToDartSumFunction =<br> sumFunctionInLibrary.asFunction&lt;int Function(int a, int b)&gt;();</pre><pre> return convertedToDartSumFunction(a, b);<br>}</pre><p>As you can see, we <em>lookup</em> for a C typed function and return it as a Dart function. We use <em>ffi.Int32</em> as an equivalent to our <em>Go</em> <em>C.int</em>. Finally, we execute the function we found.</p><p>Now that we have everything set up, we can write our program.</p><pre>import ‘dart/ffi_bridge.dart’;</pre><pre>void main() {<br> final ffibridge = FFIBridge();<br> final result = ffibridge.sum(2, 2);<br> print(result);<br>}</pre><p>If we execute it, we will see the number 4 printed. We did it!</p><h3>Conclusion</h3><p>As we had seen, <em>dart:ffi</em> isn’t that hard to use. Drawbacks are that we rely on the low level C type, and that we need to glue everything up.</p><p>There is a great package named <em>ffigen</em> that creates our <em>FFIBridge</em> class automatically. It uses <em>C headers</em> to generate the class.</p><blockquote>If you are on a mac M1 desktop, you might be struggling to compile your code. The solution for me was to install the ARM version of Dart from <a href="https://dart.dev/get-dart/archive">here</a>. More info about that <a href="https://stackoverflow.com/questions/71882029/mach-o-file-but-is-an-incompatible-architecture-have-arm64-need-x86-64-i">on this stackoverflow question</a></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7d3575fe9c0b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Getting waveform data with flutter and FFmpeg]]></title>
            <link>https://pmonier.medium.com/getting-waveform-data-with-flutter-and-ffmpeg-32b7abe2a026?source=rss-de40fb673fd9------2</link>
            <guid isPermaLink="false">https://medium.com/p/32b7abe2a026</guid>
            <category><![CDATA[flutter-app-development]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[ffprobe]]></category>
            <category><![CDATA[ffmpeg]]></category>
            <dc:creator><![CDATA[Pierre Monier]]></dc:creator>
            <pubDate>Mon, 14 Mar 2022 19:46:47 GMT</pubDate>
            <atom:updated>2022-03-14T19:46:47.889Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hn4ZmZXR7HhsUxtS" /></figure><p>Photo by <a href="https://unsplash.com/@atimagodie?utm_source=Hashnode&amp;utm_medium=referral">Godfrey Nyangechi</a> on <a href="https://unsplash.com/?utm_source=Hashnode&amp;utm_medium=referral">Unsplash</a></p><h3>Introduction</h3><p>Waveform is a way of representing sound.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1008/0*smtPKrDgZzM2NpJe" /></figure><p>In this article, we are going to see how we can get the data needed to draw these waveforms, in a Flutter app.</p><p>For this we are going to use a tool named FFmpeg.</p><h3>What is FFmpeg?</h3><p>FFmpeg is a cross-platform command line used to encode and decode video. It’s a very powerful tool and one of the most complex command lines I’ve ever used.</p><p>In this article, we are going to use it to get the data needed to draw a waveform.</p><p>To use it in Flutter, we are going to use the <a href="https://pub.dev/packages/ffmpeg_kit_flutter">ffmpeg_kit_flutter</a>.</p><p>This plugin only works on Android, iOS and macOS.</p><p>But isn’t FFmpeg a “cross-platform” tool ?</p><p>Yeah, but the implementation isn’t currently done. You can read this <a href="https://github.com/tanersener/flutter-ffmpeg/issues/156">GitHub issue</a> about this topic.</p><h3>What kind of data do we need?</h3><p>Waveforms represent sound. More accurately, it represents the loudness of a sound. If it’s near zero (the center of the graph), that means the sound is very quiet.</p><p>So this</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/28/0*PNl9Lvjj1lTmR9yJ" /></figure><p>Means the sound isn’t very loud.</p><p>And this</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/66/0*LqeUMKtAEw1zZyN0" /></figure><p>Means the sound is very loud.</p><p>We can accomplish this in many ways.</p><p>In our case, we are going to use the peak level of the signal in dBFS. Peak level, highlight the maximum value.</p><blockquote>If you want to show more nuance in your waveform, you should use the RMS level, more info about this topic in <a href="https://support.biamp.com/General/Audio/Peak_vs_RMS_Meters#:~:text=The%20peak%20value%20is%20the,power%20as%20the%20periodic%20signal.">this article</a></blockquote><h3>How do we get the peak level of a sound with FFmpeg?</h3><p>First thing, to get the peak level of a sound, you need a file containing the sound data.</p><p>In this article, we are going to use a cymbal sound</p><h3>Fetching the file</h3><p>For our example, we are going to put our cymbal sound file inside the app assets folder.</p><pre>assets:<br>    - assets/cymbal.wav</pre><p>Then we can get the file like this in our app:</p><pre>Future&lt;File&gt; getAudioFileFromAssets() async {</pre><pre>  final byteData = await rootBundle.load(&#39;assets/cymbal.wav&#39;);</pre><pre>  final file = File(&#39;${(await getTemporaryDirectory()).path}/cymbal.wav&#39;);<br>  await file.writeAsBytes(byteData.buffer<br>      .asUint8List(byteData.offsetInBytes, byteData.lengthInBytes));</pre><pre>  return file;<br>}</pre><p>It gives us a File object. Now we can use it to retrieve the information we want</p><h3>Getting data from the file</h3><p>Now that we have our file, we can retrieve the data.</p><p>We are going to use FFprobe. FFprobe is a part of the FFmpeg project and it’s used to get information from multimedia streams.</p><p>Here is the final command line we want to use:</p><pre>ffprobe -v error -f lavfi -i amovie=/path/to/cymbal.wav,astats=metadata=1:reset=1 -show_entries frame_tags=lavfi.astats.Overall.Peak_level -of json</pre><p>This is pretty intense, so I will explain it.</p><p>The -f lavfi option define a <a href="https://ffmpeg.org/ffmpeg-all.html#filter_005fcomplex_005foption">complex filtergraph</a>.</p><p>The -v error specifies the log level and the -of json specifies that we want the output in json format.</p><p>Finally, the most interesting parts.</p><pre>amovie=/path/to/cymbal.wav,astats=metadata=1:reset=1 -show_entries frame_tags=lavfi.astats.Overall.Peak_level</pre><p>This is what we pass to the -i parameter. The -i parameter specifies the input data. Here with amovie we tell ffprobe that we will use an audio input.</p><p>The astats part is where the magic happens. We tell ffprobe to display information about the audio channel. The metadata and reset parameters are mandatory. metadata sets up the metadata injection, which defines the number of channels. reset defines the number of frames over which cumulative stats are calculated before being reset. By default, in ffmpeg an audio frame regroups 1024 samples.</p><blockquote>If you want to change this number you can pass the asetnsamples to define how many samples should contain an audio frame. So if you set asetnsamples parameter to the audio file sample rate (usually 44.1kHz or 48kHz), you will fetch data every second.</blockquote><blockquote>All of this might sound complicated, here are some links to have a better understanding of what it is all about: <a href="https://sound.stackexchange.com/questions/41567/difference-between-frame-and-sample-in-waveform">https://sound.stackexchange.com/questions/41567/difference-between-frame-and-sample-in-waveform</a> | <a href="https://stackoverflow.com/questions/3957025/what-does-a-audio-frame-contain#:~:text=An%20audio%20frame%2C%20or%20sample,44%2C100%20frames%2Fsamples%20per%20second.">https://stackoverflow.com/questions/3957025/what-does-a-audio-frame-contain#:~:text=An%20audio%20frame%2C%20or%20sample,44%2C100%20frames%2Fsamples%20per%20second.</a></blockquote><p>We are setting metadata and reset parameters to 1. In most cases, this is what we want.</p><p>Finally, -show_entries frame_tags=lavfi.astats.Overall.Peak_level is what tells ffprobe to display the peak level information of the file. You can display a good deal of other information, just check <a href="https://ffmpeg.org/ffmpeg-all.html#astats-1">the documentation</a></p><h3>Implementation in Flutter</h3><p>Now that we know what we are going to do, let’s start the fun part.</p><p>We are going to use the <a href="https://pub.dev/packages/ffmpeg_kit_flutter">ffmpeg_kit_flutter</a> to execute our command. It works like this:</p><pre>FFprobeKit.executeAsync(command, () {<br>  callback executed when the command is done<br>});</pre><p>FFprobeKit is the entry point of the FFprobe part of the plugin. In this example, we execute The executeAsync method to execute a FFprobe command asynchronously.</p><p>The result of the command is accessible from a FFprobeSession object. There are two ways of getting a FFprobeSession object. We can await the executeAsync method, or we can execute a callback when the FFprobe command is done.</p><p>Both work. In this article, we are going to register a callback when the command is done.</p><p>Here is the final code:</p><pre>Future&lt;List&lt;double&gt;&gt; fetchDataFromFile(File audioFile) async {<br>  final audioFilePath = audioFile.path;</pre><pre>  final _completer = Completer&lt;List&lt;double&gt;&gt;();</pre><pre>  await FFprobeKit.executeAsync(<br>      &quot;-v error -f lavfi -i amovie=$audioFilePath&quot;<br>      &quot;,astats=metadata=1:reset=1 -show_entries&quot;<br>      &quot; frame_tags=lavfi.astats.Overall.Peak_level -of json&quot;,<br>      (session) async {<br>    final returnCode = await session.getReturnCode();<br>    final output = await session.getOutput();</pre><pre>    if (ReturnCode.isSuccess(returnCode)) {<br>      _completer.complete(_outputToList(output));<br>    } else {<br>      _handleFailure(returnCode: returnCode, completer: _completer);<br>    }<br>  });</pre><pre>  return _completer.future;<br>}</pre><p>As you can see we are using a Completer object to return data when our callback is executed.</p><p>When the FFprobe command is done, we get the return code and the output. We did a simple check to see if it was successful. If it’s not, we execute the _handleFailure function:</p><pre>void _handleFailure({<br>    required ReturnCode? returnCode,<br>    required Completer&lt;List&lt;double&gt;?&gt; completer,<br>  }) {<br>    if (ReturnCode.isCancel(returnCode)) {<br>      print(&quot;ffprobe commande canceled&quot;);<br>      completer.complete(null)<br>    } else {<br>      final e = Exception(&quot;command fail&quot;);<br>      completer.completeError(e);<br>    }<br>  }</pre><p>In this function, we just check that it is really a failure, and if that is the case, we make our Completer object complete with an error.</p><p>If the command is successful, we complete our Completer object with the result of the _outputToList function:</p><pre>List&lt;double&gt; _outputToList(String? output) {<br>    if (output == null || output.isEmpty) {<br>      throw Exception(&quot;No data&quot;);<br>    }</pre><pre>    try {<br>      final parsedOutput = jsonDecode(output);<br>      final frameData = parsedOutput[&quot;frames&quot;] as List;<br>      return frameData<br>          .map&lt;double?&gt;((e) {<br>            try {<br>              return (double.parse(e[&quot;tags&quot;][&quot;lavfi.astats.Overall.Peak_level&quot;] as String));<br>            } catch (e) {<br>              return null;<br>            }<br>          })<br>          .whereType&lt;double&gt;()<br>          .toList();<br>    } on Exception catch (_) {<br>      throw Exception(&quot;Parsing failed&quot;);<br>    }<br>  }</pre><p>In this function, we make sure that we have data in the output. If this is not the case, we raise an exception. Then, we try to parse the data. If the parsing fails at some point, again we raise an exception. And if everything is fine we parse the output and create a List&lt;double&gt; object out of it.</p><p>Here’s a short sample of the data we get with our cymbal sound file:</p><pre>Cymbal sound peak level:<br>-0.409556<br>-0.099999<br>-1.481092<br>-4.157582<br>-4.091515<br>-5.026431<br>-6.185354<br>-6.70617<br>-7.72662<br>-7.750185<br>-7.882804<br>-8.599196<br>-10.24849<br>-10.57842<br>-10.160495<br>-9.912185<br>-10.668357<br>-11.534391<br>-11.89096<br>-11.727936<br>-11.59894<br>...</pre><h3>Conclusion</h3><p>Ok, we now know how to use FFprobe to get data to draw waveforms. Now we can use this precious data to draw waveform using CustomPainter :). Hope you learn some stuff</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=32b7abe2a026" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flutter, add an undo feature]]></title>
            <link>https://pmonier.medium.com/flutter-add-an-undo-feature-6808b58f88b9?source=rss-de40fb673fd9------2</link>
            <guid isPermaLink="false">https://medium.com/p/6808b58f88b9</guid>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[flutter-app-development]]></category>
            <dc:creator><![CDATA[Pierre Monier]]></dc:creator>
            <pubDate>Mon, 14 Mar 2022 19:32:36 GMT</pubDate>
            <atom:updated>2022-03-14T19:41:27.286Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*V6cw744MU4Nug80U" /></figure><p>Photo by <a href="https://unsplash.com/@markuswinkler?utm_source=Hashnode&amp;utm_medium=referral">Markus Winkler</a> on <a href="https://unsplash.com/?utm_source=Hashnode&amp;utm_medium=referral">Unsplash</a></p><h3>Introduction</h3><p>Humans make mistakes all the time.</p><p>In this article, we are going to see how we can avoid unwanted regular actions that have an impact on an external system state.</p><h3>Use case</h3><p>For example, imagine you are writing a super important email for a job interview. Unfortunately, you accidentally press the send button, and you send a dummy email to your future boss. Now you have to write a new email that explains why you have just sent this strange email. This is not really the perfect scenario for your future job interview.</p><h3>What can we do?</h3><p>In this case, it will be impossible to add an alert dialog. The goal of the app is to view and send emails. If each time you send an email you have to go through a dialog that prevents a mistake that is occurring at most 1% of the time, it will be pretty annoying.</p><p>Also, you can’t add a Ctrl+z feature. When the email is sent, it’s out of the app’s control.</p><p>You might implement logic on the server side. You can push emails into a queue. Then emails are sent after a safety delay. And during this delay, the email app can tell the server “please stop this email sending task, it was a mistake.”</p><p>This can totally work, but I see three drawbacks to this solution.</p><p>First, it requires that you have total control of the server.</p><p>Second, if you have total control on the server. Implementing this feature can take a significant amount of time and effort.</p><p>Third, it’s dependent on network calls.</p><blockquote>Even if it has drawbacks, I think this solution is the best in most cases</blockquote><p>In this article, we are going to see another way. Basically, we are going to set the safety delay inside the app. It might not be the perfect solution (it depends on each case), but it’s a pretty straightforward solution, easily implemented.</p><h3>Cancelable to the rescue</h3><p>There is a great package named <a href="https://pub.dev/packages/async">async</a>. It adds some great features on asynchronous operation.</p><p>We are going to use an object from this library named CancelableOperation. It&#39;s an object that wraps a future, and it can cancel the future operation programmatically.</p><p>It works like this:</p><pre>final cancelableOperation = CancelableOperation.fromFuture(<br>      your future function here,<br>)</pre><p>What we can do now is pass a Future.delay with the desired safety delay. Then, we chain the cancelable operation with the action we want to do (for example, send an email). We can also create a function which will cancel our operation</p><p>Like this:</p><pre>const duration = Duration(seconds: 2);</pre><pre>final cancelableOperation = CancelableOperation.fromFuture(<br>      Future.delayed(duration),<br>    ).then(<br>      (_) =&gt; your action,<br>);</pre><pre>void undoFunction() {<br>      cancelableOperation.cancel();<br>}</pre><p>Now we can pass the undoFunction to a button on a snack bar for example. So if the user makes a mistake, he can still undo it.</p><p>Final code with demo</p><pre>import &quot;package:async/async.dart&quot;;<br>import &#39;package:flutter/material.dart&#39;;</pre><pre>const Color darkBlue = Color.fromARGB(255, 18, 32, 47);</pre><pre>void main() {<br>  runApp(const MyApp());<br>}</pre><pre>class MyApp extends StatelessWidget {<br>  const MyApp({Key? key}) : super(key: key);</pre><pre>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      theme: ThemeData.dark().copyWith(<br>        scaffoldBackgroundColor: darkBlue,<br>      ),<br>      home: const Scaffold(<br>        body: Center(<br>          child: MyWidget(),<br>        ),<br>      ),<br>    );<br>  }<br>}</pre><pre>class MyWidget extends StatefulWidget {<br>  const MyWidget({Key? key}) : super(key: key);</pre><pre>  @override<br>  State&lt;StatefulWidget&gt; createState() =&gt; _MyWidgetState();<br>}</pre><pre>class _MyWidgetState extends State&lt;MyWidget&gt; {<br>  final _items = &lt;String&gt;[];</pre><pre>  // HERE <br>  void _triggerActionWithAnUndoOption(BuildContext context) {<br>    const duration = Duration(seconds: 2);</pre><pre>    final cancelableOperation = CancelableOperation.fromFuture(<br>      Future.delayed(duration),<br>    ).then(<br>      (_) =&gt; _addItem(&quot;My action is done&quot;),<br>    );</pre><pre>    void undoFunction() {<br>      cancelableOperation.cancel();<br>      _addItem(&quot;My action is canceled&quot;);<br>    }</pre><pre>    _showSnackbar(<br>      context: context,<br>      undoFunction: undoFunction,<br>      duration: duration,<br>    );<br>  }</pre><pre>  void _addItem(String item) {<br>    setState(() {<br>      _items.add(item);<br>    });<br>  }</pre><pre>  void _showSnackbar(<br>      {required BuildContext context,<br>      required void Function() undoFunction,<br>      required Duration duration}) {<br>    final snackBar = SnackBar(<br>      content: const Text(&#39;Ok, you can still undo this&#39;),<br>      duration: duration,<br>      action: SnackBarAction(<br>        label: &#39;UNDO&#39;,<br>        onPressed: undoFunction,<br>      ),<br>    );</pre><pre>    // Find the ScaffoldMessenger in the widget tree<br>    // and use it to show a SnackBar.<br>    ScaffoldMessenger.of(context).showSnackBar(snackBar);<br>  }</pre><pre>  @override<br>  Widget build(BuildContext context) {<br>    return ListView(children: [<br>      TextButton(<br>          child: const Text(&quot;Click me :)&quot;),<br>          onPressed: () =&gt; _triggerActionWithAnUndoOption(context)),<br>      ..._items.map&lt;Widget&gt;((e) =&gt; Center(child: Text(e))),<br>    ]);<br>  }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*5hWyJYCeUpiS5qRSKClxFA.gif" /></figure><h3>Drawback</h3><p>This is great. The user can undo his action if he wants, without being annoyed.</p><p>A major drawback is that if the user cleans the app resources while the action is still in the safety delay, the action will not be executed.</p><p>So if the user does that</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/150/1*ub6gLQ8iwVzcAJocgkySJg.gif" /></figure><p>Our action might not be executed.</p><p>This is pretty logical, because we clean all resources and our desired action is still waiting to be executed.</p><h3>What can we do?</h3><p>We can still make it work on the app side.</p><p>For this, we will have to execute our action with a safety delay in the background.</p><p>There is a great package for this named <a href="https://pub.dev/packages/workmanager">workmanager</a></p><blockquote>Only works on Android and iOS</blockquote><p>The action will be executed in the background. That means that it will not be able to access the app resources. You can’t pass any object instance from your app. The action must be static.</p><blockquote>First you must set up your app, for this check the <a href="https://github.com/fluttercommunity/flutter_workmanager/blob/main/ANDROID_SETUP.md">documentation for the Android platform</a> and for the <a href="https://github.com/fluttercommunity/flutter_workmanager/blob/main/IOS_SETUP.md">iOS platform</a></blockquote><p>It works like this.</p><p>First, we register a callback that is going to execute different functions based on a task name.</p><pre>void callbackDispatcher() {<br>  Workmanager().executeTask((task, inputData) {<br>    print(&quot;Native called background task: $task&quot;); //simpleTask will be emitted here.<br>    return Future.value(true);<br>  });<br>}</pre><blockquote>This must be at the top level of the app, or a static method of a class</blockquote><p>We are going to create a service who are going to do three things.</p><ul><li>register the callbackDispatcher</li><li>Send task on the background</li><li>Cancel task</li></ul><pre>import &#39;package:workmanager/workmanager.dart&#39;;</pre><pre>void callbackDispatcher() {<br>  BackgroundService.workmanager.executeTask((task, inputData) {<br>    print(&quot;Native called background task:&quot;); //simpleTask will be emitted here.<br>    return Future.value(true);<br>  });<br>}</pre><pre>class BackgroundService {<br>  static final Workmanager workmanager = Workmanager();<br>  static const String taskName = &quot;UndoDemo&quot;;</pre><pre>  static Future&lt;void&gt; initialize() {<br>    try {<br>      return workmanager.initialize(callbackDispatcher, isInDebugMode: true);<br>    } on Exception catch (e) {<br>      print(e);<br>      rethrow;<br>    }<br>  }</pre><pre>  static Future&lt;void&gt; executeTaskInBackground(<br>      {required String uniqueName, Duration duration = Duration.zero}) async {<br>    return workmanager.registerOneOffTask(uniqueName, taskName,<br>        initialDelay: duration);<br>  }</pre><pre>  static Future&lt;void&gt; cancelTask(String uniqueName) {<br>    return workmanager.cancelByUniqueName(uniqueName);<br>  }<br>}</pre><blockquote>We use static method, this is great for our simple use case. One drawback of this is that static method are hard to mock, meaning they are hard to use in test.</blockquote><p>First, you must update the main method to initialize the BackgroundService</p><pre>void main() async {<br>  WidgetsFlutterBinding.ensureInitialized();<br>  await BackgroundService.initialize();<br>  runApp(const MyApp());<br>}</pre><p>Now we can update the function trigger by our button to make it like this</p><pre>void _triggerActionWithAnUndoOption(BuildContext context) {<br>    const duration = Duration(seconds: 2);</pre><pre>    const taskUniqueName = &quot;simpleTask&quot;;</pre><pre>    BackgroundService.executeTaskInBackground(<br>        uniqueName: taskUniqueName, duration: duration);</pre><pre>    void undoFunction() {<br>      BackgroundService.cancelTask(taskUniqueName);<br>      _addItem(&quot;My action is canceled&quot;);<br>    }</pre><pre>    _showSnackbar(<br>      context: context,<br>      undoFunction: undoFunction,<br>      duration: duration,<br>    );<br>  }</pre><p>Now we have the same workflow as before, but our actions are executed in the background. So even if our user clears the app process before our action is executed, it will still be executed (in the background).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/300/1*wmt8aFVT6gncTojsToeXug.gif" /></figure><blockquote>Note the debug notification who tell us that our background task was successful</blockquote><h3>Conclusion</h3><p>Our solution was pretty simple and straightforward.</p><p>Now our user can make errors without much consequence. We learn what was a CancelableOperation and that we can execute tasks in the background with <a href="https://pub.dev/packages/workmanager">workmanager</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6808b58f88b9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Get the best out of Flutter flavors with flutter_flavorizr]]></title>
            <link>https://pmonier.medium.com/get-the-best-out-of-flutter-flavors-with-flutter-flavorizr-5c5da0f00b99?source=rss-de40fb673fd9------2</link>
            <guid isPermaLink="false">https://medium.com/p/5c5da0f00b99</guid>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[flutter-app-development]]></category>
            <category><![CDATA[mobile]]></category>
            <dc:creator><![CDATA[Pierre Monier]]></dc:creator>
            <pubDate>Mon, 21 Feb 2022 07:37:41 GMT</pubDate>
            <atom:updated>2022-02-21T07:37:41.513Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*26j9DFanqFGJUtuGuv2o-w.jpeg" /></figure><h3>Introduction</h3><p>Flavors are a way of creating different environments for our application. It allows us to specify the app configuration for each of our cases. For example, we want to create a development flavor that is going to use different API endpoints than the production flavor which will be available to customers. Flutter makes it easy, we will see how, but there is currently no out-of-the-box solution inside Flutter itself. Thanks to the awesome Flutter community, there are some pretty good packages on <a href="https://pub.dev/">pub.dev</a> to make our life easier. In this post, we will see how to set flavors to a Flutter application. Then we are going to see how we can get the best out of these flavors. We will see how it simplifies the CI/CD and how we can configure VsCode to run each flavor from the interface.</p><h3>Demo App</h3><p>To demonstrate the power of flavors, we are going to create a small, useless app that is only going to display its boring information (app name, package name) using <a href="https://pub.dev/packages/package_info_plus">package_info_plus</a> and a GIF, which will be different for each flavor.</p><p>You can find the repo on <a href="https://github.com/Pierre-Monier/flavor_demo">github</a></p><p>The code of the main.dart file looks like this:</p><pre>import &#39;dart:async&#39;;</pre><pre>import &#39;package:flavors_demo/flavors.dart&#39;;<br>import &#39;package:flutter/material.dart&#39;;<br>import &#39;package:package_info_plus/package_info_plus.dart&#39;;</pre><pre>void main() {<br>  runApp(const MyApp());<br>}</pre><pre>class MyApp extends StatelessWidget {<br>  const MyApp({Key? key}) : super(key: key);</pre><pre>  @override<br>  Widget build(BuildContext context) {<br>    return MaterialApp(<br>      title: &#39;Flavors Demo&#39;,<br>      theme: ThemeData.dark(),<br>      home: const MyHomePage(title: &#39;Flavors Demo&#39;),<br>    );<br>  }<br>}</pre><pre>class MyHomePage extends StatefulWidget {<br>  const MyHomePage({Key? key, required this.title}) : super(key: key);</pre><pre>  final String title;</pre><pre>  @override<br>  _MyHomePageState createState() =&gt; _MyHomePageState();<br>}</pre><pre>class _MyHomePageState extends State&lt;MyHomePage&gt; {<br>  PackageInfo _packageInfo = PackageInfo(<br>    appName: &#39;Unknown&#39;,<br>    packageName: &#39;Unknown&#39;,<br>    version: &#39;Unknown&#39;,<br>    buildNumber: &#39;Unknown&#39;,<br>  );</pre><pre>  @override<br>  void initState() {<br>    super.initState();<br>    _initPackageInfo();<br>  }</pre><pre>  Future&lt;void&gt; _initPackageInfo() async {<br>    final info = await PackageInfo.fromPlatform();<br>    setState(() {<br>      _packageInfo = info;<br>    });<br>  }</pre><pre>  Widget _infoTile(String title, String subtitle) {<br>    return ListTile(<br>      title: Text(title),<br>      subtitle: Text(subtitle.isEmpty ? &#39;Not set&#39; : subtitle),<br>    );<br>  }</pre><pre>  @override<br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(widget.title),<br>      ),<br>      body: ListView(<br>        children: &lt;Widget&gt;[<br>          _infoTile(&#39;App name&#39;, _packageInfo.appName),<br>          _infoTile(&#39;Package name&#39;, _packageInfo.packageName),<br>          Image.network(<br>            &quot;https://media.giphy.com/media/s9ijJ0AI4JKko/giphy.gif&quot;,<br>          ),<br>        ],<br>      ),<br>    );<br>  }<br>}</pre><p>Nothing much is happening here, just displaying some app info and an Image from a classic french TV show. The result look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/858/0*m2TuZ_MbikKdyS1Z" /></figure><p>Now we are going to add 3 flavors to our app. One for development, another one for staging, and finally, one for production.</p><h3>Setting up flavors</h3><p>Configuring flavors is mostly done by setting up some config files in the Android and iOS folder. It’s from my point of view, not really the most exciting part of creating an app, but thanks to the community, some have created tools to make this part easier. In this article, we are going to focus on <a href="https://pub.dev/packages/flutter_flavorizr">flutter_flavorizr</a>.</p><blockquote>At the time I am writing this article, the latest version (2.1.2) isn’t compatible with the integration_test package. So if you want to use this package and still write integration tests, you should add the following line to your pubspec.yaml file flutter_flavorizr: &lt;2.1.2.</blockquote><p>Once installed, we can add the following lines to our pubspec.yaml file.</p><pre>flavorizr:<br>  ide: &quot;vscode&quot;<br>  app:<br>    android:<br>      flavorDimensions: &quot;flavor-type&quot;<br>    ios:</pre><pre>  flavors:<br>    development:<br>      app:<br>        name: &quot;Development Flavor&quot;</pre><pre>      android:<br>        applicationId: &quot;com.dev.flavorsdemo&quot;</pre><pre>      ios:<br>        bundleId: &quot;com.dev.flavorsdemo&quot;<br>        buildSettings:<br>          # Development Team is visible in the apple developer portal <br>          DEVELOPMENT_TEAM: YOURDEVTEAMID <br>          PROVISIONING_PROFILE_SPECIFIER: &quot;Dev-ProvisioningProfile&quot;</pre><pre>    staging:<br>      app:<br>        name: &quot;Staging App&quot;</pre><pre>      android:<br>        applicationId: &quot;com.staging.flavorsdemo&quot;<br>      ios:<br>        bundleId: &quot;com.staging.flavorsdemo&quot;<br>        buildSettings:<br>          DEVELOPMENT_TEAM: YOURSTAGINGTEAMID<br>          PROVISIONING_PROFILE_SPECIFIER: &quot;Staging-ProvisioningProfile&quot;</pre><pre>    production:<br>      app:<br>        name: &quot;Production App&quot;</pre><pre>      android:<br>        applicationId: &quot;com.production.flavorsdemo&quot;<br>      ios:<br>        bundleId: &quot;com.production.flavorsdemo&quot;<br>        buildSettings:<br>          DEVELOPMENT_TEAM: YOURPRODUCTIONTEAMID<br>          PROVISIONING_PROFILE_SPECIFIER: &quot;Production-ProvisioningProfile&quot;</pre><p>What’s happening ?</p><p>The first config we see is the flavorDimensions for Android. This is to create complex flavors that can combine with each other. Note that the value flavor-type is the default, and this is what we want in most cases. If you want to read about this topic, here is the <a href="https://developer.android.com/studio/build/build-variants">documentation</a></p><p>Next we can see our array of flavors. In our case we have defined 3 flavors, development, staging and production. Each of these flavors has a different app name, a different applicationId for Android and different bundleId/buildSettings for iOS.</p><h3>Other config</h3><p>There are more configurations we can add. First, if you have already configured app icons with flutter_launcher_icons, you can add generateDummyAssets to false. This will prevent flutter_flavorizr to create dummy assets for each of your flavors.</p><p>If you want to set a different icon for each of your flavors, you can do that with flutter_launcher_icons. flutter_flavorizr also has this feature, just add the icon field and set it to the path of the desired icon. For example:</p><pre>flavors:<br>    development:<br>      app:<br>        name: &quot;Development Flavor&quot;</pre><pre>      android:<br>        applicationId: &quot;com.dev.flavorsdemo&quot;<br>        icon: &quot;assets/path-to-your-icon&quot;<br></pre><pre>      ios:<br>        bundleId: &quot;com.dev.flavorsdemo&quot;<br>        buildSettings:<br>          # Development Team is visible in the apple developer portal <br>          DEVELOPMENT_TEAM: YOURDEVTEAMID <br>          PROVISIONING_PROFILE_SPECIFIER: &quot;Dev-ProvisioningProfile&quot;<br>        icon: &quot;assets/path-to-your-icon&quot;</pre><p>You can also add a configuration related to Firebase. Check <a href="https://pub.dev/packages/flutter_flavorizr">the documentation</a> for more info about that</p><p>There is an IDE field to configure your IDE. But we will see that later.</p><h3>Create flavors</h3><p>Now that we have our flavors set up, we can create them! For that run the command flutter pub run flutter_flavorizr. This will create all the configuration files for Android and iOS. It will also generate dart files. Let&#39;s see what they are used for</p><h3>flavors.dart</h3><p>You can see this file like a .env file, but with more power because it’s a dart file. It’s pretty simple, you have an enum named Flavor and this enum has 3 possible values (each for one of your defined flavors). Now, to fetch a specific value for one flavor, we create a static method. Here is the generated default file:</p><pre>enum Flavor {<br>  DEVELOPMENT,<br>  STAGING,<br>  PRODUCTION,<br>}</pre><pre>class F {<br>  static Flavor? appFlavor;</pre><pre>  static String get name =&gt; appFlavor?.name ?? &#39;&#39;;</pre><pre>  static String get title {<br>    switch (appFlavor) {<br>      case Flavor.DEVELOPMENT:<br>        return &#39;Development Flavor&#39;;<br>      case Flavor.STAGING:<br>        return &#39;Staging App&#39;;<br>      case Flavor.PRODUCTION:<br>        return &#39;Production App&#39;;<br>      default:<br>        return &#39;title&#39;;<br>    }<br>  }</pre><pre>}</pre><p>To define an env variable, we add something like this in our flavors file:</p><pre>static String get memeUrl {<br>    switch (appFlavor) {<br>      case Flavor.DEVELOPMENT:<br>        return &quot;https://media.giphy.com/media/s9ijJ0AI4JKko/giphy.gif&quot;;<br>      case Flavor.STAGING:<br>        return &quot;https://media.giphy.com/media/XknChYwfPnp04/giphy.gif&quot;;<br>      case Flavor.PRODUCTION:<br>        return &quot;https://media.giphy.com/media/zrCSvFfl2fP7W/giphy.gif&quot;;<br>      default:<br>        throw Exception(&quot;Unknown flavor for memeUri&quot;);<br>    }<br>  }</pre><p>We just define an env variable. Now we can use it in our app like this:</p><pre>Image.network(F.memeUrl)</pre><p>You might be asking yourself “but how is my app going to know what flavor to use?”. This is our next step.</p><h3>main-flavor.dart</h3><p>In order to know which flavor to use, we need a new entry point for each of our flavors. It might be weird because we are used to only one app entry point, main.dart. Now we have three of them. In each of these new entry points, we are setting the static field appFlavor of the F class to the desired flavor.</p><pre>import &#39;package:flutter/material.dart&#39;;<br>import &#39;app.dart&#39;;<br>import &#39;flavors.dart&#39;;</pre><pre>void main() {<br>  F.appFlavor = Flavor.DEVELOPMENT;<br>  runApp(MyApp());<br>}</pre><p>For example, here in main-development.dart we are settingFto Flavor.DEVELOPMENT. This is what tells the app, &quot;hey, please use the integration flavor this time.&quot;</p><p>Finally, we are calling the runApp function with a MyApp object instance. Note that by default it creates an App widget. This isn&#39;t the entry point widget we want. Indeed, this is a dummy widget, created byflutter_flavorizr. We need to replace this.</p><h3>app.dart</h3><p>This is the place where we place the root widget. We are just going to replace everything inside that file with the content of the main.dart file.</p><blockquote>There is also a home.dart file that is generated. You can delete it safely. You can also delete the main.dart file which is no longer useful.</blockquote><h3>result</h3><p>Now we can run our 3 different flavors, the result look like this:</p><h3>development flavor</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/858/0*ihfz6V0kfRRCu6PO.png" /></figure><h3>staging flavor</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/858/0*tknZslORBqDiaSkx" /></figure><h3>production flavor</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/858/0*-UdJDxHl4_JHUmbU" /></figure><h3>Flavor update</h3><p>The next time you want to update your flavors, you don’t want to redo everything. For this, you can run a specific processor. A processor is a specific task done byflutter_flavorizr. For example, maybe you don&#39;t want to override your dart file or recreate your app icon. For this, use the following syntax flutter pub run flutter_flavorizr -p &lt;processor&gt;. For example</p><pre>flutter pub run flutter_flavorizr -p android:buildGradle,android:androidManifest,ios:xcconfig,ios:buildTargets,ios:schema,ios:plist</pre><p>If you want to update only Android and iOS without the icon generation.</p><blockquote>YOU MUST AVOID ANY SPACE BETWEEN EACH PROCESSOR BECAUSE IT WILL NOT WORK.</blockquote><p>You do</p><pre> -p &lt;processor1&gt;,&lt;processor2&gt;</pre><p>but don&#39;t</p><pre> -p &lt;processor1&gt;,  &lt;processor2&gt;</pre><blockquote>The full list of processors can be found <a href="https://pub.dev/packages/flutter_flavorizr#available-instructions">here</a></blockquote><h3>Configure VsCode</h3><p>Have you noticed this line in our flavors’ config ?</p><p>ide: &quot;vscode&quot;</p><p>This tells flutter_flavorizr to configure your IDE, in our case VsCode. You can also set the value to idea if you use Android Studio.</p><p>Now in the debug panel of VsCode, you can see this</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/473/0*xasZn29ufIzKbus7" /></figure><p>With this you can run each of your flavors in every flutter mode (debug, profile, release).</p><p>This is really neat, let’s see how it works.</p><p>The classic way to run a Flutter app is flutter run. However this command assumes that you have a file named main.dart as the app entry point and no flavor configuration. To run one of our flavors, we need to do flutter run --flavor &lt;flavor&gt; -t lib/main-&lt;flavor&gt;.dart. So, to run or staging flavor flutter run --flavor -lib/main-staging.dart.</p><p>The --flavor argument specifies which flavor build config it should use. The -t argument specifies the entry point file.</p><p>Now, to run a flutter in a specific flutter mode use the flag --&lt;flutter-mode&gt;. So to run our production flavor in profile mode we do flutter run --profile --flavor production -t lib/main-production.dart</p><blockquote>The debug mode is the default mode</blockquote><p>All this stuff is already done inside the .vscode/launch.json (created by flutter_flavorizr).</p><p>Now, if we have an issue with the staging app, we can just run our app with the staging flavor in debug mode from VsCode!</p><h3>CI/CD</h3><p>With these well-configured flavors, the CI/CD is also simplified. To demonstrate how we can make our CI/CD job easier, let’s create a Jenkinsfile.</p><blockquote>The goal of this article isn’t to explain how Jenkins works, so I will not speak in deep detail about how it works</blockquote><p>We will accomplish the following things:</p><ul><li>run a flutter doctor command</li><li>build a .ipa file for iOS</li><li>build an app bundle for Android</li><li>create artifacts from these build files</li></ul><p>Here is the Jenkinsfile:</p><pre>#!/usr/bin/groovy<br>def appName = &quot;Flavor Demo&quot;</pre><pre>node(&#39;someNode&#39;) {<br>    withEnv([&quot;PATH=path-to-a-flutter-bin:$PATH&quot;]) {<br>        def flavorName = env.BRANCH_NAME;</pre><pre>        if (flavorName == &#39;development&#39; || flavorName == &#39;recette&#39; || flavorName == &#39;production&#39;) {<br>            def appNameAndroid = appName + &quot;-${flavorName}-&quot; + env.BUILD_NUMBER + &quot;.aab&quot;;<br>            def appNameiOS = appName + &quot;-${flavorName}-&quot; + env.BUILD_NUMBER + &quot;.ipa&quot;;<br>            def exportPlistFile = &quot;./ios/exportOptions.${flavorName}.plist&quot;</pre><pre>            stage(&#39;flutter doctor&#39;) {<br>                sh &quot;flutter doctor&quot;<br>            }</pre><pre>            stage(&quot;Build AAB Flutter Android for flavor ${flavorName}&quot;) {<br>                sh &quot;mkdir -p build_ci&quot;<br>                sh &quot;&quot;&quot;<br>                    flutter build appbundle --release --flavor ${flavorName} -t lib/main-${flavorName}.dart<br>                    cp -pr build/app/outputs/bundle/${flavorName}Release/app-${flavorName}-release.aab build_ci/flutter_app.aab<br>                &quot;&quot;&quot;<br>            }</pre><pre>            stage(&quot;Build IPA Flutter iOS for flavor ${flavorName}&quot;) {<br>                sh &quot;mkdir -p build_ci&quot;<br>                sh &quot;&quot;&quot;<br>                    export LANG=en_US.UTF-8<br>                    flutter build ipa --release --flavor ${flavorName} -t lib/main-${flavorName}.dart<br>                    xcodebuild -exportArchive -archivePath ./build/ios/archive/Runner.xcarchive -exportOptionsPlist ${exportPlistFile} -exportPath flutter_app_ios<br>                    mv flutter_app_ios/*.ipa ./build_ci/flutter_app.ipa<br>                &quot;&quot;&quot;<br>            }</pre><pre>            stage(&#39;Creating Artifacts&#39;) {<br>                dir(&quot;build_ci&quot;) {<br>                    sh &quot;mv flutter_app.aab ${appNameAndroid}&quot;<br>                    sh &quot;mv flutter_app.ipa ${appNameiOS}&quot;</pre><pre>                    archiveArtifacts allowEmptyArchive: true, artifacts: appNameAndroid<br>                    archiveArtifacts allowEmptyArchive: true, artifacts: appNameiOS<br>                }<br>            }<br>        }<br>    }<br>}</pre><blockquote>You need to create one export.plist file for each flavor.</blockquote><p>As you can see, we are doing the exact same thing, just with different parameters.</p><p>In this case, we are assuming that each flavors have a specific git branch. This is really useful because we know what flavor we want to process based on the branch name.</p><p>We just have to define 4 variables.</p><p>Just like with the flutter run command, we are using the -t and the --flavor to build a specific flavor.</p><h3>Conclusion</h3><p>What did we do?</p><p>We have created 3 different flavors for our app, configured our IDE to take full advantage of our flavors and finally, we demonstrated how we can make our CI/CD easier.</p><p>It was not that hard. In real projects, I usually get some issues with the iOS configuration. You might spend some time debugging the configuration in Xcode. It’s still better than creating it from scratch.</p><p>flutter_flavorizr is currently only working on Android and iOS. Hopefully it will handle desktop as well in the future.</p><p>You can find the demo app on <a href="https://github.com/Pierre-Monier/flavor_demo">github</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5c5da0f00b99" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Create your first Flutter app, an APOD cross-platform application]]></title>
            <link>https://pmonier.medium.com/create-your-first-flutter-app-an-apod-cross-platform-application-f3786f5e8cdd?source=rss-de40fb673fd9------2</link>
            <guid isPermaLink="false">https://medium.com/p/f3786f5e8cdd</guid>
            <category><![CDATA[cross-platform]]></category>
            <category><![CDATA[first-app-flutter]]></category>
            <category><![CDATA[flutter]]></category>
            <category><![CDATA[dart]]></category>
            <dc:creator><![CDATA[Pierre Monier]]></dc:creator>
            <pubDate>Sun, 28 Feb 2021 07:35:20 GMT</pubDate>
            <atom:updated>2021-02-28T17:17:40.624Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TKQzeEK3nzhjSuXD.png" /></figure><p>Flutter is a cross-platform framework, built by Google to create beautiful and robust applications. It’s written in a relatively new language named Dart, also built by Google. In this tutorial, we’re going to create our first app with flutter. We’re going to create a to-do app or a counter app like every other tutorial out there. We’re going to create an APOD app. If you’re an astronomy fan you already know what I’m talking about. APOD (Astromical Picture Of the Day) is a fantastic website maintained by NASA. Every day, a new astronomical picture is uploaded, followed by a description to explain what is going on.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sz7ClGsKWISPZxHKc7w5FQ.png" /></figure><p>Our app is going to display these informations, but we’re going to do more than that. We’re going to add an option to set the daily picture as the device wallpaper.</p><p>This tutorial requires to have a functional version of Flutter installed. Also, you need an IDE (Android Studio or VsCode) with Flutter and Dart support and a mobile device or a mobile device emulator.</p><p>First step we are going to create the default Flutter with the Flutter cli tool. It’s really straightforward, just open a terminal and tap</p><pre>flutter create apod_app</pre><p>This will create a counter app. Now you can run the project on a device. Just go in your app with a terminal and tap</p><pre>flutter run</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*YDs5mQkBMk77-rSyrtzZeg.jpeg" /></figure><p>Now the application is running on your device. It’s a simple example, but it shows up how we build a UI with Flutter. We use widget, it’s the basic of class you are going to use a lot. There is two type of widget, the <em>StatelessWidget</em> and the <em>StatefulWidget</em>. <em>StatelessWidget</em> are immutable, once a <em>StatelessWidget</em> is render by Flutter, it doesn’t change. <em>StatefulWidget</em> on the other hand hold a state object. This object can’t change over time and when it changes, the UI is updated. In the default app, tapping on the button call this function</p><pre>void _incrementCounter() {<br>    setState(() {<br>      _counter++;<br>    });<br>  }</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*jHG_i5UE3zbdnSI0.gif" /></figure><p>As you can see, it calls another function named <em>setState</em>. This function tells Flutter that something has changed in this <em>State</em>, which trigger the <em>build</em> method. The build method is the one who display the data. It returns others widget objects.</p><pre><a href="http://twitter.com/override">@override</a><br>  Widget build(BuildContext context) {<br>    return Scaffold(<br>      appBar: AppBar(<br>        title: Text(widget.title),<br>      ),<br>      body: Center(<br>        child: Column(<br>          mainAxisAlignment: MainAxisAlignment.center,<br>          children: &lt;Widget&gt;[<br>            Text(<br>              &#39;You have pushed the button this many times:&#39;,<br>            ),<br>            Text(<br>              &#39;$_counter&#39;,<br>              style: Theme.of(context).textTheme.headline4,<br>            ),<br>          ],<br>        ),<br>      ),<br>      floatingActionButton: FloatingActionButton(<br>        onPressed: _incrementCounter,<br>        tooltip: &#39;Increment&#39;,<br>        child: Icon(Icons.add),<br>      ), <br>    );<br>  }</pre><p>You must use <em>StatelessWidget</em> over <em>StatefullWidget</em>. <em>StatelessWidget</em> are much simpler so they render much faster. Of course, you will be forced to use <em>StatefullWidget</em> in your Flutter journey. But we can get rid of them in a lot of case. We are going to see that in this tutorial.</p><p>Now that we have seen the basic of Flutter, let’s get back on our project. We need to access the APOD website data. Thankfully, the NASA has made a sweet API. They also have others very cool API, you can <a href="https://api.nasa.gov/">check that out</a>. The APOD API is really, really simple, you just have to call the right endpoint and boom, it gives you nice json data.</p><p>We are going to do things the right way, because we want to learn how to use Flutter. So we are going to create two files, a <em>.env</em> file and a <em>.env.dist</em> file at the root of the project. We should add the <em>.env</em> file to our <em>.gitignore</em> file. In the <em>.env</em> file add the following variable</p><pre>API_URL=<a href="https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&amp;thumbs=true">https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&amp;thumbs=true</a></pre><p>The demo key limit you to 30 requests per IP address per hour and 50 requests per IP address per day. So it’s fine for our tutorial but if you want to create a real application based on this tutorial, you should get a real API key. You can notice that we add a parameter to the URL named “thumbs”. It tells the NASA server to give us thumbnails when the astronomical picture of the day is a … video. It will be useful to detect if the pictures of the day is a video, also it will be necessary to the set has wallpaper feature.</p><p>Now we need to tell our Flutter project to use the .env file. Go to your pubspec.yml file and add this line</p><pre>flutter:<br>   assets:<br>     - .env</pre><p>You can add all kind of file using this method (Image, Json…). Now we need something to parse our .env file. We are going to use a library named <a href="https://pub.dev/packages/flutter_dotenv">flutter_dotenv</a>. First go check the documentation to see what it’s all about. To install the plugin, add the dependency in the <em>pubspec.yml</em> file.</p><pre>dependencies:<br>  flutter:<br>    sdk: flutter</pre><pre>  flutter_dotenv: ^2.1.0</pre><p>Next you need to get the new dependency with this command</p><pre>flutter pub get</pre><p>Now you have add the flutter_dotenv to your project. Just import the plugin in your main file</p><pre>import &#39;package:flutter_dotenv/flutter_dotenv.dart&#39;;</pre><p>Now we can change the main function a bit to parse our <em>.env</em> file access the API URL</p><pre>void main() async {<br>  await DotEnv().load(&#39;.env&#39;);<br>  final apiUrl = DotEnv().env[&#39;API_URL&#39;];<br>  runApp(MyApp(apiUrl: apiUrl));<br>}</pre><p>We also need to change our <em>MyApp</em> widget a bit, add this at the beginning of the class</p><pre>MyApp({Key key, this.apiUrl}) : super(key: key);<br>final String apiUrl;</pre><p>It defines a <em>String</em> attribute named <em>apiUrl</em>, and set this attribute to the value pass in the constructor. You can add this</p><pre>print(apiUrl);</pre><p>At the start of the <em>build</em> method, to check the value of the <em>apiUrl</em>. Great, we have access to the right URL, now we can make an HTTP call to get the data ! Before that, just make a little clean up. We don’t need the <em>StatefullWidget</em>, so you can change</p><pre>MyHomePage extends StatefullWidget</pre><p>to</p><pre>MyHomePage extends StatelessWidget</pre><p>and remove the <em>State</em> property. You can also change the attribute definition to</p><pre>MyHomePage({Key key, <a href="http://twitter.com/required">@required</a> this.apiUrl}) : super(key: key);<br>  final String apiUrl;<br>  final title = &quot;APOD WALLPAPER&quot;;</pre><p>And of course pass the apiUrl to the <em>MyHomePage</em> object</p><pre>home: MyHomePage(apiUrl: apiUrl)</pre><p>Ok great ! Now we can make an HTTP call. We are going to use another plugin named <a href="https://pub.dev/packages/dio">dio</a>. It’s a high level HTTP library, it’s really the go-to solution to make an HTTP call, very easy to use with some helpers for testing. Remember, to add a plugin to our app, 3 steps. First add the dependency to the <em>pubspect.yml</em> file</p><pre>dependencies:<br>  flutter:<br>    sdk: flutter</pre><pre>  flutter_dotenv: ^2.1.0<br>  dio: ^3.0.10</pre><p>next</p><pre>flutter pub get</pre><p>and finally in our code</p><pre>import &#39;package:dio/dio.dart&#39;;</pre><p>Magnificent ! Now we can use this plugin in our app. We can add an attribute to our class, an attribute name <em>_dio</em>. This attribute will be a private member (cause of the _) and will contain a <em>Dio</em> object, the one we are going to use to make HTTP call</p><pre>final _dio = Dio();</pre><p>Let&#39;s write a function which returns the APOD website data.</p><pre>Future&lt;void&gt; _getApodData() async {<br> final json = await _dio.get(apiUrl);<br> print(json.data);<br> print(jYou can improve this app ! There is a lot of stuff to do. You can for example add the possibility to see the APOD content of a specific day or let the user set the picture as lock screen wallpaper. This is the GitHub repository, pull request are welcomed :)son.data.runtimeType.toString());<br>}</pre><p>Ok so this function is doing three things. First it is awaiting the result of the get method, called on the <em>Dio</em> object. We need to await this result because it’s a Future. In Dart, Future is the Promise concept implementation. It will return a value, and this value might be the wanted result or an error. Next we print our result, and the type of this result.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sQ56fONUfFVOU03lTD__YQ.png" /></figure><p>So <em>_InternalLinkedHashMap</em> is not a very useable type in Dart. We want to do things the right way. That means we should implement a model class which represent the APOD API data. To create a model class in lazy mode, we are going to use <a href="https://javiercbk.github.io/json_to_dart/">this website</a>. It’s a model generator, you pass json data, you get a Dart model. Perfect, let’s do it !</p><pre><em>class </em>Apod {<br>  String date;<br>  String explanation;<br>  String mediaType;<br>  String serviceVersion;<br>  String thumbnailUrl;<br>  String title;<br>  String url;<br><br>  Apod.fromJson(Map&lt;String, <em>dynamic</em>&gt; json) {<br>    date = json[&#39;date&#39;];<br>    explanation = json[&#39;explanation&#39;];<br>    mediaType = json[&#39;media_type&#39;];<br>    serviceVersion = json[&#39;service_version&#39;];<br>    thumbnailUrl = json[&#39;thumbnail_url&#39;];<br>    title = json[&#39;title&#39;];<br>    url = json[&#39;url&#39;];<br>  }<br><br>  Map&lt;String, <em>dynamic</em>&gt; toJson() {<br>    <em>final </em>Map&lt;String, <em>dynamic</em>&gt; data = <em>new </em>Map&lt;String, <em>dynamic</em>&gt;();<br>    data[&#39;date&#39;] = <em>this</em>.date;<br>    data[&#39;explanation&#39;] = <em>this</em>.explanation;<br>    data[&#39;media_type&#39;] = <em>this</em>.mediaType;<br>    data[&#39;service_version&#39;] = <em>this</em>.serviceVersion;<br>    data[&#39;thumbnail_url&#39;] = <em>this</em>.thumbnailUrl;<br>    data[&#39;title&#39;] = <em>this</em>.title;<br>    data[&#39;url&#39;] = <em>this</em>.url;<br>    <em>return </em>data;<br>  }<br>}</pre><p>Let’s put this in subdirectory named “models” in a file named <em>apod_model.dart</em>. Now we just need to change the <em>_getApodData</em> method</p><pre>Future&lt;Apod&gt; _getApodData() async {<br>    final json = await _dio.get(apiUrl);<br><br>    final data = jsonDecode(json.toString());<br>    final apod = Apod.fromJson(Map&lt;String, dynamic&gt;.from(data));<br>    <br>    _apod = apod;<br>    return apod;<br>  }</pre><p>We use a method to decode the json data (<em>jsonDecode</em>). We must import a package in order to use it. It’s a Dart native package. At the top of your <em>main.dart</em> file, add</p><pre>import &#39;dart:convert&#39;;</pre><p>You can notice that we are creating a <em>Map</em> with the static from method. We are casting our data to avoid problems. You can also notice the <em>_apod</em> class attribute, we are setting the value of this attribute to the value of our created <em>Apod</em> object. Spoiler, but we are going to use it later :), just add another attribute to the <em>MyHomePage</em> class</p><pre>Apod _apod;</pre><p>At this stage we have a perfect useable model, now it’s time to use it ! But there is a problem. We are getting data in an asynchronous way. So we don’t have the data when the app is launching. So how to we update our UI ? Use a state ! Ok, it might work but remember, we must use state as a last resort. The goal of state is to update the UI based on user interaction. In our case, the user doesn’t have any interaction, so we must not use state.</p><p>There is a lot of very helpful widget in Flutter and guess what, there is one to solve our problem. It’s called <em>FutureBuilder</em> and it takes two parameters. One named <em>future</em>, which is a function returning <em>Future</em>. The second parameter is <em>builder</em>, a callback function called to build the UI, depending on the state of the <em>Future</em>.</p><p>Let’s right our new <em>build</em> method !</p><pre>Widget build(BuildContext context) {<br>  <em>return </em>Scaffold(<br>    appBar: AppBar(<br>      title: Text(title),<br>    ),<br>    body: Center(<br>      child: SingleChildScrollView(child: Column(<br>        mainAxisAlignment: MainAxisAlignment.center,<br>        children: &lt;Widget&gt;[<br>          FutureBuilder&lt;Apod&gt;(<br>            future: _getApodData(),<br>            builder: (BuildContext context, AsyncSnapshot&lt;Apod&gt; snapshot) {<br>              Widget children;<br>              <em>if </em>(snapshot.hasData) {<br>                children = _formatData(snapshot: snapshot.data);<br>              } <em>else if </em>(snapshot.hasError) {<br>                children = _formatError();<br>              } <em>else </em>{<br>                children = _formatLoading();<br>              }<br><br>              <em>return </em>children;<br>            },<br>          )<br>        ],<br>      ),<br>    )),<br>  );<br>}</pre><p>Ok, what’s going on here ? First we render a <em>Scaffold</em>, this widget is the base of what to draw on the screen. Next we have a <em>Center</em> widget use to … center his child component. <em>SingleChildScrollView</em> allow the user to scroll the screen. It’s useful here because the explanation text might overflow the screen. Next we have <em>Column</em> component which takes a children property. This widget is useful when you want to render more than one child. In our case it’s useless yeah, because we only have the <em>FutureBuilder</em> has a child. We can change it with a <em>Container</em> widget for example. Finally, we have the <em>FutureBuilder</em>. If you just copy and paste the code your IDE should be warning you because he doesn’t know _formatData, _formatError and _formatLoading method. These functions render widget, depending on the state of the <em>Future</em>, represented by the <em>AsyncSnapshot&lt;Apod&gt;</em> object. Let’s be negative, we are going to right the <em>_formatError</em> function. This function render a widget to tell the user that an error has occurred.</p><pre>Container _formatError() {<br>  <em>return </em>Container(child: Icon(Icons.<em>signal_wifi_off </em>));<br>}</pre><p>Really simple it renders an <em>Icon</em> who tells the user, there is no network connection. Next, <em>_formatLoading</em> function render a widget to tell the user: Please wait, I’m loading data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*axXu2-dFUSjM2sL2JHfGYA.jpeg" /></figure><pre>Container _formatLoading() {<br>  <em>return </em>Container(child: CircularProgressIndicator());<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*HFO80aSHfUTyHe-h.gif" /></figure><p>Again really simple ! The <em>CircularProgressIndicator</em> is a built-in Flutter widget, very useful. We can customize it has we<a href="https://api.flutter.dev/flutter/material/CircularProgressIndicator-class.html"> want</a>. And last but certainly not least, the <em>_formatData</em> function</p><pre>Column _formatData({@required Apod snapshot}) {<br>  <em>return </em>Column(children: [<br>    Padding(<br>      padding: <em>const </em>EdgeInsets.all(20),<br>      child: Text.rich(<br>        TextSpan(<br>          text: snapshot.date + &quot;: &quot;,<br>          children: &lt;TextSpan&gt;[<br>            TextSpan(text: snapshot.title, style: TextStyle(fontWeight: FontWeight.<em>bold</em>, fontSize: 24)),<br>          ],<br>        ),<br>      ),<br>    ),<br>    _getMainContent(apod: snapshot),<br>    Padding(padding: <em>const </em>EdgeInsets.all(20),<br>    child: Text(snapshot.explanation))<br>  ]);<br>}</pre><p>Ok, you can notice the <em>@required</em> annotation and the in braces {} parameter. In braces implement named parameter. Now we can do square(x: x). But the default is named parameter are optional. So we add the required annotation to solve this issue.</p><p>Next we are rendering <em>Padding</em> widget, it’s a container who add padding to his child. Next we have a <em>_getMainContent</em> function, we will see that in a minute. And finally a basic text, wrapped inside a <em>Padding</em> widget. Notice how we use the <em>Text.rich</em> method to render a text compose by two <em>TextSpan</em></p><p>The main job happened in the <em>_getMainContent</em> method. Let’s see what going on out there</p><pre>Widget _getMainContent({@required Apod apod}) {<br>  <em>return </em>apod.isImage() ? Image.network(<br>      apod.url,<br>      loadingBuilder: (context, child, progress) {<br>        <em>return </em>progress == <em>null </em>? child : CircularProgressIndicator(<br>          value: progress.cumulativeBytesLoaded / progress.expectedTotalBytes,<br>        );<br>      }<br>  ) : YoutubePlayer(controller: _getVideoController(videoId: YoutubePlayer.<em>convertUrlToId</em>(apod.url)));<br>}</pre><p>We use a specific method to render the “main content” which is the picture or video of the day. We determine if it’s a video or an image with a method implemented on the model</p><pre>bool isImage() {<br>  <em>return </em>(thumbnailUrl == <em>null</em>);<br>}</pre><p>Simple, if we have a <em>thumbnailUrl</em>, that means it is a video. In this case we render a <em>YoutubePlayer</em>. It’s another plugin, now you now how to install plugin in Flutter, if you have some doubts, just follow the<a href="https://pub.dev/packages/youtube_player_flutter"> documentation</a>. A <em>YoutubePlayer</em> widget need to have a <em>YoutubePlayerController</em> widget. It’s used to get the video, add some option (autoplay, mute…). We render the controller with another method named <em>_getVideoController</em>, and yeah I like to split my code</p><pre>YoutubePlayerController _getVideoController({@required String videoId}) {<br>  <em>return </em>YoutubePlayerController(<br>    initialVideoId: videoId,<br>    flags: YoutubePlayerFlags(<br>      autoPlay: <em>false</em>,<br>    ),<br>  );<br>}</pre><p>Very simple, the hard job is done before by the <em>YoutubePlayer</em> plugin. The <em>convertUrlToId </em>method which take our <em>apod.url </em>and convert it to a <em>videoId</em>.</p><p>If the picture of the day is a real picture then we return an Image. We use the built-in Flutter widget named Image. We use a static method named <em>network </em>to get the image based on an URL. Next we add a <em>loadingBuilder </em>function to render a nice <em>CircularProgressIndicator </em>widget while getting the image data.</p><pre>loadingBuilder: (context, child, progress) {<br>  <em>return </em>progress == <em>null </em>? child : CircularProgressIndicator(<br>    value: progress.cumulativeBytesLoaded / progress.expectedTotalBytes,<br>  );<br>}</pre><p>Something useful here is the parameter named <em>progress</em>. It’s an <em>ImageChunkEvent </em>object. This object represents progress notifications while an image is being loaded. If our object is null, then it means that the image is fully load. In this case the <em>loadingBuilder</em> function return the child (the image). If it’s still loading, we use our <em>ImageChunkEvent </em>object to give value about the loading to our <em>CircularProgressIndicator </em>object. By specifying the value to <em>progress.cumulativeBytesLoaded / progress.expectedTotalBytes, </em>we tell the user when our Image will be loaded.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*krlLd5q0WLKJnwSz.gif" /></figure><p>We are almost finish. The last step is to implement the <em>setHasWallpaper</em> feature. We will need three very useful plugins. First<a href="https://pub.dev/packages/fluttertoast"> fluttertoast</a>, simplify how our app is displaying <em>Toast. </em>We are also going to use<a href="https://pub.dev/packages/flutter_cache_manager"> flutter_cache_manager</a> to download the picture of the day on disk. Finally<a href="https://pub.dev/packages/wallpaper_manager"> wallpaper_manager</a>, a plugin to set picture has wallpaper.</p><p>Now we are going to update our build method to render a <em>FloatingActionButton widget.</em></p><pre>Widget build(BuildContext context) {<br>  <em>return </em>Scaffold(<br>    appBar: AppBar(<br>      title: Text(title),<br>    ),<br>    body: Center(<br>      child: SingleChildScrollView(child: Column(<br>        mainAxisAlignment: MainAxisAlignment.center,<br>        children: &lt;Widget&gt;[<br>          FutureBuilder&lt;Apod&gt;(<br>            future: _getApodData(),<br>            builder: (BuildContext context, AsyncSnapshot&lt;Apod&gt; snapshot) {<br>              Widget children;<br>              <em>if </em>(snapshot.hasData) {<br>                children = _formatData(snapshot: snapshot.data);<br>              } <em>else if </em>(snapshot.hasError) {<br>                children = _formatError();<br>              } <em>else </em>{<br>                children = _formatLoading();<br>              }<br><br>              <em>return </em>children;<br>            },<br>          )<br>        ],<br>      ),<br>    )),<br>    floatingActionButton: FloatingActionButton(<br>      onPressed: () =&gt; _apod != <em>null </em>? _setHasWallpaper(context) : _showToast(&quot;Wait for data&quot;),<br>      child: Icon(Icons.<em>wallpaper</em>),<br>    ),<br>  );<br>}</pre><p><em>floatingActionButton</em> is a <em>Scaffold</em> property, used to display a<a href="https://material.io/components/buttons-floating-action-button#usage"> FAB</a>. Notice how simple we do to add an <em>Icon</em> to this button. We bind the <em>onPressed </em>event to a function executing a specific function, depending on our <em>_apod</em> class attribute. If the <em>MyHomePage</em> object hasn’t fetch the APOD API wet we just display a toast to the user with this method</p><pre><em>void </em>_showToast(String result) {<br>  Fluttertoast.<em>showToast</em>(<br>      msg: result,<br>      toastLength: Toast.LENGTH_SHORT,<br>      gravity: ToastGravity.CENTER,<br>      timeInSecForIosWeb: 1,<br>      backgroundColor: Colors.<em>grey</em>,<br>      textColor: Colors.<em>white</em>,<br>      fontSize: 16.0<br>  );<br>}</pre><p>Here we use the<a href="https://pub.dev/packages/fluttertoast"> fluttertoast</a> plugin. It’s very simple and straight forward. And finally the <em>_setHasWallpaper </em>function.</p><pre>Future&lt;<em>void</em>&gt; _setHasWallpaper(BuildContext context) <em>async </em>{<br>  <em>final </em>file = <em>await </em>DefaultCacheManager().getSingleFile(_apod.getWallpaperUrl());<br>  <em>final </em>String result = <em>await </em>WallpaperManager.<em>setWallpaperFromFile</em>(file.path, WallpaperManager.<em>HOME_SCREEN</em>);<br><br>  _showToast(result);<br>}</pre><p>So first we are downloading the APOD picture with the<a href="https://pub.dev/packages/flutter_cache_manager"> flutter_cache_manager</a> plugin. Next we set the downloaded file as wallpaper thanks to the<a href="https://pub.dev/packages/wallpaper_manager"> wallpaper_manager</a> plugin. Very simple !</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*iEq7N4rMR1A7a-gE.gif" /></figure><p>With all this you have now a good overview of how to use Flutter plus a cool app. If you want to compile this code to an iOS application you can ! Just make sure to follow the iOS set up of the<a href="https://pub.dev/packages/youtube_player_flutter"> youtube_player_flutter</a> plugin.</p><p>You can improve this app ! There is a lot of stuff to do. You can for example add the possibility to see the APOD content of a specific day or let the user set the picture as lock screen wallpaper. This is the<a href="https://github.com/Pierre-Monier/apod_wallpaper"> GitHub repository</a>, pull request are welcomed :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f3786f5e8cdd" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>