<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Emilio Schepis</title>
    <description>The latest articles on DEV Community by Emilio Schepis (@emilioschepis).</description>
    <link>https://dev.to/emilioschepis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F354531%2Fbdb15f61-fde3-4e62-bcd3-46fe40e88685.jpg</url>
      <title>DEV Community: Emilio Schepis</title>
      <link>https://dev.to/emilioschepis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emilioschepis"/>
    <language>en</language>
    <item>
      <title>Developing Geonotes — Creating new notes — Ep. 4</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Fri, 06 Aug 2021 19:56:47 +0000</pubDate>
      <link>https://dev.to/emilioschepis/developing-geonotes-creating-new-notes-ep-4-7ji</link>
      <guid>https://dev.to/emilioschepis/developing-geonotes-creating-new-notes-ep-4-7ji</guid>
      <description>&lt;p&gt;Now that we can display notes in a pretty way (read more in &lt;a href="https://dev.to/emilioschepis/developing-geonotes-animations-and-interactions-ep-3-1f9"&gt;Episode 3&lt;/a&gt;), it is time to let the users create their own notes.&lt;/p&gt;

&lt;p&gt;This will mainly be a technical post, since much of the changes are business logic related.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Custom business logic with Actions
&lt;/h2&gt;

&lt;p&gt;As I anticipated in the last episode, I decided to only let users create notes by invoking a  &lt;a href="https://hasura.io/docs/latest/graphql/core/actions/index.html"&gt;Hasura Action&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What this does, is allow you to have a GraphQL interface (like you would have for database queries and mutations) to a serverless function.&lt;/p&gt;

&lt;p&gt;In Geonotes' case, the actions will call a &lt;a href="https://firebase.google.com/products/functions"&gt;Firebase Cloud Function&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first step is to define the action's input and output in the Hasura console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--wO2ZuhuW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1628059782931/624nlFm3X.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wO2ZuhuW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1628059782931/624nlFm3X.png" alt="Screen Shot 2021-08-04 at 08.49.16.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can then define which endpoint is responsible for handling the action.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔥 Handling the action with a Cloud Function
&lt;/h2&gt;

&lt;p&gt;The action sends a &lt;code&gt;POST&lt;/code&gt; request to the specified endpoint, so the function can extract session variables and the various parameters passed as input.&lt;/p&gt;

&lt;p&gt;To continue the type-safety first approach I created a small helper that, before running any code, verifies the authenticity of the request and that the parameters are valid. &lt;a href="https://github.com/emilioschepis/geonotes-backend/blob/02fe4bc579fee54909d7d56e7930562e74db0524/functions/src/utils/actionsHelpers.ts#L12-L47"&gt;Source&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actionWrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;O&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActionHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;O&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;O&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;ErrorOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session_variables&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-hasura-user-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Actions should be performed by a specific user. No "x-hasura-user-id" was provided.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;O&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ErrorOutput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this wrapper, actions now have a much simpler interface for sending the responses or errors in the supported format. &lt;a href="https://github.com/emilioschepis/geonotes-backend/blob/02fe4bc579fee54909d7d56e7930562e74db0524/functions/src/handlers/createNoteActionHandler.ts#L7"&gt;Source&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action is responsible of actually mutating the database with higher permissions, and then returning the id of the just created note.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⭐️ The result
&lt;/h2&gt;

&lt;p&gt;I quickly wired up an empty screen with a button to the map screen, so that notes can be created wherever the user is. For now, the text is static as I only needed to test the business logic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/emilioschepis/status/1422816262359498752"&gt;Watch a short video of a note being created&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🚧 Next steps
&lt;/h2&gt;

&lt;p&gt;As I said in one of the first episodes, notes can only be created by signed in users. Right now, I created a quick account just to have the permissions to invoke the action.&lt;/p&gt;

&lt;p&gt;The next step is to add an actual authentication flow to the app, so that different users can create their own notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎙 How to follow the project
&lt;/h2&gt;

&lt;p&gt;I'll be posting updates throughout the development process and as I learn new thing regarding development, design, and marketing.&lt;/p&gt;

&lt;p&gt;If you'd like to have even more real-time updates you can&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/emilioschepis"&gt;Follow me on Twitter @emilioschepis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Checkout the public GitHub &lt;a href="https://github.com/emilioschepis/geonotes-app/tree/develop"&gt;app repository&lt;/a&gt; and &lt;a href="https://github.com/emilioschepis/geonotes-backend/tree/develop"&gt;backend repository&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>coding</category>
    </item>
    <item>
      <title>Developing Geonotes — Animations and interactions — Ep. 3</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Wed, 04 Aug 2021 09:13:48 +0000</pubDate>
      <link>https://dev.to/emilioschepis/developing-geonotes-animations-and-interactions-ep-3-1f9</link>
      <guid>https://dev.to/emilioschepis/developing-geonotes-animations-and-interactions-ep-3-1f9</guid>
      <description>&lt;p&gt;With the notes being displayed around the user that we discussed in &lt;a href="https://dev.to/emilioschepis/developing-geonotes-maps-and-the-postgis-extension-ep-2-3i9j"&gt;Episode 2&lt;/a&gt;, it's now time to add a bit of interesting UI and effects.&lt;/p&gt;

&lt;p&gt;This will be a bit of a shorter episode, since all the changes were made in a few hours after work.&lt;/p&gt;

&lt;h2&gt;
  
  
   ✨ The note-opening effect
&lt;/h2&gt;

&lt;p&gt;I decided to move as much information outside the marker callout as possible. It now only shows the first few words of the note and a "view" call to action.&lt;/p&gt;

&lt;p&gt;When the user taps on the callout, the note itself appears like a modal with a dark transparent background. The modal presents a post-it-like note with the content in the center. Tapping on the note starts a flip animation just like you were watching the back of the note, where the username, time and date are displayed.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://github.com/react-native-modal/react-native-modal"&gt;React Native Modal&lt;/a&gt; to achieve the modal effect, and &lt;a href="https://github.com/software-mansion/react-native-reanimated"&gt;Reanimated 2&lt;/a&gt; for the flip effect.&lt;/p&gt;

&lt;p&gt;I haven't played much with animations before, but the API seems very straightforward! I also took a lot of inspiration from &lt;a href="https://blog.swmansion.com/building-reigns-how-to-strengthen-reign-over-mobile-game-development-using-reanimated-2-66c127a210e9"&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🕸 Taking advantage of GraphQL
&lt;/h2&gt;

&lt;p&gt;Using GraphQL with Hasura allowed me to make the notes-around-me query even lighter by removing information about the user and the creation date without having to modify any backend code.&lt;/p&gt;

&lt;p&gt;I then added a new query that fetches all the data of a single note by passing the id. &lt;a href="https://github.com/emilioschepis/geonotes-app/blob/86e8bd500536ce5f8a615912b4f346bb0cb0cf3d/src/graphql/queries.graphql#L18-L27"&gt;Source&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;note_by_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⭐️ The result
&lt;/h2&gt;

&lt;p&gt;In the end I was able to achieve this nice-looking effect!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Front&lt;/th&gt;
&lt;th&gt;Animating&lt;/th&gt;
&lt;th&gt;Back&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--otwZl7Wa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1627969722714/afoMvZxNv.png" alt="Screen Shot 2021-08-03 at 07.48.15.png"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JHmUngRe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1627969967427/kW3ZJ2hsg.png" alt="Screen Shot 2021-08-03 at 07.51.05.png"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4S3U1lvk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1627969971522/3v1UrnE1x.png" alt="Screen Shot 2021-08-03 at 07.51.11.png"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://twitter.com/emilioschepis/status/1422286864879325184"&gt;And here's the animation in action!&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🚧 Next steps
&lt;/h2&gt;

&lt;p&gt;The next step to tackle will be a big one: creating a new note. I want to implement it using &lt;a href="https://hasura.io/docs/latest/graphql/core/actions/index.html"&gt;Hasura Actions&lt;/a&gt; right away, to have more control over the business logic and to perform custom checks!&lt;/p&gt;

&lt;h2&gt;
  
  
  🎙 How to follow the project
&lt;/h2&gt;

&lt;p&gt;I'll be posting updates throughout the development process and as I learn new thing regarding development, design, and marketing.&lt;/p&gt;

&lt;p&gt;If you'd like to have even more real-time updates you can&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/emilioschepis"&gt;Follow me on Twitter @emilioschepis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Checkout the public GitHub &lt;a href="https://github.com/emilioschepis/geonotes-app/tree/develop"&gt;app repository&lt;/a&gt; and &lt;a href="https://github.com/emilioschepis/geonotes-backend/tree/develop"&gt;backend repository&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>typescript</category>
      <category>coding</category>
    </item>
    <item>
      <title>Developing Geonotes — Maps and the PostGIS extension — Ep. 2</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Tue, 03 Aug 2021 05:36:23 +0000</pubDate>
      <link>https://dev.to/emilioschepis/developing-geonotes-maps-and-the-postgis-extension-ep-2-3i9j</link>
      <guid>https://dev.to/emilioschepis/developing-geonotes-maps-and-the-postgis-extension-ep-2-3i9j</guid>
      <description>&lt;p&gt;With the local infrastructure completed in &lt;a href="https://dev.to/emilioschepis/developing-geonotes-adding-authentication-and-connecting-to-graphql-ep-1-48ep"&gt;Episode 1&lt;/a&gt;, it is time to add the main map to Geonotes.&lt;/p&gt;

&lt;h2&gt;
  
  
   📱 Adding a map to the main screen
&lt;/h2&gt;

&lt;p&gt;On the client, the first step was to add a map to the main screen. Fortunately, Expo supports a library out of the box: &lt;a href="https://docs.expo.dev/versions/latest/sdk/map-view/"&gt;React Native Maps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With this library, a &lt;code&gt;MapKit&lt;/code&gt; instance is created on iOS, while Android uses Google Maps. The props interface is the same across both platforms.&lt;/p&gt;

&lt;p&gt;One thing I really like about RNM is that it has built-in support for requesting permissions. Originally I thought I'd do it with &lt;a href="https://docs.expo.dev/versions/latest/sdk/location/"&gt;Expo Location&lt;/a&gt; but so far I haven't felt the need to switch to a dedicated location library.&lt;/p&gt;

&lt;p&gt;RNM is able to request the current location of the user, and then display it directly on the map. Adding the &lt;code&gt;followsUserLocation&lt;/code&gt; prop and blocking the various movements, I was able to center the map on the user and update the visible region when they move.&lt;/p&gt;

&lt;p&gt;This also allows me to listen to the &lt;code&gt;onRegionChangeComplete&lt;/code&gt; event and extract the current location.&lt;/p&gt;

&lt;p&gt;In order not to perform too many queries against the database, the current location is only updated when the user moves more than a given distance from the last location.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌍 Measuring the distance client-side and in queries
&lt;/h2&gt;

&lt;p&gt;To measure the distance between the last and the current location of the user, I decided to use the "&lt;a href="https://www.movable-type.co.uk/scripts/latlong.html#equirectangular"&gt;Equirectangular approximation&lt;/a&gt;" that should have plenty of precision over relatively small distances.&lt;/p&gt;

&lt;p&gt;The implementation is as follows &lt;a href="https://github.com/emilioschepis/geonotes-app/blob/cf86d2272e5d81e14027158646e667af05116eea/src/utils/locationUtils.ts#L3-L30"&gt;Source&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6371&lt;/span&gt;&lt;span class="nx"&gt;e3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Earth's radius&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lat1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lon1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lon2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaLat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deltaLon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lon2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lon1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;deltaLon&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;lat1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;deltaLat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server, however, I needed something more battle-tested and performant.&lt;/p&gt;

&lt;p&gt;Since Hasura uses PostgreSQL as a database, I decided to use the &lt;a href="https://postgis.net/"&gt;PostGIS&lt;/a&gt; extension. Another option I have considered is the GeoHash algorithm, but PostGIS has a much better integration with the current stack.&lt;/p&gt;

&lt;p&gt;With PostGIS enabled, I set the "location" column in the "note" table as &lt;code&gt;geography&lt;/code&gt;, which allows me to perform queries like this &lt;a href="https://github.com/emilioschepis/geonotes-app/blob/2331ef3a678c88e8924c36c6c2ab5b30c08c1913/src/graphql/queries.graphql#L1-L16"&gt;Source&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Notes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;_st_d_within&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$distance&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Point"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$latitude&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⭐️ The result
&lt;/h2&gt;

&lt;p&gt;Ultimately, I was able to query notes in a range around the user, and have those notes update as the user moved.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First location&lt;/th&gt;
&lt;th&gt;Second location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hBqHO7pj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1627890454908/ftw3ugSLh.png" alt="Screen Shot 2021-08-02 at 09.46.22.png"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--64itm96U--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1627890466494/IU9IKGYjG.png" alt="Screen Shot 2021-08-02 at 09.46.11.png"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🚧 Next steps
&lt;/h2&gt;

&lt;p&gt;Now it is time to work a bit on the presentation of the various notes. My current plan is to show a small callout when a note is tapped, and then display a bottom sheet with the full information / actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎙 How to follow the project
&lt;/h2&gt;

&lt;p&gt;I'll be posting updates throughout the development process and as I learn new thing regarding development, design, and marketing.&lt;/p&gt;

&lt;p&gt;If you'd like to have even more real-time updates you can&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/emilioschepis"&gt;Follow me on Twitter @emilioschepis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Checkout the public GitHub &lt;a href="https://github.com/emilioschepis/geonotes-app/tree/develop"&gt;app repository&lt;/a&gt; and &lt;a href="https://github.com/emilioschepis/geonotes-backend/tree/develop"&gt;backend repository&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>graphql</category>
      <category>coding</category>
    </item>
    <item>
      <title>Developing Geonotes — Adding authentication and connecting to GraphQL — Ep. 1</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Sun, 01 Aug 2021 16:58:56 +0000</pubDate>
      <link>https://dev.to/emilioschepis/developing-geonotes-adding-authentication-and-connecting-to-graphql-ep-1-48ep</link>
      <guid>https://dev.to/emilioschepis/developing-geonotes-adding-authentication-and-connecting-to-graphql-ep-1-48ep</guid>
      <description>&lt;p&gt;After writing &lt;a href="https://dev.to/emilioschepis/developing-geonotes-a-location-based-app-made-with-expo-react-native-hasura-graphql-and-firebase-ep-0-42eb"&gt;Episode 0&lt;/a&gt; I started working on Geonotes right away.&lt;/p&gt;

&lt;p&gt;The goal was to setup a solid environment to serve as foundation for all future work.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏠 Local by default
&lt;/h2&gt;

&lt;p&gt;To increase the speed of iteration on all features and to have more control over all of the infrastructure I decided to go with a local-first approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Expo
&lt;/h3&gt;

&lt;p&gt;This is the default way of working with Expo. Starting the project launches the React Native packager on the local machine and all of the devices / simulators connect directly to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Firebase
&lt;/h3&gt;

&lt;p&gt;One of the features I love most about Firebase (and that I would really like to see more PaaS providers offering) is a suite of &lt;a href="https://firebase.google.com/docs/emulator-suite"&gt;local emulators&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These emulators have support for basically all Firebase services. Local apps can connect to the authentication emulator to create accounts, sign in and generate tokens. There is no difference between connecting to an actual Firebase project and a local environment.*&lt;/p&gt;

&lt;p&gt;Cloud functions are also emulated on the host machine, resulting in very fast execution times that are great for iterating on the different functionalities.&lt;/p&gt;

&lt;p&gt;*: the JWT token provided by the Auth emulator is not signed, so Hasura does not accept it as a form of authentication. There is a &lt;a href="https://github.com/hasura/graphql-engine/issues/6338"&gt;GitHub issue&lt;/a&gt; on accepting the &lt;code&gt;"none"&lt;/code&gt; algorithm, but for the moment I had to implement a &lt;a href="https://github.com/emilioschepis/geonotes-app/blob/084e34485dd2677873f4d1d0816f2ac9110a4b43/src/utils/authUtils.ts#L64-L75"&gt;local-only HS256 signing&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Hasura
&lt;/h3&gt;

&lt;p&gt;Hasura works great with a local environment. You can download a Docker compose file directly from their repository and it will take care of setting up a local PostgreSQL database and the GraphQL engine.&lt;/p&gt;

&lt;p&gt;Using the &lt;a href="https://hasura.io/docs/latest/graphql/core/hasura-cli/index.html"&gt;Hasura CLI&lt;/a&gt; I can launch a local instance of the console. Working from this console has the benefit of automatically generating migrations and metadata that can be later applied to the production environment. I'll talk more about this once I set up the various build pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔐 Getting started with authentication
&lt;/h2&gt;

&lt;p&gt;Authentication will be a crucial part of the application infrastructure, and ideally every access should be at least partially authenticated.&lt;/p&gt;

&lt;p&gt;From the experience I've gained with several past apps, asking the user to create an account before being able to use the app once leads to a very low retention rate — and for good reason!&lt;/p&gt;

&lt;p&gt;This time I decided to use an hybrid approach. I still don't want to allow unauthenticated requests to my backend, since it can be easily exploited by people trying to bring the platform down. While this should be heavily mitigated by hosting everything on massive public cloud providers (Google and Heroku, or AWS), I'd still like to have this extra layer of security.&lt;/p&gt;

&lt;p&gt;For this reason, any user that opens the app for the first time automatically signs in with an &lt;a href="https://firebase.google.com/docs/auth/web/anonymous-auth"&gt;anonymous account&lt;/a&gt;. This auth provider receives fewer grants (for example, they can only read notes and not create their own) and the user can then decide to create a "regular" account to access all of the features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client side auth
&lt;/h3&gt;

&lt;p&gt;On the client, when no user is authenticated, an anonymous sign in is invoked. &lt;a href="https://github.com/emilioschepis/geonotes-app/blob/7ea44de9ea9aa8a72c8577713572bec9e1605f21/src/context/AuthContext.tsx#L36-L51"&gt;Source&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;firebase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;onAuthStateChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;firebase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;signInAnonymously&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;setAuthenticated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isAnonymous&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Server side auth
&lt;/h3&gt;

&lt;p&gt;On the server, when a new user is created (either anonymously or with a provider), the appropriate claims are applied. &lt;a href="https://github.com/emilioschepis/geonotes-backend/blob/8f286e62a122c28a8da66551f97b21fe8f03a724/functions/src/handlers/onCreateUserHandler.ts#L9-L19"&gt;Source&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAnonymous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;providerData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customUserClaims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://hasura.io/jwt/claims&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-hasura-default-role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isAnonymous&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-hasura-allowed-roles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isAnonymous&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-hasura-user-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;setCustomUserClaims&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customUserClaims&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ☂️ Type-safety first
&lt;/h2&gt;

&lt;p&gt;As I anticipated in &lt;a href="https://dev.to/emilioschepis/developing-geonotes-a-location-based-app-made-with-expo-react-native-hasura-graphql-and-firebase-ep-0-42eb"&gt;Episode 0&lt;/a&gt;, I want to make sure that as much code as possible is both type-safe and autogenerated from the database schema. This will result in the code being more reliable and standardized.&lt;/p&gt;

&lt;p&gt;For this reason, both client- and server-side code have their GraphQL interface autogenerated by &lt;a href="https://www.graphql-code-generator.com/"&gt;GraphQL Code Generator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I decided to keep the generated files in version control. A lot of code will depend on the generated interface to work properly, and I prefer it to use a fixed version rather than running the codegen on-demand.&lt;/p&gt;

&lt;p&gt;The generator's plugins allowed me to also create type-safe hooks for the app and a custom sdk for the cloud functions.&lt;/p&gt;

&lt;p&gt;The code generation is launched only in the local environment, that downloads the current schema from the Hasura instance. To better leverage Hasura's access control system, the app receives only the schema that applies to users, while the cloud functions receive the schema that applies to the "backend" role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080/v1/graphql"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Hasura-Admin-Secret"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${HASURA_GRAPHQL_ADMIN_SECRET}&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Hasura-Role"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend"&lt;/span&gt;
&lt;span class="na"&gt;documents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;src/graphql/**/queries.graphql&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;src/graphql/**/mutations.graphql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚧 Next steps
&lt;/h2&gt;

&lt;p&gt;Next, I'll be working on adding a navigation scaffolding to the app, and on integrating the first empty map.&lt;/p&gt;

&lt;p&gt;With everything else in place, I should be able to show custom markers on the app depending on the notes saved on the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎙 How to follow the project
&lt;/h2&gt;

&lt;p&gt;I'll be posting updates throughout the development process and as I learn new thing regarding development, design, and marketing.&lt;/p&gt;

&lt;p&gt;If you'd like to have even more real-time updates you can&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/emilioschepis"&gt;Follow me on Twitter @emilioschepis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Checkout the public GitHub &lt;a href="https://github.com/emilioschepis/geonotes-app/tree/develop"&gt;app repository&lt;/a&gt; and &lt;a href="https://github.com/emilioschepis/geonotes-backend/tree/develop"&gt;backend repository&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>grapqhl</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Developing Geonotes, a location-based app made with Expo React Native, Hasura GraphQL, and Firebase — Ep. 0</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Sun, 01 Aug 2021 06:18:56 +0000</pubDate>
      <link>https://dev.to/emilioschepis/developing-geonotes-a-location-based-app-made-with-expo-react-native-hasura-graphql-and-firebase-ep-0-42eb</link>
      <guid>https://dev.to/emilioschepis/developing-geonotes-a-location-based-app-made-with-expo-react-native-hasura-graphql-and-firebase-ep-0-42eb</guid>
      <description>&lt;p&gt;It's in the nature of many developers to come up with a side project every once in a while. Be it to keep up with the latest changes in the industry, to scratch a creative itch, or to create &lt;em&gt;the next big thing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For me, this time, the reason is to explore new platforms in a production-environment while hopefully creating something that can make people feel more connected to others and to the places they visit.&lt;/p&gt;

&lt;h2&gt;
  
  
  🗺 What is Geonotes?
&lt;/h2&gt;

&lt;p&gt;Geonotes is a location-based multi-platform mobile app where users can leave little text notes wherever they are, and read all the notes that other users have left around them.&lt;/p&gt;

&lt;p&gt;I can see this working especially well when visiting a beautiful place, leaving a note where you spent a holiday with your loved ones or to save the time and place of a special event. Even better, reading the notes left by other people can make us feel closer to what we're experiencing and the world around us.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Some technical details
&lt;/h2&gt;

&lt;p&gt;The application will be developed with the &lt;a href="https://reactnative.dev/"&gt;React Native&lt;/a&gt; framework, using &lt;a href="https://expo.dev/"&gt;Expo&lt;/a&gt; so that I can focus more on the actual code and less on the platform-specific code.&lt;/p&gt;

&lt;p&gt;With Expo I'll be able to distribute early versions of the application more easily, and ideally I'll be able to use the upcoming &lt;a href="https://expo.dev/eas"&gt;Expo's EAS&lt;/a&gt; to create a more robust build &amp;amp; delivery pipeline later down the road.&lt;/p&gt;

&lt;p&gt;The backend will be built on the open-source &lt;a href="https://hasura.io/"&gt;Hasura&lt;/a&gt; GraphQL engine and will be initially hosted on &lt;a href="https://www.heroku.com/"&gt;Heroku&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Authentication, file storage and serverless functions will be provided by the &lt;a href="https://firebase.google.com/"&gt;Firebase&lt;/a&gt; platform that I have used in the past for many side projects.&lt;/p&gt;

&lt;p&gt;I am a firm believer in the benefits of a strongly-typed approach. Both the frontend and the backend will be in Node.js and written in TypeScript.&lt;/p&gt;

&lt;p&gt;Hasura already does a fantastic job in providing a stable and type-safe GraphQL interface to the underlying PostgreSQL database, and I want to take further advantage of it by having all of the client code be auto-generated directly from the API's schema. I'll use &lt;a href="https://www.graphql-code-generator.com/"&gt;GraphQL Code Generator&lt;/a&gt; for that.&lt;/p&gt;

&lt;p&gt;To connect to the API I'll use the &lt;a href="https://www.apollographql.com/docs/react/"&gt;Apollo client&lt;/a&gt; in the application and the simpler &lt;a href="https://github.com/prisma-labs/graphql-request"&gt;GraphQL request&lt;/a&gt; library in the serverless functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚧 The first few steps
&lt;/h2&gt;

&lt;p&gt;My idea for this project is to build it completely in the open. This means public GitHub repositories and written updates whenever the project reaches any big or small milestones. I really hope you'll be along for the ride.&lt;/p&gt;

&lt;p&gt;The first step will be to create a proof of concept to validate early my choice of technologies and platforms.&lt;/p&gt;

&lt;p&gt;To keep track of the tasks I'll be using &lt;a href="https://notion.so"&gt;Notion&lt;/a&gt;'s Kanban feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---bKTfRwK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1627729319179/vesbbpKFP.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---bKTfRwK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.hashnode.com/res/hashnode/image/upload/v1627729319179/vesbbpKFP.png" alt="Screen Shot 2021-07-31 at 13.01.46.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎙 How to follow the project
&lt;/h2&gt;

&lt;p&gt;I'll be posting updates throughout the development process and as I learn new thing regarding development, design, and marketing.&lt;/p&gt;

&lt;p&gt;If you'd like to have even more real-time updates you can&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/emilioschepis"&gt;Follow me on Twitter @emilioschepis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Checkout the public GitHub &lt;a href="https://github.com/emilioschepis/geonotes-app/tree/develop"&gt;app repository&lt;/a&gt; and &lt;a href="https://github.com/emilioschepis/geonotes-backend/tree/develop"&gt;backend repository&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>typescript</category>
      <category>graphql</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Adding continuous integration to a Flutter project with GitHub Actions</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Sat, 14 Nov 2020 11:05:30 +0000</pubDate>
      <link>https://dev.to/emilioschepis/adding-continuous-integration-to-a-flutter-project-with-github-actions-48p1</link>
      <guid>https://dev.to/emilioschepis/adding-continuous-integration-to-a-flutter-project-with-github-actions-48p1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Whenever you start a new project, either by yourself or as part of a team, it is important to define a set of rules to adhere to and to select tools that can automate the process of detecting if and when those rules are broken.&lt;/p&gt;

&lt;p&gt;One such tool is the recently-announced &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this post I will show you how to setup a workflow to analyze, lint and test your Flutter project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;A workflow is a set of steps that are executed whenever an event happens on your repository. Common triggers for these actions are pushes or pull requests on a given branch.&lt;/p&gt;

&lt;p&gt;Workflows must be stored in the &lt;code&gt;.github/workflows/&lt;/code&gt; directory in your project. A single workflow is defined by a YAML file that can be named however we want.&lt;/p&gt;

&lt;p&gt;For more information about Actions I highly recommend going through the extensive &lt;a href="https://docs.github.com/en/free-pro-team@latest/actions"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A really nice feature of GitHub Actions is its generous free tier: you can run actions on Linux machines for up to 2000 minutes / month for free. If your project is open source (on a public repo), Actions are always free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up a CI workflow for Flutter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 - Setting up Pedantic on your project (Optional)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://pub.dev/packages/pedantic"&gt;Pedantic&lt;/a&gt; is a Dart package created by Google that provides the extensive set of strict rules that Google itself uses in their projects.&lt;/p&gt;

&lt;p&gt;You can add it as a development dependency to your &lt;code&gt;pubspec.yaml&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dev_dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pedantic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.9.0&lt;/span&gt; &lt;span class="c1"&gt;# check the latest version on the package's page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then define the set of rules that Pedantic applies adding a &lt;code&gt;analysis_options.yaml&lt;/code&gt; to the root of our project.&lt;br&gt;
To use Google's rules without overriding anything, the file is as straightforward as one line.&lt;br&gt;
Since we will be analyzing the project in our CI environment, we will add three more lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;package:pedantic/analysis_options.1.9.0.yaml&lt;/span&gt;

&lt;span class="na"&gt;analyzer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;flutter/**&lt;/span&gt; &lt;span class="c1"&gt;# Do not analyze the Flutter repository in the CI environment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is needed because the GitHub Action will download a copy of the Flutter framework in the same directory of our project, making the Analysis step check the whole Flutter project, dramatically increasing the action duration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 - Setting up the CI workflow file
&lt;/h3&gt;

&lt;p&gt;Create a new workflow file in the workflows directory. For example you might create &lt;code&gt;.github/workflows/ci.yaml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt; &lt;span class="c1"&gt;# (1)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Flutter&lt;/span&gt; &lt;span class="c1"&gt;# (2)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git clone https://github.com/flutter/flutter.git --depth 1&lt;/span&gt;
          &lt;span class="s"&gt;echo "$GITHUB_WORKSPACE/flutter/bin" &amp;gt;&amp;gt; $GITHUB_PATH&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Checks&lt;/span&gt; &lt;span class="c1"&gt;# (3)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;flutter pub get&lt;/span&gt;
          &lt;span class="s"&gt;flutter format lib/** --set-exit-if-changed&lt;/span&gt;
          &lt;span class="s"&gt;flutter analyze --no-pub&lt;/span&gt;
          &lt;span class="s"&gt;flutter test --no-pub&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go through this file point by point:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;This Action, provided by GitHub, clones our project on the workflow machine&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This step clones the latest commit of the Flutter framework on the workflow machine and sets the PATH so that we can use &lt;code&gt;flutter&lt;/code&gt; commands&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you added Pedantic in Step 1, this is why we needed to exclude the flutter directory from the analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This step runs the actual checks on your code&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downloads all of your project's dependencies&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;dartfmt&lt;/code&gt; to check your code's formatting (failing if it not properly formatted)&lt;/li&gt;
&lt;li&gt;Analyzes it with Pedantic (highlighting broken rules)&lt;/li&gt;
&lt;li&gt;Runs all tests (both unit tests and headless widget tests)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;Every push on the &lt;code&gt;main&lt;/code&gt; branch will now trigger this CI action, providing quick and precise feedback on possible problems in the codebase.&lt;br&gt;
You can enhance this simple workflow with anything you might want to track (adding code coverage, build steps for each platform, and so on).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Having an automated process that checks your project's health at every push can be invaluable, especially when paired with extensive tests.&lt;/p&gt;

&lt;p&gt;I really hope this will prove useful in your Flutter project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thank you for reading!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>github</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Adding a statically-generated RSS feed to a Next.js 9.3+ Blog</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Sat, 12 Sep 2020 22:43:58 +0000</pubDate>
      <link>https://dev.to/emilioschepis/adding-a-statically-generated-rss-feed-to-a-next-js-9-3-blog-58id</link>
      <guid>https://dev.to/emilioschepis/adding-a-statically-generated-rss-feed-to-a-next-js-9-3-blog-58id</guid>
      <description>&lt;p&gt;Learn how to expose the required files to make your blog RSS-compatible while mantaining full static generation on Next.js 9.3+.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Today I updated my website to support RSS reader apps and services.&lt;/p&gt;

&lt;p&gt;My goals were to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Completely support RSS readers as defined by the &lt;a href="https://validator.w3.org/feed/"&gt;W3C Feed Validation Service&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Keep full static generation working&lt;/li&gt;
&lt;li&gt;Make this a fully automatic step without modifying the build and / or bundle configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Generating the necessary XML
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Note: I already have a method that parses Markdown files to extract metadata about my blog posts. &lt;a href="https://github.com/emilioschepis/website/blob/main/lib/posts.ts"&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To implement this feature I created two functions: &lt;code&gt;generateRssItem&lt;/code&gt; and &lt;code&gt;generateRss&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first function generates the necessary XML to describe a single blog post according to the &lt;a href="https://validator.w3.org/feed/docs/rss2.html#hrelementsOfLtitemgt"&gt;specification&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateRssItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="err"&gt;
  &amp;lt;item&amp;gt;
    &amp;lt;guid&amp;gt;https://emilioschepis.com/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/guid&amp;gt;
    &amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/title&amp;gt;
    &amp;lt;link&amp;gt;https://emilioschepis.com/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/link&amp;gt;
    &amp;lt;description&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/description&amp;gt;
    &amp;lt;pubDate&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/pubDate&amp;gt;
  &amp;lt;/item&amp;gt;
&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second function generates the necessary XML to describe the whole "channel":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateRss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="err"&gt;
  &amp;lt;rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"&amp;gt;
    &amp;lt;channel&amp;gt;
      &amp;lt;title&amp;gt;Blog - Emilio Schepis&amp;lt;/title&amp;gt;
      &amp;lt;link&amp;gt;https://emilioschepis.com/blog&amp;lt;/link&amp;gt;
      &amp;lt;description&amp;gt;[...]&amp;lt;/description&amp;gt;
      &amp;lt;language&amp;gt;en&amp;lt;/language&amp;gt;
      &amp;lt;lastBuildDate&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/lastBuildDate&amp;gt;
      &amp;lt;atom:link href="https://emilioschepis.com/rss.xml" rel="self" type="application/rss+xml"/&amp;gt;
      &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generateRssItem&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="err"&gt;
    &amp;lt;/channel&amp;gt;
  &amp;lt;/rss&amp;gt;
&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding the generated XML to the website
&lt;/h2&gt;

&lt;p&gt;While the previous step was fairly straightforward, I could not find a complete explanation on how to add the generated XML to the website's files.&lt;/p&gt;

&lt;p&gt;Existing tutorials either exposed the feed as the result of an API call (which would recalculate the XML each time server-side) or modified Next.js's Webpack configuration or the build function itself.&lt;/p&gt;

&lt;p&gt;My solution was to generate the XML in the &lt;code&gt;getStaticProps&lt;/code&gt; method of my blog page.&lt;/p&gt;

&lt;p&gt;Since this page is statically generated, the method is only executed while building the project itself. &lt;br&gt;
Another benefit of this choice is that the Markdown files are only parsed once, as the &lt;code&gt;getStaticProps&lt;/code&gt; needs to extract metadata to build the page itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStaticProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetStaticProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;generateRss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./public/rss.xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: the XML file can be named however you prefer, but it must be written in the &lt;code&gt;public&lt;/code&gt; directory.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The last step is to add a link to the RSS feed inside your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tag. I added mine in the &lt;code&gt;_document.tsx&lt;/code&gt; file to make it available to all pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt;
  &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/rss+xml"&lt;/span&gt;
  &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"RSS feed for blog posts"&lt;/span&gt;
  &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://emilioschepis.com/rss.xml"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deploying these changes to your hosting platform you should check that the resulting feed is valid. You can do so using W3C's &lt;a href="https://validator.w3.org/feed/"&gt;Feed Validation Service&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outcome and conclusion
&lt;/h2&gt;

&lt;p&gt;You can find the RSS feed of my blog posts &lt;a href="https://emilioschepis.com/rss.xml"&gt;here&lt;/a&gt;. The commit for this feature can be found &lt;a href="https://github.com/emilioschepis/website/commit/7b31367ef8d3713b99f5e814fef4f077d3798d54"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I hope this post will be useful to developers trying to add this feature to their own Next.js websites.&lt;/p&gt;

&lt;p&gt;Thank you for reading!&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Learning the Go programming language by creating the Amanuense bot</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Fri, 24 Jul 2020 10:00:41 +0000</pubDate>
      <link>https://dev.to/emilioschepis/learning-the-go-programming-language-by-creating-the-amanuense-bot-1dk8</link>
      <guid>https://dev.to/emilioschepis/learning-the-go-programming-language-by-creating-the-amanuense-bot-1dk8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Recently I've felt the urge to learn a new programming language.&lt;/p&gt;

&lt;p&gt;I believe that learning a new programming language is a great opportunity to see common problems in a different light, solving them in new ways and learning something that can then be applied to languages you already know.&lt;/p&gt;

&lt;p&gt;The idea itself comes from &lt;a href="https://pragprog.com/titles/tpp20/"&gt;The Pragmatic Programmer&lt;/a&gt; book and has been discussed by many different programmers and authors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Go programming language
&lt;/h2&gt;

&lt;p&gt;After reading about different languages for a while I decided to learn the basics of the &lt;a href="https://golang.org"&gt;Go&lt;/a&gt; language.&lt;/p&gt;

&lt;p&gt;What inspired me to try this language is its philosophy that favours simplicity over (perceived) completeness, and its concurrency model.&lt;/p&gt;

&lt;p&gt;In Go there almost always is only one way of achieving the desired behavior, and that alleviates the cognitive load of deciding how to implement a specific functionality.&lt;/p&gt;

&lt;p&gt;This felt a bit weird at first, especially as I always try to write code in a functional style. Using a plain old &lt;code&gt;for-loop&lt;/code&gt; instead of a &lt;code&gt;map&lt;/code&gt; of &lt;code&gt;for-each&lt;/code&gt; took some time to get used to.&lt;/p&gt;

&lt;p&gt;This, however, is perfectly in line with their philosophy: I can now read any program written in Go and know how and why each portion of code is written the way it is.&lt;/p&gt;

&lt;p&gt;Another fundamental difference is the absence of exceptions. Code that can fail usually returns two values (the result and the error). Using the word "values" is not accidental: errors in Golang are simply values. There is no throwing or catching.&lt;/p&gt;

&lt;p&gt;I believe this error handling method will still take some time for me to get used to. Fortunately, I've used a similar structure in the past: Swift's &lt;code&gt;Result&lt;/code&gt; type and, before that, handling a tuple of values as the return type of a closure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning the basics
&lt;/h2&gt;

&lt;p&gt;When learning a new programming language I usually follow basic tutorials until I feel confident enough to write code from scratch, and at the same time I try  to watch talks and demos about the language to see how expert programmers handle common and uncommon scenarios.&lt;/p&gt;

&lt;p&gt;For the first few days I followed Jon Calhoun's&lt;br&gt;
&lt;a href="https://gophercises.com"&gt;Gophercises&lt;/a&gt;, and I absolutely recommend them. It is difficult to come up with so many different projects on our own, and the Gophercises offer very practical advice on how to handle scenarios that come up often while developing a Go program. &lt;/p&gt;

&lt;p&gt;After going through &lt;a href="https://github.com/emilioschepis/gophercises"&gt;the first six exercises&lt;/a&gt; of the course I felt ready to write my own program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the bot
&lt;/h2&gt;

&lt;p&gt;For this project I decided that I wanted to build something using Google's serverless Cloud Functions.&lt;/p&gt;

&lt;p&gt;I found out about Google's Speech-to-Text technology, and I knew what I wanted to use it for: a Telegram bot that could transcribe voice messages so that I could read them without interrupting whatever I was listening to.&lt;/p&gt;

&lt;p&gt;The bot's name is Amanuense, which is Italian for Amanuensis or Scribe.&lt;/p&gt;

&lt;p&gt;The first step to create a Go project is to initialize a module. This is useful even if you're not publishing your code as explained in &lt;a href="https://golang.org/doc/code.html"&gt;How to Write Go Code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Doing so creates the &lt;code&gt;go.mod&lt;/code&gt; and &lt;code&gt;go.sum&lt;/code&gt;. If you're familiar with NodeJS, you can think of these files as &lt;code&gt;package.json&lt;/code&gt; and its lockfile.&lt;/p&gt;

&lt;p&gt;Then, I had to add the external dependencies through the &lt;code&gt;go get&lt;/code&gt; command: &lt;code&gt;cloud.google.com/go&lt;/code&gt; and &lt;code&gt;github.com/go-telegram-bot-api/telegram-bot-api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once I finished writing the code for the bot, I simply set up a Telegram webhook to call my Cloud Function with data about the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  My experience
&lt;/h2&gt;

&lt;p&gt;My experience with Go has been very positive. I have not had the chance to try concurrency-related features, but with this simple project I've been able to get used to the language's basic features and great tooling.&lt;/p&gt;

&lt;p&gt;I've particularly enjoyed &lt;code&gt;gofmt&lt;/code&gt;, the standard formatter that is automatically applied to every Go program, further reinforcing the simplicity of reading someone else's code.&lt;/p&gt;

&lt;p&gt;Other languages' formatters are not as opinionated and are often built by third-parties, thus lowering consistency across different codebases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;downloadVoiceMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tgbotapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BotAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tgbotapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Voice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetFileDirectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not get voice file url: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not download voice file: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ioutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this snippet you can see the idiomatic way of handling errors, both as the producer (L1, L5) and the consumer (L2, L8).&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;transcribeVoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;transcription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;

    &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;extractSampleRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not instantiate speech client: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this snippet you can see how you can use methods exported by external dependencies. Methods are exported if their name starts with a capital letter.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;defer&lt;/code&gt; statement ensures that the speech client gets correctly closed regardless of how / where the function returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;I plan on continuing to learn Go and on using it in some of my side projects.&lt;br&gt;
The next step I want to take is to get familiar with Go's testing suite.&lt;/p&gt;

&lt;p&gt;In particular, I'd like to explore the concept of&lt;br&gt;
&lt;a href="https://github.com/golang/go/wiki/TableDrivenTests"&gt;table-driven tests&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This has been my first experience with the Go programming language.&lt;/p&gt;

&lt;p&gt;I especially like the large collection of first-party tools and the  "less is more" philosophy that seems to drive the language design since its origins.&lt;/p&gt;

&lt;p&gt;If you want to take a look at the Amanuense code you can find it &lt;a href="https://github.com/emilioschepis/amanuense-go"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to use the bot and you speak Italian you can find it &lt;a href="https://t.me/amanuensebot"&gt;here&lt;/a&gt;. Go's Speech-to-Text library still does not support multiple languages, but I'll make sure to update the bot once that feature is available.&lt;/p&gt;

&lt;p&gt;If you are interested in the language I recommend reading the docs and checking out some online resources like the talks on the &lt;a href="https://www.youtube.com/c/GopherAcademy/videos"&gt;Gopher Academy&lt;/a&gt;, the videos on &lt;a href="https://www.youtube.com/channel/UC_BzFbxG2za3bp5NRRRXJSw"&gt;justforfunc&lt;/a&gt; and &lt;a href="https://www.youtube.com/channel/UCwFl9Y49sWChrddQTD9QhRA"&gt;TutorialEdge&lt;/a&gt;, and the &lt;a href="https://gophercises.com"&gt;Gophercises&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thank you for reading!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>googlecloud</category>
      <category>bot</category>
    </item>
    <item>
      <title>How I used SwiftUI to create Devui, a companion app for DEV authors</title>
      <dc:creator>Emilio Schepis</dc:creator>
      <pubDate>Thu, 16 Apr 2020 16:36:40 +0000</pubDate>
      <link>https://dev.to/emilioschepis/how-i-used-swiftui-to-create-devui-a-companion-app-for-dev-authors-579l</link>
      <guid>https://dev.to/emilioschepis/how-i-used-swiftui-to-create-devui-a-companion-app-for-dev-authors-579l</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;Hi, my name is Emilio. I'm a software developer living in Milan, Italy.&lt;/p&gt;

&lt;p&gt;For a few months now I have been thinking about starting to write about software development, new technologies and my personal journey as a developer.&lt;/p&gt;

&lt;p&gt;Last year I completed the &lt;a href="https://github.com/emilioschepis/100-days-of-swiftui"&gt;100 Days of SwiftUI&lt;/a&gt; journey, and I've been thinking about using it to create an app that I could share with the world ever since.&lt;/p&gt;

&lt;p&gt;My goal was to create a simple app that would use all the latest technologies in the iOS development world, the best practices I learned in the past few months, and that could be a good "platform citizen".&lt;/p&gt;

&lt;p&gt;Also, I wanted it to be &lt;a href="https://github.com/emilioschepis/devui-app"&gt;completely open source&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The app
&lt;/h2&gt;

&lt;p&gt;Meet &lt;strong&gt;Devui&lt;/strong&gt;, a companion app for DEV authors.&lt;/p&gt;

&lt;p&gt;Devui shows you the available statistics for all your posts: page views, positive reactions and number of comments.&lt;/p&gt;

&lt;p&gt;I've been developing it in my free time over the past week, and now I think I've reached a nice MVP that features pagination, caching, system logging, dark mode support, first-class accessibility and more.&lt;/p&gt;

&lt;p&gt;Do you write on DEV? Would you like to try Devui to keep an eye on your articles?&lt;/p&gt;

&lt;p&gt;📲 &lt;strong&gt;You can join the public TestFlight beta &lt;a href="https://testflight.apple.com/join/ZThoD9Q3"&gt;here&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Show me some code!
&lt;/h2&gt;

&lt;p&gt;This app utilizes the brand new &lt;a href="https://developer.apple.com/documentation/combine"&gt;Combine&lt;/a&gt; framework to handle every aspect of the application flow with a reactive approach.&lt;/p&gt;

&lt;p&gt;All network requests expose publishers that can be observed, manipulated and then used to define the state of the application at any given time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DEVService.swift&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataTaskPublisher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tryMap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Profile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;NetworkError&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;receiveOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profileCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;forKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eraseToAnyPublisher&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This, combined with the declarative nature of the SwiftUI framework, allows the entire program to always be in a well-defined and deterministic state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ArticlesListViewModel.swift&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="n"&gt;devService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getArticles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cancellables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ArticlesListView.swift&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="kt"&gt;ArticlesListItemView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onAppear&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onArticleAppear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI utilizes system-defined colors to adapt not only to iOS 13's dark mode, but also to the numerous accessibility settings that can be enabled by users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ArticlesListItemView.swift&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;tagsList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;ScrollView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;showsIndicators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;footnote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UIColor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondaryLabel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/"&gt;SF Symbols&lt;/a&gt; are used throughout the various views to ensure consistency and familiarity with the entire Apple ecosystem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ArticlesListItemInteractionsView.swift&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="kt"&gt;HStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"bubble.right.fill"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commentsCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only external dependency is &lt;a href="https://github.com/hmlongco/Resolver"&gt;Resolver&lt;/a&gt;, an ultralight dependency injection framework.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AppDelegate+Injection.swift&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="kt"&gt;Resolver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;registerServices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;register&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;AuthService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;register&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;DEVService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;register&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;ImagesService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combination of all these features allow for an incredibly small bundle size (less than 800KB as of 1.0.0) and fast performances for all devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Conclusion
&lt;/h2&gt;

&lt;p&gt;Thank you for reading my first ever DEV article. &lt;/p&gt;

&lt;p&gt;I hope you enjoyed a glimpse into this small project. I'm looking forward to reading your comments.&lt;/p&gt;

&lt;p&gt;📲 &lt;strong&gt;If you'd like to join the public TestFlight beta you can do so &lt;a href="https://testflight.apple.com/join/ZThoD9Q3"&gt;here&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;If you'd like to take a look at the source code you can do so &lt;a href="https://github.com/emilioschepis/devui-app"&gt;here&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;🌍 &lt;strong&gt;If you'd like to help me translate this into as many languages as possible please &lt;a href="https://github.com/emilioschepis/devui-app/pulls"&gt;open a PR&lt;/a&gt; or leave a comment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Until the next one. 👋🏻&lt;/p&gt;

&lt;p&gt;- &lt;strong&gt;Emilio&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>opensource</category>
      <category>meta</category>
    </item>
  </channel>
</rss>
